This commit is contained in:
Hongbo Wu 2023-09-04 19:35:11 +08:00
parent 65bb4aa85f
commit 3f572ea89a
36 changed files with 562 additions and 1286 deletions

View file

@ -1,235 +0,0 @@
// Define the type of the body for the Search request
import { PubsubClient } from '../pubsub'
import { PickTuple } from '../util'
import {
DateFilter,
FieldFilter,
HasFilter,
InFilter,
LabelFilter,
NoFilter,
ReadFilter,
SortParams,
} from '../utils/search'
// Complete definition of the Search response
export interface ShardsResponse {
total: number
successful: number
failed: number
skipped: number
}
export interface Explanation {
value: number
description: string
details: Explanation[]
}
export interface SearchResponse<T> {
took: number
timed_out: boolean
_scroll_id?: string
_shards: ShardsResponse
hits: {
total: {
value: number
}
max_score: number
hits: Array<{
_index: string
_type: string
_id: string
_score: number
_source: T
_version?: number
_explanation?: Explanation
fields?: never
highlight?: never
inner_hits?: unknown
matched_queries?: string[]
sort?: string[]
}>
}
aggregations?: never
}
export enum PageType {
Article = 'ARTICLE',
Book = 'BOOK',
File = 'FILE',
Profile = 'PROFILE',
Unknown = 'UNKNOWN',
Website = 'WEBSITE',
Highlights = 'HIGHLIGHTS',
Tweet = 'TWEET',
Video = 'VIDEO',
Image = 'IMAGE',
}
export enum ArticleSavingRequestStatus {
Failed = 'FAILED',
Processing = 'PROCESSING',
Succeeded = 'SUCCEEDED',
Deleted = 'DELETED',
Archived = 'ARCHIVED',
}
export enum HighlightType {
Highlight = 'HIGHLIGHT',
Redaction = 'REDACTION', // allowing people to remove text from the page
Note = 'NOTE', // allowing people to add a note at the document level
}
export interface Label {
id: string
name: string
color: string
description?: string | null
createdAt?: Date
}
export interface Highlight {
id: string
shortId: string
patch?: string | null
quote?: string | null
userId: string
createdAt: Date
prefix?: string | null
suffix?: string | null
annotation?: string | null
sharedAt?: Date | null
updatedAt: Date
labels?: Label[]
highlightPositionPercent?: number | null
highlightPositionAnchorIndex?: number | null
type: HighlightType
html?: string | null
color?: string | null
}
export interface RecommendingUser {
userId: string
name: string
username: string
profileImageURL?: string | null
}
export interface Recommendation {
id: string
name: string
note?: string | null
user: RecommendingUser
recommendedAt: Date
}
export interface Page {
id: string
userId: string
title: string
author?: string
description?: string
content: string
url: string
hash: string
uploadFileId?: string | null
image?: string
pageType: PageType
originalHtml?: string | null
slug: string
labels?: Label[]
readingProgressTopPercent?: number
readingProgressPercent: number
readingProgressAnchorIndex: number
createdAt: Date
updatedAt?: Date
publishedAt?: Date
savedAt: Date
sharedAt?: Date
archivedAt?: Date | null
siteName?: string
_id?: string
siteIcon?: string
highlights?: Highlight[]
subscription?: string
unsubMailTo?: string
unsubHttpUrl?: string
state: ArticleSavingRequestStatus
taskName?: string
language?: string
readAt?: Date
listenedAt?: Date
wordsCount?: number
recommendations?: Recommendation[]
rssFeedUrl?: string
}
export interface SearchItem {
annotation?: string | null
author?: string | null
createdAt: Date
description?: string | null
id: string
image?: string | null
pageId?: string
pageType: PageType
publishedAt?: Date
quote?: string | null
shortId?: string | null
slug: string
title: string
uploadFileId?: string | null
url: string
archivedAt?: Date | null
readingProgressTopPercent?: number
readingProgressPercent: number
readingProgressAnchorIndex: number
userId: string
state?: ArticleSavingRequestStatus
language?: string
readAt?: Date
savedAt: Date
updatedAt?: Date
labels?: Label[]
highlights?: Highlight[]
wordsCount?: number
siteName?: string
siteIcon?: string
recommendations?: Recommendation[]
content?: string
}
const keys = ['_id', 'url', 'slug', 'userId', 'uploadFileId', 'state'] as const
export type ParamSet = PickTuple<Page, typeof keys>
export interface PageContext {
pubsub: PubsubClient
refresh?: boolean
uid: string
shouldPublish?: boolean
}
export interface PageSearchArgs {
from?: number
size?: number
sort?: SortParams
query?: string
inFilter?: InFilter
readFilter?: ReadFilter
typeFilter?: PageType
labelFilters?: LabelFilter[]
hasFilters?: HasFilter[]
dateFilters?: DateFilter[]
termFilters?: FieldFilter[]
matchFilters?: FieldFilter[]
includePending?: boolean | null
includeDeleted?: boolean
ids?: string[]
recommendedBy?: string
includeContent?: boolean
noFilters?: NoFilter[]
siteName?: string
}

View file

@ -1,4 +0,0 @@
import { entityManager } from '.'
import { Group } from '../entity/groups/group'
export const groupRepository = entityManager.getRepository(Group)

View file

@ -1,5 +1,5 @@
import * as httpContext from 'express-http-context2'
import { EntityManager } from 'typeorm'
import { EntityManager, EntityTarget } from 'typeorm'
import { appDataSource } from '../data_source'
import { Claims } from '../resolvers/types'
@ -33,4 +33,8 @@ export const authTrx = async <T>(
})
}
export const getRepository = <T>(entity: EntityTarget<T>) => {
return entityManager.getRepository(entity)
}
export const entityManager = appDataSource.manager

View file

@ -1,4 +0,0 @@
import { Profile } from '../entity/profile'
import { entityManager } from '.'
export const profileRepository = entityManager.getRepository(Profile)

View file

@ -13,21 +13,21 @@ import {
RevokeApiKeyErrorCode,
RevokeApiKeySuccess,
} from '../../generated/graphql'
import { getRepository } from '../../repository'
import { analytics } from '../../utils/analytics'
import { generateApiKey, hashApiKey } from '../../utils/auth'
import { authorized } from '../../utils/helpers'
export const apiKeysResolver = authorized<ApiKeysSuccess, ApiKeysError>(
async (_, __, { log, authTrx }) => {
async (_, __, { log, uid }) => {
try {
const apiKeys = await authTrx(async (tx) => {
return tx.getRepository(ApiKey).find({
select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'],
order: {
usedAt: { direction: 'DESC', nulls: 'last' },
createdAt: 'DESC',
},
})
const apiKeys = await getRepository(ApiKey).find({
select: ['id', 'name', 'scopes', 'expiresAt', 'createdAt', 'usedAt'],
where: { user: { id: uid } },
order: {
usedAt: { direction: 'DESC', nulls: 'last' },
createdAt: 'DESC',
},
})
return {
@ -47,17 +47,15 @@ export const generateApiKeyResolver = authorized<
GenerateApiKeySuccess,
GenerateApiKeyError,
MutationGenerateApiKeyArgs
>(async (_, { input: { name, expiresAt } }, { authTrx, log, uid }) => {
>(async (_, { input: { name, expiresAt } }, { log, uid }) => {
try {
const exp = new Date(expiresAt)
const originalKey = generateApiKey()
const apiKeyCreated = await authTrx(async (tx) => {
return tx.getRepository(ApiKey).save({
user: { id: uid },
name,
key: hashApiKey(originalKey),
expiresAt: exp,
})
const apiKeyCreated = await getRepository(ApiKey).save({
user: { id: uid },
name,
key: hashApiKey(originalKey),
expiresAt: exp,
})
analytics.track({
@ -89,22 +87,17 @@ export const revokeApiKeyResolver = authorized<
MutationRevokeApiKeyArgs
>(async (_, { id }, { claims: { uid }, log, authTrx }) => {
try {
const deletedApiKey = await authTrx(async (tx) => {
const apiRepo = tx.getRepository(ApiKey)
const apiKey = await apiRepo.findOneBy({ id })
if (!apiKey) {
return null
}
const apiRepo = getRepository(ApiKey)
return apiRepo.remove(apiKey)
})
if (!deletedApiKey) {
const apiKey = await apiRepo.findOneBy({ id, user: { id: uid } })
if (!apiKey) {
return {
errorCodes: [RevokeApiKeyErrorCode.NotFound],
}
}
const deletedApiKey = await apiRepo.remove(apiKey)
analytics.track({
userId: uid,
event: 'api_key_revoked',

View file

@ -3,7 +3,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { getPageByParam } from '../elastic/pages'
import { Subscription } from '../entity/subscription'
import { Article, PageType, SearchItem } from '../generated/graphql'
import { findUploadFileById } from '../services/upload_file'
@ -109,7 +108,7 @@ import {
} from './index'
import { markEmailAsItemResolver, recentEmailsResolver } from './recent_emails'
import { recentSearchesResolver } from './recent_searches'
import { Claims, WithDataSourcesContext } from './types'
import { WithDataSourcesContext } from './types'
import { updateEmailResolver } from './user'
/* eslint-disable @typescript-eslint/naming-convention */
@ -387,36 +386,6 @@ export const functionResolvers = {
async originalArticleUrl(article: { url: string }) {
return article.url
},
async savedByViewer(
article: { id: string; savedByViewer?: boolean },
__: unknown,
ctx: WithDataSourcesContext & { claims: Claims }
) {
if (article.savedByViewer) {
return article.savedByViewer
}
if (!ctx.claims?.uid) return undefined
const page = await getPageByParam({
userId: ctx.claims.uid,
_id: article.id,
})
return !!page
},
async postedByViewer(
article: { id: string; postedByViewer?: boolean },
__: unknown,
ctx: WithDataSourcesContext & { claims: Claims }
) {
if (article.postedByViewer) {
return article.postedByViewer
}
if (!ctx.claims?.uid) return false
const page = await getPageByParam({
userId: ctx.claims.uid,
_id: article.id,
})
return !!page?.sharedAt
},
hasContent(article: {
content: string | null
originalHtml: string | null

View file

@ -13,6 +13,7 @@ import {
NewsletterEmailsErrorCode,
NewsletterEmailsSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../repository'
import {
createNewsletterEmail,
deleteNewsletterEmail,
@ -57,7 +58,6 @@ export const newsletterEmailsResolver = authorized<
NewsletterEmailsSuccess,
NewsletterEmailsError
>(async (_parent, _args, { uid, log }) => {
try {
const newsletterEmails = await getNewsletterEmails(uid)

View file

@ -11,6 +11,7 @@ import {
RecentEmailsErrorCode,
RecentEmailsSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../repository'
import { updateReceivedEmail } from '../../services/received_emails'
import { saveNewsletter } from '../../services/save_newsletter_email'
import { authorized } from '../../utils/helpers'

View file

@ -1,7 +1,5 @@
import { In } from 'typeorm'
import { getPageByParam } from '../../elastic/pages'
import { Group } from '../../entity/groups/group'
import { User } from '../../entity/user'
import { env } from '../../env'
import {
CreateGroupError,
@ -28,6 +26,7 @@ import {
RecommendHighlightsSuccess,
RecommendSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../repository'
import { userRepository } from '../../repository/user'
import {
createGroup,
@ -38,6 +37,7 @@ import {
joinGroup,
leaveGroup,
} from '../../services/groups'
import { findLibraryItemById } from '../../services/library_item'
import { analytics } from '../../utils/analytics'
import { enqueueRecommendation } from '../../utils/createTask'
import { authorized, userDataToUser } from '../../utils/helpers'
@ -167,7 +167,7 @@ export const recommendResolver = authorized<
RecommendSuccess,
RecommendError,
MutationRecommendArgs
>(async (_, { input }, { claims: { uid }, log, signToken }) => {
>(async (_, { input }, { uid, log, signToken }) => {
log.info('Recommend', {
input,
labels: {
@ -178,18 +178,8 @@ export const recommendResolver = authorized<
})
try {
const user = await userRepository.findOne({
where: { id: uid },
relations: ['profile'],
})
if (!user) {
return {
errorCodes: [RecommendErrorCode.Unauthorized],
}
}
const page = await getPageByParam({ _id: input.pageId, userId: uid })
if (!page) {
const item = await findLibraryItemById(input.pageId, uid)
if (!item) {
return {
errorCodes: [RecommendErrorCode.NotFound],
}
@ -205,7 +195,7 @@ export const recommendResolver = authorized<
// only recommend highlights created by the user
const recommendedHighlightIds = input.recommendedWithHighlights
? page.highlights?.filter((h) => h.userId === uid)?.map((h) => h.id)
? item.highlights?.filter((h) => h.user.id === uid)?.map((h) => h.id)
: undefined
const exp = Math.floor(Date.now() / 1000) + 60 * 60 * 24 // 1 day
@ -216,18 +206,13 @@ export const recommendResolver = authorized<
group.members.map((member) =>
enqueueRecommendation(
member.user.id,
page.id,
item.id,
{
id: group.id,
name: group.name,
note: input.note ?? null,
user: {
userId: user.id,
name: user.name,
username: user.profile.username,
profileImageURL: user.profile.pictureUrl,
},
recommendedAt: new Date(),
recommender: item.user,
createdAt: new Date(),
libraryItem: item,
},
auth,
recommendedHighlightIds
@ -318,7 +303,7 @@ export const recommendHighlightsResolver = authorized<
RecommendHighlightsSuccess,
RecommendHighlightsError,
MutationRecommendHighlightsArgs
>(async (_, { input }, { claims: { uid }, log, signToken }) => {
>(async (_, { input }, { uid, log, signToken }) => {
log.info('Recommend highlights', {
input,
labels: {
@ -349,8 +334,8 @@ export const recommendHighlightsResolver = authorized<
}
}
const page = await getPageByParam({ _id: input.pageId, userId: uid })
if (!page) {
const item = await findLibraryItemById(input.pageId, uid)
if (!item) {
return {
errorCodes: [RecommendHighlightsErrorCode.NotFound],
}
@ -366,18 +351,13 @@ export const recommendHighlightsResolver = authorized<
.map((member) =>
enqueueRecommendation(
member.user.id,
page.id,
item.id,
{
id: group.id,
name: group.name,
note: input.note,
user: {
userId: user.id,
name: user.name,
username: user.profile.username,
profileImageURL: user.profile.pictureUrl,
},
recommendedAt: new Date(),
recommender: user,
createdAt: new Date(),
libraryItem: item,
},
auth,
input.highlightIds

View file

@ -1,5 +1,4 @@
import { Rule } from '../../entity/rule'
import { User } from '../../entity/user'
import {
DeleteRuleError,
DeleteRuleErrorCode,
@ -14,48 +13,28 @@ import {
SetRuleErrorCode,
SetRuleSuccess,
} from '../../generated/graphql'
import { getRepository } from '../../repository'
import { authorized } from '../../utils/helpers'
export const setRuleResolver = authorized<
SetRuleSuccess,
SetRuleError,
MutationSetRuleArgs
>(async (_, { input }, { claims, log }) => {
log.info('Setting rules', {
input,
labels: {
source: 'resolver',
resolver: 'setRulesResolver',
uid: claims.uid,
},
})
>(async (_, { input }, { authTrx, uid, log }) => {
try {
const user = await getRepository(User).findOneBy({ id: claims.uid })
if (!user) {
return {
errorCodes: [SetRuleErrorCode.Unauthorized],
}
}
const rule = await getRepository(Rule).save({
...input,
id: input.id || undefined,
user: { id: claims.uid },
})
const rule = await authTrx((t) =>
t.getRepository(Rule).save({
...input,
id: input.id || undefined,
user: { id: uid },
})
)
return {
rule,
}
} catch (error) {
log.error('Error setting rules', {
error,
labels: {
source: 'resolver',
resolver: 'setRulesResolver',
uid: claims.uid,
},
})
log.error('Error setting rules', error)
return {
errorCodes: [SetRuleErrorCode.BadRequest],
@ -68,23 +47,7 @@ export const rulesResolver = authorized<
RulesError,
QueryRulesArgs
>(async (_, { enabled }, { claims, log }) => {
log.info('Getting rules', {
enabled,
labels: {
source: 'resolver',
resolver: 'rulesResolver',
uid: claims.uid,
},
})
try {
const user = await getRepository(User).findOneBy({ id: claims.uid })
if (!user) {
return {
errorCodes: [RulesErrorCode.Unauthorized],
}
}
const rules = await getRepository(Rule).findBy({
user: { id: claims.uid },
enabled: enabled === null ? undefined : enabled,

View file

@ -1,8 +1,6 @@
import Parser from 'rss-parser'
import { Brackets } from 'typeorm'
import { appDataSource } from '../../data_source'
import { Subscription } from '../../entity/subscription'
import { User } from '../../entity/user'
import { env } from '../../env'
import {
MutationSubscribeArgs,
@ -120,24 +118,28 @@ export const unsubscribeResolver = authorized<
UnsubscribeSuccessPartial,
UnsubscribeError,
MutationUnsubscribeArgs
>(async (_, { name, subscriptionId }, { claims: { uid }, log }) => {
>(async (_, { name, subscriptionId }, { authTrx, uid, log }) => {
log.info('unsubscribeResolver')
try {
const queryBuilder = getRepository(Subscription)
.createQueryBuilder('subscription')
.leftJoinAndSelect('subscription.newsletterEmail', 'newsletterEmail')
.where({ user: { id: uid } })
const subscription = await authTrx(async (t) => {
const queryBuilder = t
.getRepository(Subscription)
.createQueryBuilder('subscription')
.leftJoinAndSelect('subscription.newsletterEmail', 'newsletterEmail')
.where({ user: { id: uid } })
if (subscriptionId) {
// if subscriptionId is provided, ignore name
queryBuilder.andWhere({ id: subscriptionId })
} else {
// if subscriptionId is not provided, use name for old clients
queryBuilder.andWhere({ name })
}
if (subscriptionId) {
// if subscriptionId is provided, ignore name
queryBuilder.andWhere({ id: subscriptionId })
} else {
// if subscriptionId is not provided, use name for old clients
queryBuilder.andWhere({ name })
}
return queryBuilder.getOne()
})
const subscription = await queryBuilder.getOne()
if (!subscription) {
return {
errorCodes: [UnsubscribeErrorCode.NotFound],
@ -189,25 +191,20 @@ export const subscribeResolver = authorized<
SubscribeSuccessPartial,
SubscribeError,
MutationSubscribeArgs
>(async (_, { input }, { claims: { uid }, log }) => {
>(async (_, { input }, { authTrx, uid, log }) => {
log.info('subscribeResolver')
try {
const user = await getRepository(User).findOneBy({ id: uid })
if (!user) {
return {
errorCodes: [SubscribeErrorCode.Unauthorized],
}
}
// find existing subscription
const subscription = await getRepository(Subscription).findOneBy({
url: input.url || undefined,
name: input.name || undefined,
user: { id: uid },
status: SubscriptionStatus.Active,
type: input.subscriptionType || SubscriptionType.Rss, // default to rss
})
const subscription = await authTrx((t) =>
t.getRepository(Subscription).findOneBy({
url: input.url || undefined,
name: input.name || undefined,
user: { id: uid },
status: SubscriptionStatus.Active,
type: input.subscriptionType || SubscriptionType.Rss, // default to rss
})
)
if (subscription) {
return {
errorCodes: [SubscribeErrorCode.AlreadySubscribed],
@ -223,30 +220,6 @@ export const subscribeResolver = authorized<
},
})
// create new newsletter subscription
if (input.name && input.subscriptionType === SubscriptionType.Newsletter) {
const subscribeHandler = getSubscribeHandler(input.name)
if (!subscribeHandler) {
return {
errorCodes: [SubscribeErrorCode.NotFound],
}
}
const newSubscriptions = await subscribeHandler.handleSubscribe(
uid,
input.name
)
if (!newSubscriptions) {
return {
errorCodes: [SubscribeErrorCode.BadRequest],
}
}
return {
subscriptions: newSubscriptions,
}
}
// create new rss subscription
if (input.url) {
const MAX_RSS_SUBSCRIPTIONS = 150
@ -254,21 +227,23 @@ export const subscribeResolver = authorized<
const feed = await parser.parseURL(input.url)
// limit number of rss subscriptions to 50
const newSubscriptions = (await appDataSource.query(
`insert into omnivore.subscriptions (name, url, description, type, user_id, icon)
const newSubscriptions = (await authTrx((t) =>
t.query(
`insert into omnivore.subscriptions (name, url, description, type, user_id, icon)
select $1, $2, $3, $4, $5, $6 from omnivore.subscriptions
where user_id = $5 and type = 'RSS' and status = 'ACTIVE'
having count(*) < $7
returning *;`,
[
feed.title,
input.url,
feed.description || null,
SubscriptionType.Rss,
uid,
feed.image?.url || null,
MAX_RSS_SUBSCRIPTIONS,
]
[
feed.title,
input.url,
feed.description || null,
SubscriptionType.Rss,
uid,
feed.image?.url || null,
MAX_RSS_SUBSCRIPTIONS,
]
)
)) as Subscription[]
if (newSubscriptions.length === 0) {

View file

@ -1,53 +1,39 @@
import { getPageById, updatePage } from '../../elastic/pages'
import { Page } from '../../elastic/types'
import { LibraryItem, LibraryItemState } from '../../entity/library_item'
import {
MutationUpdatePageArgs,
UpdatePageError,
UpdatePageErrorCode,
UpdatePageSuccess,
} from '../../generated/graphql'
import { updateLibraryItem } from '../../services/library_item'
import { Merge } from '../../util'
import { authorized } from '../../utils/helpers'
export type UpdatePageSuccessPartial = Merge<
UpdatePageSuccess,
{ updatedPage: Partial<Page> }
{ updatedPage: Partial<LibraryItem> }
>
export const updatePageResolver = authorized<
UpdatePageSuccessPartial,
UpdatePageError,
MutationUpdatePageArgs
>(async (_, { input }, ctx) => {
const { pubsub, uid } = ctx
const page = await getPageById(input.pageId)
if (!page) {
return { errorCodes: [UpdatePageErrorCode.NotFound] }
}
const pageData = {
id: input.pageId,
title: input.title ?? undefined,
description: input.description ?? undefined,
author: input.byline ?? undefined,
savedAt: input.savedAt ? new Date(input.savedAt) : undefined,
publishedAt: input.publishedAt ? new Date(input.publishedAt) : undefined,
image: input.previewImage ?? undefined,
state: input.state ?? undefined,
}
const updateResult = await updatePage(input.pageId, pageData, {
pubsub: pubsub,
uid,
refresh: true,
})
if (!updateResult) return { errorCodes: [UpdatePageErrorCode.UpdateFailed] }
const updatedPage = (await getPageById(input.pageId)) as unknown as Page
>(async (_, { input }, { uid }) => {
const updatedPage = await updateLibraryItem(
input.pageId,
{
title: input.title ?? undefined,
description: input.description ?? undefined,
author: input.byline ?? undefined,
savedAt: input.savedAt ? new Date(input.savedAt) : undefined,
publishedAt: input.publishedAt ? new Date(input.publishedAt) : undefined,
thumbnail: input.previewImage ?? undefined,
state: input.state
? (input.state as unknown as LibraryItemState)
: undefined,
},
uid
)
return {
updatedPage: updatedPage,
__typename: 'UpdatePageSuccess',
updatedPage,
}
})

View file

@ -18,9 +18,10 @@ import { appDataSource } from '../../data_source'
import { RegistrationType, StatusType, User } from '../../entity/user'
import { env } from '../../env'
import { LoginErrorCode, SignupErrorCode } from '../../generated/graphql'
import { getRepository, setClaims, userRepository } from '../../repository'
import { getRepository, setClaims } from '../../repository'
import { userRepository } from '../../repository/user'
import { isErrorWithCode } from '../../resolvers'
import { createUser, getUserByEmail } from '../../services/create_user'
import { createUser } from '../../services/create_user'
import {
sendConfirmationEmail,
sendPasswordResetEmail,
@ -419,7 +420,7 @@ export function authRouter() {
}
const { email, password } = req.body
try {
const user = await getUserByEmail(email.trim())
const user = await userRepository.findByEmail(email.trim())
if (!user?.id) {
return res.redirect(
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.UserNotFound}`
@ -608,7 +609,7 @@ export function authRouter() {
}
try {
const user = await getUserByEmail(email)
const user = await userRepository.findByEmail(email)
if (!user) {
return res.redirect(`${env.client.url}/auth/reset-sent`)
}

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { StatusType } from '../../../entity/user'
import { getUserByEmail } from '../../../services/create_user'
import { userRepository } from '../../../repository/user'
import { sendConfirmationEmail } from '../../../services/send_emails'
import { comparePassword } from '../../../utils/auth'
import { logger } from '../../../utils/logger'
@ -45,7 +45,7 @@ export async function createMobileEmailSignInResponse(
throw new Error('Missing username or password')
}
const user = await getUserByEmail(email.trim())
const user = await userRepository.findByEmail(email.trim())
if (!user?.id || !user?.password) {
throw new Error('user not found')
}

View file

@ -2,7 +2,7 @@ import cors from 'cors'
import express, { Router } from 'express'
import { env } from '../env'
import { LoginErrorCode } from '../generated/graphql'
import { userRepository } from '../repository'
import { userRepository } from '../repository/user'
import { createUser } from '../services/create_user'
import { corsConfig } from '../utils/corsConfig'
import { createWebAuthToken } from './auth/jwt_helpers'

View file

@ -5,18 +5,19 @@
import cors from 'cors'
import express from 'express'
import * as jwt from 'jsonwebtoken'
import { createPage, getPageByParam, updatePage } from '../elastic/pages'
import { addRecommendation } from '../elastic/recommendation'
import { Recommendation } from '../elastic/types'
import { LibraryItemState, LibraryItemType } from '../entity/library_item'
import { Recommendation } from '../entity/recommendation'
import { UploadFile } from '../entity/upload_file'
import { env } from '../env'
import {
ArticleSavingRequestStatus,
PageType,
UploadFileStatus,
} from '../generated/graphql'
import { createPubSubClient } from '../pubsub'
import { uploadFileRepository } from '../repository'
import { UploadFileStatus } from '../generated/graphql'
import { authTrx } from '../repository'
import { Claims } from '../resolvers/types'
import {
createLibraryItem,
findLibraryItemById,
findLibraryItemByUrl,
updateLibraryItem,
} from '../services/library_item'
import { getTokenByRequest } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'
import {
@ -75,20 +76,20 @@ export function pageRouter() {
return res.status(400).send({ errorCode: 'BAD_DATA' })
}
const ctx = {
uid: claims.uid,
pubsub: createPubSubClient(),
}
const title = titleForFilePath(url)
const fileName = fileNameForFilePath(url)
const uploadFileData = await uploadFileRepository.save({
url,
userId: claims.uid,
fileName,
status: UploadFileStatus.Initialized,
contentType: 'application/pdf',
})
const uploadFileData = await authTrx(
(t) =>
t.getRepository(UploadFile).save({
url,
userId: claims.uid,
fileName,
status: UploadFileStatus.Initialized,
contentType: 'application/pdf',
}),
undefined,
claims.uid
)
const uploadFilePathName = generateUploadFilePathName(
uploadFileData.id,
@ -100,45 +101,35 @@ export function pageRouter() {
'application/pdf'
)
const page = await getPageByParam({
userId: claims.uid,
url: url,
})
const item = await findLibraryItemByUrl(url, claims.uid)
if (page) {
if (item) {
logger.info('updating page')
await updatePage(
page.id,
await updateLibraryItem(
item.id,
{
savedAt: new Date(),
archivedAt: null,
state: LibraryItemState.Succeeded,
},
ctx
claims.uid
)
} else {
logger.info('creating page')
const pageId = await createPage(
await createLibraryItem(
{
url: signedUrl,
originalUrl: signedUrl,
id: clientRequestId,
userId: claims.uid,
title: title,
hash: uploadFilePathName,
content: '',
pageType: PageType.File,
uploadFileId: uploadFileData.id,
user: { id: claims.uid },
title,
originalContent: '',
itemType: LibraryItemType.File,
uploadFile: { id: uploadFileData.id },
slug: generateSlug(uploadFilePathName),
createdAt: new Date(),
savedAt: new Date(),
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
state: ArticleSavingRequestStatus.Processing,
state: LibraryItemState.Processing,
},
ctx
claims.uid
)
if (!pageId) {
return res.sendStatus(500)
}
}
logger.info('redirecting to signed URL', signedUrl)
@ -170,16 +161,8 @@ export function pageRouter() {
return res.status(400).send({ errorCode: 'BAD_DATA' })
}
const ctx = {
uid: userId,
pubsub: createPubSubClient(),
}
const page = await getPageByParam({
userId: claims.uid,
_id: pageId,
})
if (!page) {
const item = await findLibraryItemById(pageId, userId)
if (!item) {
return res.status(404).send({ errorCode: 'NOT_FOUND' })
}

View file

@ -2,11 +2,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import express from 'express'
import { getPageByParam, updatePage } from '../../elastic/pages'
import { Page } from '../../elastic/types'
import { ArticleSavingRequestStatus } from '../../generated/graphql'
import { createPubSubClient, readPushSubscription } from '../../pubsub'
import { LibraryItem, LibraryItemState } from '../../entity/library_item'
import { readPushSubscription } from '../../pubsub'
import { authTrx } from '../../repository'
import { libraryItemRepository } from '../../repository/library_item'
import { updateLibraryItem } from '../../services/library_item'
import { setFileUploadComplete } from '../../services/upload_file'
import { logger } from '../../utils/logger'
@ -27,7 +27,7 @@ export function contentServiceRouter() {
logger.info('read pubsub message', msgStr, 'has expired', expired)
if (!msgStr) {
res.status(400).send('Bad Request')
res.status(200).send('Bad Request')
return
}
@ -40,7 +40,7 @@ export function contentServiceRouter() {
const data = JSON.parse(msgStr)
if (!('fileId' in data) || !('content' in data)) {
logger.info('No file id or content found in message')
res.status(400).send('Bad Request')
res.status(200).send('Bad Request')
return
}
const msg = data as UpdateContentMessage
@ -50,43 +50,53 @@ export function contentServiceRouter() {
const fileId = parts && parts.length > 1 ? parts[1] : undefined
if (!fileId) {
logger.info('No file id found in message')
res.status(400).send('Bad Request')
res.status(200).send('Bad Request')
return
}
const page = await getPageByParam({ uploadFileId: fileId })
if (!page) {
const libraryItem = await authTrx(async (tx) =>
tx
.withRepository(libraryItemRepository)
.createQueryBuilder('item')
.innerJoinAndSelect('item.user', 'user')
.innerJoinAndSelect('item.uploadFile', 'file')
.where('item.fileId = :fileId', { fileId })
.getOne()
)
if (!libraryItem) {
logger.info('No upload file found for id:', fileId)
res.status(400).send('Bad Request')
return
}
const pageToUpdate: Partial<Page> = { content: msg.content }
if (msg.title) pageToUpdate.title = msg.title
if (msg.author) pageToUpdate.author = msg.author
if (msg.description) pageToUpdate.description = msg.description
const itemToUpdate: Partial<LibraryItem> = { originalContent: msg.content }
if (msg.title) itemToUpdate.title = msg.title
if (msg.author) itemToUpdate.author = msg.author
if (msg.description) itemToUpdate.description = msg.description
// This event is fired after the file is fully uploaded,
// so along with upadting content, we mark it as
// so along with updateing content, we mark it as
// succeeded.
pageToUpdate.state = ArticleSavingRequestStatus.Succeeded
itemToUpdate.state = LibraryItemState.Succeeded
try {
const uploadFileData = await authTrx(async (t) =>
setFileUploadComplete(fileId)
const uploadFileData = await setFileUploadComplete(
fileId,
libraryItem.user.id
)
logger.info('updated uploadFileData', uploadFileData)
} catch (error) {
logger.info('error marking file upload as completed', error)
}
const result = await updatePage(page.id, pageToUpdate, {
pubsub: createPubSubClient(),
uid: page.userId,
})
const result = await updateLibraryItem(
libraryItem.id,
itemToUpdate,
libraryItem.user.id
)
logger.info(
'Updating article text',
page.id,
'Updating library item text',
libraryItem.id,
result,
msg.content.substring(0, 20)
)

View file

@ -1,14 +1,21 @@
import express from 'express'
import { appDataSource } from '../../data_source'
import { createPage } from '../../elastic/pages'
import { ArticleSavingRequestStatus, Page } from '../../elastic/types'
import { DeepPartial } from 'typeorm'
import {
LibraryItem,
LibraryItemState,
LibraryItemType,
} from '../../entity/library_item'
import { UploadFile } from '../../entity/upload_file'
import { env } from '../../env'
import { PageType, UploadFileStatus } from '../../generated/graphql'
import { createPubSubClient } from '../../pubsub'
import { setClaims, uploadFileRepository } from '../../repository'
import { UploadFileStatus } from '../../generated/graphql'
import { authTrx } from '../../repository'
import { createLibraryItem } from '../../services/library_item'
import { getNewsletterEmail } from '../../services/newsletters'
import { updateReceivedEmail } from '../../services/received_emails'
import { setFileUploadComplete } from '../../services/save_file'
import {
findUploadFileById,
setFileUploadComplete,
} from '../../services/upload_file'
import { analytics } from '../../utils/analytics'
import { getClaimsByToken } from '../../utils/auth'
import { generateSlug } from '../../utils/helpers'
@ -54,13 +61,15 @@ export function emailAttachmentRouter() {
})
try {
const uploadFileData = await uploadFileRepository.save({
url: '',
userId: user.id,
fileName: fileName,
status: UploadFileStatus.Initialized,
contentType: contentType,
})
const uploadFileData = await authTrx((tx) =>
tx.getRepository(UploadFile).save({
url: '',
userId: user.id,
fileName: fileName,
status: UploadFileStatus.Initialized,
contentType: contentType,
})
)
if (uploadFileData.id) {
const uploadFilePathName = generateUploadFilePathName(
@ -116,10 +125,7 @@ export function emailAttachmentRouter() {
})
try {
const uploadFile = await uploadFileRepository.findOneBy({
id: uploadFileId,
user: { id: user.id },
})
const uploadFile = await findUploadFileById(uploadFileId)
if (!uploadFile) {
return res.status(400).send('BAD REQUEST')
}
@ -129,10 +135,7 @@ export function emailAttachmentRouter() {
uploadFile.fileName
)
const uploadFileData = await appDataSource.transaction(async (tx) => {
await setClaims(tx, user.id)
return setFileUploadComplete(uploadFileId, tx)
})
const uploadFileData = await setFileUploadComplete(uploadFileId, user.id)
if (!uploadFileData || !uploadFileData.id || !uploadFileData.fileName) {
return res.status(400).send('BAD REQUEST')
}
@ -143,32 +146,23 @@ export function emailAttachmentRouter() {
)
const uploadFileHash = uploadFileDetails.md5Hash
const pageType =
const itemType =
uploadFile.contentType === 'application/pdf'
? PageType.File
: PageType.Book
? LibraryItemType.File
: LibraryItemType.Book
const title = subject || uploadFileData.fileName
const articleToSave: Page = {
id: '',
url: uploadFileUrlOverride,
pageType,
hash: uploadFileHash,
uploadFileId,
const articleToSave: DeepPartial<LibraryItem> = {
originalUrl: uploadFileUrlOverride,
itemType,
textContentHash: uploadFileHash,
uploadFile: { id: uploadFileData.id },
title,
content: '',
userId: user.id,
readableContent: '',
slug: generateSlug(title),
createdAt: new Date(),
savedAt: new Date(),
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
state: ArticleSavingRequestStatus.Succeeded,
state: LibraryItemState.Succeeded,
}
const pageId = await createPage(articleToSave, {
pubsub: createPubSubClient(),
uid: user.id,
})
const pageId = await createLibraryItem(articleToSave, user.id)
// update received email type
await updateReceivedEmail(receivedEmailId, 'article')

View file

@ -5,13 +5,16 @@ import { stringify } from 'csv-stringify'
import express from 'express'
import { DateTime } from 'luxon'
import { v4 as uuidv4 } from 'uuid'
import { getPageById, searchLibraryItems } from '../../elastic/pages'
import { Page } from '../../elastic/types'
import { Integration, IntegrationType } from '../../entity/integration'
import { LibraryItem } from '../../entity/library_item'
import { EntityType, readPushSubscription } from '../../pubsub'
import { getRepository } from '../../repository'
import { Claims } from '../../resolvers/types'
import { getIntegrationService } from '../../services/integrations'
import {
findLibraryItemById,
searchLibraryItems,
} from '../../services/library_item'
import { getClaimsByToken } from '../../utils/auth'
import { logger } from '../../utils/logger'
import { DateFilter } from '../../utils/search'
@ -95,13 +98,13 @@ export function integrationsServiceRouter() {
res.status(200).send('Bad Request')
return
}
const page = await getPageById(id)
const page = await findLibraryItemById(id, userId)
if (!page) {
logger.info('No page found for id', { id })
res.status(200).send('No page found')
return
}
if (page.userId !== userId) {
if (page.user.id !== userId) {
logger.info('Page does not belong to user', { id, userId })
return res.status(200).send('Page does not belong to user')
}
@ -124,7 +127,10 @@ export function integrationsServiceRouter() {
const size = 50
for (
let hasNextPage = true, count = 0, after = 0, pages: Page[] = [];
let hasNextPage = true,
count = 0,
after = 0,
pages: LibraryItem[] = [];
hasNextPage;
after += size, hasNextPage = count > after
) {
@ -133,10 +139,11 @@ export function integrationsServiceRouter() {
const dateFilters: DateFilter[] = []
syncedAt &&
dateFilters.push({ field: 'updatedAt', startDate: syncedAt })
;[pages, count] = (await searchLibraryItems(
const { libraryItems } = await searchLibraryItems(
{ from: after, size, dateFilters },
userId
)) as [Page[], number]
)
pages = libraryItems
const pageIds = pages.map((p) => p.id)
logger.info('syncing pages', { pageIds })

View file

@ -6,7 +6,6 @@ import { homePageURL } from '../../env'
import { ContentReader } from '../../generated/graphql'
import { createPubSubClient } from '../../pubsub'
import { setClaims } from '../../repository'
import { PageReminder, setRemindersComplete } from '../../services/reminders'
interface PageToNotify {
title: string
@ -127,115 +126,115 @@ interface PageToNotify {
// return router
// }
const getPagesToNotifyAndUnarchive = (
pageReminders: PageReminder[],
username: string
): [pages: PageToNotify[], linkIds: string[]] => {
const pageIds: string[] = []
const pages: PageToNotify[] = []
pageReminders.forEach((pageReminder) => {
pageIds.push(pageReminder.pageId)
// const getPagesToNotifyAndUnarchive = (
// pageReminders: PageReminder[],
// username: string
// ): [pages: PageToNotify[], linkIds: string[]] => {
// const pageIds: string[] = []
// const pages: PageToNotify[] = []
// pageReminders.forEach((pageReminder) => {
// pageIds.push(pageReminder.pageId)
pageReminder.sendNotification &&
pages.push({
url: `${homePageURL()}/${username}/${pageReminder.slug}`,
title: pageReminder.title,
description: pageReminder.description,
byline: pageReminder.author,
image: pageReminder.image,
})
})
// pageReminder.sendNotification &&
// pages.push({
// url: `${homePageURL()}/${username}/${pageReminder.slug}`,
// title: pageReminder.title,
// description: pageReminder.description,
// byline: pageReminder.author,
// image: pageReminder.image,
// })
// })
return [pages, pageIds]
}
// return [pages, pageIds]
// }
const messageForPages = (
pageReminders: PageReminder[],
deviceTokens: UserDeviceToken[]
): MulticastMessage => {
const pages = pageReminders.filter((reminder) => reminder.sendNotification)
// const messageForPages = (
// pageReminders: PageReminder[],
// deviceTokens: UserDeviceToken[]
// ): MulticastMessage => {
// const pages = pageReminders.filter((reminder) => reminder.sendNotification)
// If the user only has one reminder triggered we send a deep
// link to that link.
if (pages.length === 1) {
const page = pages[0]
let title = 'Snoozed: You have one snoozed article to read on Omnivore'
// // If the user only has one reminder triggered we send a deep
// // link to that link.
// if (pages.length === 1) {
// const page = pages[0]
// let title = 'Snoozed: You have one snoozed article to read on Omnivore'
if (page.author) {
title = `'Snoozed: From ${page.author}`
}
// if (page.author) {
// title = `'Snoozed: From ${page.author}`
// }
const pushData = !page
? undefined
: {
link: Buffer.from(
JSON.stringify({
id: page.pageId,
url: page.url,
slug: page.slug,
title: page.title,
image: page.image,
author: page.author,
isArchived: false,
contentReader: ContentReader.Web,
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
})
).toString('base64'),
}
// const pushData = !page
// ? undefined
// : {
// link: Buffer.from(
// JSON.stringify({
// id: page.pageId,
// url: page.url,
// slug: page.slug,
// title: page.title,
// image: page.image,
// author: page.author,
// isArchived: false,
// contentReader: ContentReader.Web,
// readingProgressPercent: 0,
// readingProgressAnchorIndex: 0,
// })
// ).toString('base64'),
// }
return {
notification: {
title,
body: page.title,
imageUrl: page.image || undefined,
},
data: pushData,
tokens: deviceTokens.map((token) => token.token),
}
}
// return {
// notification: {
// title,
// body: page.title,
// imageUrl: page.image || undefined,
// },
// data: pushData,
// tokens: deviceTokens.map((token) => token.token),
// }
// }
const title = `Snoozed: You have ${pages.length} articles to read.`
let description = 'Read them now.'
const allBylines = pages.map((page) => page.author).filter((byline) => byline)
const bylines = [...new Set(allBylines)].splice(0, 5)
if (bylines.length > 0) {
description = 'From ' + bylines.map((byline) => byline).join(', ')
}
// const title = `Snoozed: You have ${pages.length} articles to read.`
// let description = 'Read them now.'
// const allBylines = pages.map((page) => page.author).filter((byline) => byline)
// const bylines = [...new Set(allBylines)].splice(0, 5)
// if (bylines.length > 0) {
// description = 'From ' + bylines.map((byline) => byline).join(', ')
// }
return {
notification: {
title: title,
body: description,
},
tokens: deviceTokens.map((token) => token.token),
}
}
// return {
// notification: {
// title: title,
// body: description,
// },
// tokens: deviceTokens.map((token) => token.token),
// }
// }
const updateRemindersStatus = async (
userId: string,
pagesToUnarchive: string[],
remindAt: Date
): Promise<void> => {
// Unarchive all the links and updated saved_at to now, so they
// appear at the top of the user's list.
for (const pageId of pagesToUnarchive) {
await updatePage(
pageId,
{
savedAt: new Date(),
archivedAt: null,
},
{
pubsub: createPubSubClient(),
uid: userId,
}
)
}
// const updateRemindersStatus = async (
// userId: string,
// pagesToUnarchive: string[],
// remindAt: Date
// ): Promise<void> => {
// // Unarchive all the links and updated saved_at to now, so they
// // appear at the top of the user's list.
// for (const pageId of pagesToUnarchive) {
// await updatePage(
// pageId,
// {
// savedAt: new Date(),
// archivedAt: null,
// },
// {
// pubsub: createPubSubClient(),
// uid: userId,
// }
// )
// }
// db update
await appDataSource.transaction(async (tx) => {
await setClaims(tx, userId)
await setRemindersComplete(tx, userId, remindAt)
})
}
// // db update
// await appDataSource.transaction(async (tx) => {
// await setClaims(tx, userId)
// await setRemindersComplete(tx, userId, remindAt)
// })
// }

View file

@ -1,123 +1,17 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { htmlToSpeechFile } from '@omnivore/text-to-speech-handler'
import cors from 'cors'
import express from 'express'
import { appDataSource } from '../data_source'
import { getPageById } from '../elastic/pages'
import { ArticleSavingRequestStatus } from '../elastic/types'
import { Speech, SpeechState } from '../entity/speech'
import { UserPersonalization } from '../entity/user_personalization'
import { readPushSubscription } from '../pubsub'
import { getRepository, setClaims } from '../repository'
import { FeatureName, getFeature } from '../services/features'
import { shouldSynthesize } from '../services/speech'
import { authTrx } from '../repository'
import { getClaimsByToken } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'
import { enqueueTextToSpeech } from '../utils/createTask'
import { logger } from '../utils/logger'
const DEFAULT_VOICE = 'Larry'
const DEFAULT_COMPLIMENTARY_VOICE = 'Evelyn'
export function textToSpeechRouter() {
const router = express.Router()
// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.post('/auto-synthesize', async (req, res) => {
logger.info('auto-synthesize')
const { message: msgStr, expired } = readPushSubscription(req)
if (!msgStr) {
return res.status(400).send('Bad Request')
}
if (expired) {
logger.info('discarding expired message')
return res.status(200).send('Expired')
}
try {
const data: { userId: string; type: string; id: string } =
JSON.parse(msgStr)
const { userId, type, id } = data
if (!userId || !type || !id) {
logger.info('Invalid data')
return res.status(400).send('Bad Request')
}
if (type.toUpperCase() !== 'PAGE') {
logger.info('Not a page')
return res.status(200).send('Not a page')
}
const page = await getPageById(id)
if (!page) {
logger.info('No page found', { id })
return res.status(200).send('No page found')
}
if (page.userId !== userId) {
logger.info('Page does not belong to user', { id, userId })
return res.status(200).send('Page does not belong to user')
}
if (page.state === ArticleSavingRequestStatus.Processing) {
logger.info('Page is still processing, try again later', { id })
return res.status(400).send('Page is still processing')
}
// checks if this page needs to be synthesized automatically
if (await shouldSynthesize(userId, page)) {
logger.info('page needs to be synthesized')
const userPersonalization = await getRepository(
UserPersonalization
).findOneBy({ user: { id: userId } })
const speechFile = htmlToSpeechFile({
title: page.title,
content: page.content,
options: {
primaryVoice: userPersonalization?.speechVoice || DEFAULT_VOICE,
secondaryVoice:
userPersonalization?.speechSecondaryVoice ||
DEFAULT_COMPLIMENTARY_VOICE,
language: page.language,
},
})
const feature = await getFeature(
FeatureName.UltraRealisticVoice,
userId
)
for (const utterance of speechFile.utterances) {
// enqueue a task to convert text to speech
const taskName = await enqueueTextToSpeech({
userId,
speechId: utterance.idx,
text: utterance.text,
voice: utterance.voice || DEFAULT_VOICE,
priority: 'high',
isUltraRealisticVoice: true,
language: speechFile.language,
rate: userPersonalization?.speechRate || '1.1',
featureName: feature?.name,
grantedAt: feature?.grantedAt,
})
logger.info('Start Text to speech task', { taskName })
}
return res.status(202).send('Text to speech task started')
}
res.status(200).send('Page should not synthesize')
} catch (err) {
logger.error('Auto synthesize failed', err)
res.status(500).send(err)
}
})
router.options('/', cors<express.Request>({ ...corsConfig, maxAge: 600 }))
// eslint-disable-next-line @typescript-eslint/no-misused-promises
router.post('/', async (req, res) => {
@ -151,14 +45,17 @@ export function textToSpeechRouter() {
}
// set state to completed
await appDataSource.transaction(async (t) => {
await setClaims(t, userId)
await t.getRepository(Speech).update(speechId, {
audioFileName: audioFileName,
speechMarksFileName: speechMarksFileName,
state,
})
})
await authTrx(
async (t) => {
await t.getRepository(Speech).update(speechId, {
audioFileName: audioFileName,
speechMarksFileName: speechMarksFileName,
state,
})
},
undefined,
userId
)
res.send('OK')
})

View file

@ -1,10 +1,9 @@
import * as privateIpLib from 'private-ip'
import { v4 as uuidv4 } from 'uuid'
import { countByCreatedAt } from '../elastic/pages'
import { ArticleSavingRequestStatus, PageType } from '../elastic/types'
import { LibraryItemState, LibraryItemType } from '../entity/library_item'
import {
ArticleSavingRequest,
ArticleSavingRequestStatus,
CreateArticleSavingRequestErrorCode,
CreateLabelInput,
} from '../generated/graphql'
@ -18,6 +17,7 @@ import {
} from '../utils/helpers'
import { logger } from '../utils/logger'
import {
countByCreatedAt,
createLibraryItem,
findLibraryItemByUrl,
updateLibraryItem,
@ -46,7 +46,7 @@ const isPrivateIP = privateIpLib.default
const getPriorityByRateLimit = async (
userId: string
): Promise<'low' | 'high'> => {
const count = await countByCreatedAt(userId, Date.now() - 60 * 1000)
const count = await countByCreatedAt(new Date(Date.now() - 60 * 1000))
return count >= 5 ? 'low' : 'high'
}

View file

@ -4,8 +4,7 @@ import { Invite } from '../entity/groups/invite'
import { Profile } from '../entity/profile'
import { StatusType, User } from '../entity/user'
import { SignupErrorCode } from '../generated/graphql'
import { authTrx, entityManager } from '../repository'
import { profileRepository } from '../repository/profile'
import { authTrx, entityManager, getRepository } from '../repository'
import { userRepository } from '../repository/user'
import { AuthProvider } from '../routers/auth/auth_types'
import { logger } from '../utils/logger'
@ -48,7 +47,7 @@ export const createUser = async (input: {
}
// create profile if user exists but profile does not exist
const profile = await profileRepository.save({
const profile = await getRepository(Profile).save({
username: input.username,
pictureUrl: input.pictureUrl,
bio: input.bio,

View file

@ -1,5 +1,4 @@
import { nanoid } from 'nanoid'
import { appDataSource } from '../data_source'
import { Group } from '../entity/groups/group'
import { GroupMembership } from '../entity/groups/group_membership'
import { Invite } from '../entity/groups/invite'
@ -7,8 +6,7 @@ import { RuleActionType } from '../entity/rule'
import { User } from '../entity/user'
import { homePageURL } from '../env'
import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql'
import { authTrx } from '../repository'
import { groupRepository } from '../repository/group'
import { entityManager, getRepository } from '../repository'
import { userDataToUser } from '../utils/helpers'
import { getLabelsAndCreateIfNotExist } from './labels'
import { createRule } from './rules'
@ -23,7 +21,7 @@ export const createGroup = async (input: {
onlyAdminCanPost?: boolean | null
onlyAdminCanSeeMembers?: boolean | null
}): Promise<[Group, Invite]> => {
const [group, invite] = await appDataSource.transaction<[Group, Invite]>(
const [group, invite] = await entityManager.transaction<[Group, Invite]>(
async (t) => {
// Max number of groups a user can create
const maxGroups = 3
@ -72,12 +70,10 @@ export const createGroup = async (input: {
export const getRecommendationGroups = async (
user: User
): Promise<RecommendationGroup[]> => {
const groupMembers = await authTrx((t) =>
t.getRepository(GroupMembership).find({
where: { user: { id: user.id } },
relations: ['invite', 'group.members.user.profile'],
})
)
const groupMembers = await getRepository(GroupMembership).find({
where: { user: { id: user.id } },
relations: ['invite', 'group.members.user.profile'],
})
return groupMembers.map((gm) => {
const admins: GraphqlUser[] = []
@ -117,7 +113,7 @@ export const joinGroup = async (
user: User,
inviteCode: string
): Promise<RecommendationGroup> => {
const invite = await appDataSource.transaction<Invite>(async (t) => {
const invite = await entityManager.transaction<Invite>(async (t) => {
// Check if the invite exists
const invite = await t
.getRepository(Invite)
@ -146,7 +142,7 @@ export const joinGroup = async (
return invite
})
const group = await groupRepository.findOneOrFail({
const group = await getRepository(Group).findOneOrFail({
where: { id: invite.group.id },
relations: ['members', 'members.user.profile'],
})
@ -177,7 +173,7 @@ export const leaveGroup = async (
user: User,
groupId: string
): Promise<boolean> => {
return authTrx(async (t) => {
return entityManager.transaction(async (t) => {
const group = await t
.getRepository(Group)
.createQueryBuilder('group')
@ -276,7 +272,7 @@ export const getGroupsWhereUserCanPost = async (
userId: string,
groupIds: string[]
): Promise<Group[]> => {
return groupRepository
return getRepository(Group)
.createQueryBuilder('group')
.innerJoin('group.members', 'members1')
.whereInIds(groupIds)

View file

@ -276,14 +276,34 @@ export const findLibraryItemByUrl = async (
url: string,
userId: string
): Promise<LibraryItem | null> => {
return authTrx(async (tx) =>
tx
.createQueryBuilder(LibraryItem, 'library_item')
.leftJoinAndSelect('library_item.labels', 'labels')
.leftJoinAndSelect('library_item.highlights', 'highlights')
.where('library_item.user_id = :userId', { userId })
.andWhere('library_item.url = :url', { url })
.getOne()
return authTrx(
async (tx) =>
tx
.createQueryBuilder(LibraryItem, 'library_item')
.leftJoinAndSelect('library_item.labels', 'labels')
.leftJoinAndSelect('library_item.highlights', 'highlights')
.where('library_item.user_id = :userId', { userId })
.andWhere('library_item.url = :url', { url })
.getOne(),
undefined,
userId
)
}
export const refreshLibraryItem = async (
id: string,
userId: string,
pubsub = createPubSubClient()
): Promise<LibraryItem> => {
return updateLibraryItem(
id,
{
state: LibraryItemState.Succeeded,
savedAt: new Date(),
archivedAt: null,
},
userId,
pubsub
)
}
@ -311,8 +331,10 @@ export const createLibraryItem = async (
userId: string,
pubsub = createPubSubClient()
): Promise<LibraryItem> => {
const newLibraryItem = await authTrx(async (tx) =>
tx.withRepository(libraryItemRepository).save(libraryItem)
const newLibraryItem = await authTrx(
async (tx) => tx.withRepository(libraryItemRepository).save(libraryItem),
undefined,
userId
)
await pubsub.entityCreated<LibraryItem>(
@ -337,3 +359,18 @@ export const findLibraryItemsByPrefix = async (
.getMany()
)
}
export const countByCreatedAt = async (
startDate = new Date(0),
endDate = new Date()
): Promise<number> => {
return authTrx(async (tx) =>
tx
.createQueryBuilder(LibraryItem, 'library_item')
.where('library_item.created_at between :startDate and :endDate', {
startDate,
endDate,
})
.getCount()
)
}

View file

@ -1,12 +1,12 @@
import * as httpContext from 'express-http-context2'
import { readFileSync } from 'fs'
import path from 'path'
import { createPage } from '../elastic/pages'
import { ArticleSavingRequestStatus, Page, PageContext } from '../elastic/types'
import { PageType } from '../generated/graphql'
import { createPubSubClient } from '../pubsub'
import { DeepPartial } from 'typeorm'
import { LibraryItem, LibraryItemType } from '../entity/library_item'
import { ArticleSavingRequestStatus } from '../generated/graphql'
import { generateSlug, stringToHash } from '../utils/helpers'
import { logger } from '../utils/logger'
import { createLibraryItem } from './library_item'
type PopularRead = {
url: string
@ -61,12 +61,6 @@ export const addPopularRead = async (
userId: string,
name: string
): Promise<string | undefined> => {
const ctx: PageContext = {
pubsub: createPubSubClient(),
refresh: true,
uid: userId,
}
const pr = popularRead(name)
if (!pr) {
return undefined
@ -75,30 +69,25 @@ export const addPopularRead = async (
const saveTime = new Date()
const slug = generateSlug(pr.title)
const articleToSave: Page = {
id: '',
const articleToSave: DeepPartial<LibraryItem> = {
slug: slug,
userId: userId,
content: pr.content,
originalHtml: pr.originalHtml,
readableContent: pr.content,
originalContent: pr.originalHtml,
description: pr.description,
title: pr.title,
author: pr.author,
url: pr.url,
pageType: PageType.Article,
hash: stringToHash(pr.content),
image: pr.previewImage,
originalUrl: pr.url,
itemType: LibraryItemType.Article,
textContentHash: stringToHash(pr.content),
thumbnail: pr.previewImage,
publishedAt: pr.publishedAt,
savedAt: saveTime,
createdAt: saveTime,
siteName: pr.siteName,
readingProgressPercent: 0,
readingProgressAnchorIndex: 0,
state: ArticleSavingRequestStatus.Succeeded,
}
const pageId = await createPage(articleToSave, ctx)
return pageId
const item = await createLibraryItem(articleToSave, userId)
return item.id
}
const addPopularReads = async (

View file

@ -1,9 +1,5 @@
import { MulticastMessage } from 'firebase-admin/messaging'
import { Page } from '../elastic/types'
import { NewsletterEmail } from '../entity/newsletter_email'
import { UserDeviceToken } from '../entity/user_device_tokens'
import { env } from '../env'
import { ContentReader } from '../generated/graphql'
import { analytics } from '../utils/analytics'
import { logger } from '../utils/logger'
import { saveEmail, SaveEmailInput } from './save_email'
@ -77,42 +73,42 @@ export const saveNewsletter = async (
return true
}
const messageForLink = (
link: Page,
deviceTokens: UserDeviceToken[]
): MulticastMessage => {
let title = '📫 - An article was added to your Omnivore Inbox'
// const messageForLink = (
// link: Page,
// deviceTokens: UserDeviceToken[]
// ): MulticastMessage => {
// let title = '📫 - An article was added to your Omnivore Inbox'
if (link.author) {
title = `📫 - ${link.author} has published a new article`
}
// if (link.author) {
// title = `📫 - ${link.author} has published a new article`
// }
const pushData = !link
? undefined
: {
link: Buffer.from(
JSON.stringify({
id: link.id,
url: link.url,
slug: link.slug,
title: link.title,
image: link.image,
author: link.author,
isArchived: !!link.archivedAt,
contentReader: ContentReader.Web,
readingProgressPercent: link.readingProgressPercent,
readingProgressAnchorIndex: link.readingProgressAnchorIndex,
})
).toString('base64'),
}
// const pushData = !link
// ? undefined
// : {
// link: Buffer.from(
// JSON.stringify({
// id: link.id,
// url: link.url,
// slug: link.slug,
// title: link.title,
// image: link.image,
// author: link.author,
// isArchived: !!link.archivedAt,
// contentReader: ContentReader.Web,
// readingProgressPercent: link.readingProgressPercent,
// readingProgressAnchorIndex: link.readingProgressAnchorIndex,
// })
// ).toString('base64'),
// }
return {
notification: {
title: title,
body: link.title,
imageUrl: link.image || undefined,
},
data: pushData,
tokens: deviceTokens.map((token) => token.token),
}
}
// return {
// notification: {
// title: title,
// body: link.title,
// imageUrl: link.image || undefined,
// },
// data: pushData,
// tokens: deviceTokens.map((token) => token.token),
// }
// }

View file

@ -1,12 +1,15 @@
import { UploadFile } from '../entity/upload_file'
import { authTrx } from '../repository'
import { authTrx, getRepository } from '../repository'
export const findUploadFileById = async (id: string) => {
return authTrx(async (tx) => tx.getRepository(UploadFile).findOneBy({ id }))
return getRepository(UploadFile).findOneBy({ id })
}
export const setFileUploadComplete = async (id: string) => {
return authTrx(async (tx) =>
tx.getRepository(UploadFile).save({ id, status: 'COMPLETED' })
export const setFileUploadComplete = async (id: string, userId?: string) => {
return authTrx(
async (tx) =>
tx.getRepository(UploadFile).save({ id, status: 'COMPLETED' }),
undefined,
userId
)
}

View file

@ -6,7 +6,7 @@ import { promisify } from 'util'
import { v4 as uuidv4 } from 'uuid'
import { ApiKey } from '../entity/api_key'
import { env } from '../env'
import { authTrx } from '../repository'
import { getRepository } from '../repository'
import { Claims, ClaimsToSet } from '../resolvers/types'
import { logger } from './logger'
@ -33,39 +33,33 @@ export const hashApiKey = (apiKey: string) => {
export const claimsFromApiKey = async (key: string): Promise<Claims> => {
const hashedKey = hashApiKey(key)
return authTrx(
async (tx) => {
const apiKeyRepo = tx.getRepository(ApiKey)
const apiKey = await apiKeyRepo.findOne({
where: {
key: hashedKey,
},
relations: ['user'],
})
if (!apiKey) {
throw new Error('api key not found')
}
const apiKeyRepo = getRepository(ApiKey)
const iat = Math.floor(Date.now() / 1000)
const exp = Math.floor(new Date(apiKey.expiresAt).getTime() / 1000)
if (exp < iat) {
throw new Error('api key expired')
}
// update last used
await apiKeyRepo.update(apiKey.id, { usedAt: new Date() })
return {
uid: apiKey.user.id,
iat,
exp,
}
const apiKey = await apiKeyRepo.findOne({
where: {
key: hashedKey,
},
undefined,
undefined,
'omnivore_admin'
)
relations: ['user'],
})
if (!apiKey) {
throw new Error('api key not found')
}
const iat = Math.floor(Date.now() / 1000)
const exp = Math.floor(new Date(apiKey.expiresAt).getTime() / 1000)
if (exp < iat) {
throw new Error('api key expired')
}
// update last used
await apiKeyRepo.update(apiKey.id, { usedAt: new Date() })
return {
uid: apiKey.user.id,
iat,
exp,
}
}
// verify jwt token first

View file

@ -102,10 +102,9 @@ CREATE TRIGGER library_item_tsv_update BEFORE INSERT OR UPDATE
ALTER TABLE omnivore.library_item ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_library_item ON omnivore.library_item FOR SELECT USING (user_id = omnivore.get_current_user_id());
CREATE POLICY insert_library_item ON omnivore.library_item FOR INSERT WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_library_item ON omnivore.library_item FOR UPDATE USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_library_item ON omnivore.library_item FOR DELETE USING (user_id = omnivore.get_current_user_id());
CREATE POLICY library_item_policy ON omnivore.library_item
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.library_item TO omnivore_user;

View file

@ -11,6 +11,8 @@ CREATE TABLE omnivore.entity_labels (
label_id uuid NOT NULL REFERENCES omnivore.labels(id) ON DELETE CASCADE
);
GRANT SELECT, INSERT, DELETE ON omnivore.entity_labels TO omnivore_user;
CREATE OR REPLACE FUNCTION update_library_item_labels()
RETURNS trigger AS $$
DECLARE

View file

@ -16,6 +16,8 @@ CREATE TABLE omnivore.library_item_preview (
updated_at timestamptz NOT NULL DEFAULT current_timestamp
);
GRANT SELECT, INSERT ON omnivore.library_item_preview TO omnivore_user;
CREATE TRIGGER update_library_item_preview_modtime BEFORE UPDATE ON omnivore.library_item_preview FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
COMMIT;

View file

@ -19,8 +19,6 @@ ALTER TABLE omnivore.highlight
ADD COLUMN html text,
ALTER COLUMN quote DROP NOT NULL,
ALTER COLUMN patch DROP NOT NULL,
ALTER COLUMN highlight_position_percent DROP NOT NULL,
ALTER COLUMN highlight_position_anchor_index DROP NOT NULL,
DROP COLUMN article_id,
DROP COLUMN elastic_page_id;

View file

@ -12,4 +12,6 @@ CREATE TABLE omnivore.recommendation (
created_at timestamptz NOT NULL DEFAULT current_timestamp
);
GRANT SELECT, INSERT ON omnivore.library_item TO omnivore_user;
COMMIT;

View file

@ -4,257 +4,57 @@
BEGIN;
ALTER TABLE omnivore.api_key ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_api_key on omnivore.api_key
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_api_key on omnivore.api_key
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_api_key on omnivore.api_key
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_api_key on omnivore.api_key
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.api_key TO omnivore_user;
ALTER TABLE omnivore.features ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_features on omnivore.features
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_features on omnivore.features
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_features on omnivore.features
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_features on omnivore.features
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.features TO omnivore_user;
ALTER TABLE omnivore.filters ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_filters on omnivore.filters
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_filters on omnivore.filters
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_filters on omnivore.filters
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_filters on omnivore.filters
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.filters TO omnivore_user;
ALTER TABLE omnivore.integrations ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_integrations on omnivore.integrations
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_integrations on omnivore.integrations
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_integrations on omnivore.integrations
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_integrations on omnivore.integrations
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.integrations TO omnivore_user;
ALTER TABLE omnivore.newsletter_emails ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_newsletter_emails on omnivore.newsletter_emails
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_newsletter_emails on omnivore.newsletter_emails
FOR INSERT TO omnivore_user
CREATE POLICY features_policy on omnivore.features
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_newsletter_emails on omnivore.newsletter_emails
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.filters ENABLE ROW LEVEL SECURITY;
CREATE POLICY filters_policy on omnivore.filters
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, DELETE ON omnivore.newsletter_emails TO omnivore_user;
ALTER TABLE omnivore.integrations ENABLE ROW LEVEL SECURITY;
CREATE POLICY integrations_policy on omnivore.integrations
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
ALTER POLICY read_labels on omnivore.labels
USING (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.newsletter_emails ENABLE ROW LEVEL SECURITY;
CREATE POLICY newsletter_emails_policy on omnivore.newsletter_emails
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
ALTER POLICY create_labels on omnivore.labels
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY labels_policy on omnivore.labels
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.received_emails ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_received_emails on omnivore.received_emails
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_received_emails on omnivore.received_emails
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_received_emails on omnivore.received_emails
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_received_emails on omnivore.received_emails
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.received_emails TO omnivore_user;
CREATE POLICY received_emails_policy on omnivore.received_emails
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.rules ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_rules on omnivore.rules
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_rules on omnivore.rules
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_rules on omnivore.rules
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_rules on omnivore.rules
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.rules TO omnivore_user;
ALTER TABLE omnivore.subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_subscriptions on omnivore.subscriptions
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_subscriptions on omnivore.subscriptions
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_subscriptions on omnivore.subscriptions
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_subscriptions on omnivore.subscriptions
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.subscriptions TO omnivore_user;
ALTER TABLE omnivore.upload_files ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_upload_files on omnivore.upload_files
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_upload_files on omnivore.upload_files
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_upload_files on omnivore.upload_files
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_upload_files on omnivore.upload_files
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.upload_files TO omnivore_user;
CREATE POLICY rules_policy on omnivore.rules
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.webhooks ENABLE ROW LEVEL SECURITY;
CREATE POLICY webhooks_policy on omnivore.webhooks
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY read_webhooks on omnivore.webhooks
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_webhooks on omnivore.webhooks
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_webhooks on omnivore.webhooks
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_webhooks on omnivore.webhooks
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.webhooks TO omnivore_user;
ALTER POLICY read_user_device_tokens on omnivore.user_device_tokens
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
ALTER POLICY create_user_device_tokens on omnivore.user_device_tokens
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY user_device_tokens_policy on omnivore.user_device_tokens
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.search_history ENABLE ROW LEVEL SECURITY;
CREATE POLICY search_history_policy on omnivore.search_history
USING (user_id = omnivore.get_current_user_id())
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY read_search_history on omnivore.search_history
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_search_history on omnivore.search_history
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_search_history on omnivore.search_history
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_search_history on omnivore.search_history
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.group_membership ENABLE ROW LEVEL SECURITY;
CREATE POLICY read_group_membership on omnivore.group_membership
FOR SELECT TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY create_group_membership on omnivore.group_membership
FOR INSERT TO omnivore_user
WITH CHECK (user_id = omnivore.get_current_user_id());
CREATE POLICY update_group_membership on omnivore.group_membership
FOR UPDATE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
CREATE POLICY delete_group_membership on omnivore.group_membership
FOR DELETE TO omnivore_user
USING (user_id = omnivore.get_current_user_id());
ALTER TABLE omnivore.abuse_report
ALTER COLUMN elastic_page_id RENAME TO library_item_id,
DROP COLUMN page_id;
ALTER TABLE omnivore.content_display_report
ALTER COLUMN elastic_page_id RENAME TO library_item_id,
DROP COLUMN page_id;
ALTER TABLE omnivore.abuse_report DROP COLUMN page_id;
ALTER TABLE omnivore.abuse_report RENAME COLUMN elastic_page_id TO library_item_id;
ALTER TABLE omnivore.content_display_report DROP COLUMN page_id;
ALTER TABLE omnivore.content_display_report RENAME COLUMN elastic_page_id TO library_item_id;
COMMIT;

View file

@ -4,97 +4,37 @@
BEGIN;
ALTER TABLE omnivore.api_key DISABLE ROW LEVEL SECURITY;
DROP POLICY read_api_key on omnivore.api_key;
DROP POLICY create_api_key on omnivore.api_key;
DROP POLICY update_api_key on omnivore.api_key;
DROP POLICY delete_api_key on omnivore.api_key;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.api_key FROM omnivore_user;
ALTER TABLE omnivore.features DISABLE ROW LEVEL SECURITY;
DROP POLICY read_features on omnivore.features;
DROP POLICY create_features on omnivore.features;
DROP POLICY update_features on omnivore.features;
DROP POLICY delete_features on omnivore.features;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.features FROM omnivore_user;
DROP POLICY features_policy on omnivore.features;
ALTER TABLE omnivore.filters DISABLE ROW LEVEL SECURITY;
DROP POLICY read_filters on omnivore.filters;
DROP POLICY create_filters on omnivore.filters;
DROP POLICY update_filters on omnivore.filters;
DROP POLICY delete_filters on omnivore.filters;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.filters FROM omnivore_user;
DROP POLICY filters_policy on omnivore.filters;
ALTER TABLE omnivore.integrations DISABLE ROW LEVEL SECURITY;
DROP POLICY read_integrations on omnivore.integrations;
DROP POLICY create_integrations on omnivore.integrations;
DROP POLICY update_integrations on omnivore.integrations;
DROP POLICY delete_integrations on omnivore.integrations;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.integrations FROM omnivore_user;
DROP POLICY integrations_policy on omnivore.integrations;
ALTER TABLE omnivore.newsletter_emails DISABLE ROW LEVEL SECURITY;
DROP POLICY read_newsletter_emails on omnivore.newsletter_emails;
DROP POLICY create_newsletter_emails on omnivore.newsletter_emails;
DROP POLICY delete_newsletter_emails on omnivore.newsletter_emails;
REVOKE SELECT, INSERT, DELETE ON omnivore.newsletter_emails FROM omnivore_user;
DROP POLICY newsletter_emails_policy on omnivore.newsletter_emails;
ALTER POLICY read_labels on omnivore.labels USING (true);
ALTER POLICY create_labels on omnivore.labels WITH CHECK (true);
DROP POLICY labels_policy on omnivore.labels;
ALTER TABLE omnivore.received_emails DISABLE ROW LEVEL SECURITY;
DROP POLICY read_received_emails on omnivore.received_emails;
DROP POLICY create_received_emails on omnivore.received_emails;
DROP POLICY update_received_emails on omnivore.received_emails;
DROP POLICY delete_received_emails on omnivore.received_emails;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.received_emails FROM omnivore_user;
DROP POLICY received_emails_policy on omnivore.received_emails;
ALTER TABLE omnivore.rules DISABLE ROW LEVEL SECURITY;
DROP POLICY read_rules on omnivore.rules;
DROP POLICY create_rules on omnivore.rules;
DROP POLICY update_rules on omnivore.rules;
DROP POLICY delete_rules on omnivore.rules;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.rules FROM omnivore_user;
ALTER TABLE omnivore.subscriptions DISABLE ROW LEVEL SECURITY;
DROP POLICY read_subscriptions on omnivore.subscriptions;
DROP POLICY create_subscriptions on omnivore.subscriptions;
DROP POLICY update_subscriptions on omnivore.subscriptions;
DROP POLICY delete_subscriptions on omnivore.subscriptions;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.subscriptions FROM omnivore_user;
ALTER TABLE omnivore.upload_files DISABLE ROW LEVEL SECURITY;
DROP POLICY read_upload_files on omnivore.upload_files;
DROP POLICY create_upload_files on omnivore.upload_files;
DROP POLICY update_upload_files on omnivore.upload_files;
DROP POLICY delete_upload_files on omnivore.upload_files;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.upload_files FROM omnivore_user;
DROP POLICY rules_policy on omnivore.rules;
ALTER TABLE omnivore.webhooks DISABLE ROW LEVEL SECURITY;
DROP POLICY read_webhooks on omnivore.webhooks;
DROP POLICY create_webhooks on omnivore.webhooks;
DROP POLICY update_webhooks on omnivore.webhooks;
DROP POLICY delete_webhooks on omnivore.webhooks;
REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.webhooks FROM omnivore_user;
DROP POLICY webhooks_policy on omnivore.webhooks;
ALTER POLICY read_user_device_tokens on omnivore.user_device_tokens
FOR SELECT FROM omnivore_user
USING (true);
ALTER POLICY create_user_device_tokens on omnivore.user_device_tokens
FOR INSERT FROM omnivore_user
WITH CHECK (true);
DROP POLICY user_device_tokens_policy on omnivore.user_device_tokens;
ALTER TABLE omnivore.search_history DISABLE ROW LEVEL SECURITY;
DROP POLICY read_search_history on omnivore.search_history;
DROP POLICY create_search_history on omnivore.search_history;
DROP POLICY update_search_history on omnivore.search_history;
DROP POLICY delete_search_history on omnivore.search_history;
DROP POLICY search_history_policy on omnivore.search_history;
ALTER TABLE omnivore.abuse_report
ALTER COLUMN library_item_id RENAME TO elastic_page_id,
ADD COLUMN page_id text;
ALTER TABLE omnivore.content_display_report
ALTER COLUMN library_item_id RENAME TO elastic_page_id,
ADD COLUMN page_id text;
ALTER TABLE omnivore.abuse_report RENAME COLUMN library_item_id TO elastic_page_id;
ALTER TABLE omnivore.abuse_report ADD COLUMN page_id text;
ALTER TABLE omnivore.content_display_report RENAME COLUMN library_item_id TO elastic_page_id;
ALTER TABLE omnivore.content_display_report ADD COLUMN page_id text;
COMMIT;