diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index f5b9f45d2..67b9bf79a 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -9,6 +9,7 @@ import * as Sentry from '@sentry/node' import { ContextFunction } from 'apollo-server-core' import { ApolloServer } from 'apollo-server-express' import { ExpressContext } from 'apollo-server-express/dist/ApolloServer' +import * as httpContext from 'express-http-context2' import * as jwt from 'jsonwebtoken' import { EntityManager } from 'typeorm' import { promisify } from 'util' @@ -45,6 +46,8 @@ const contextFunc: ContextFunction = async ({ const token = req?.cookies?.auth || req?.headers?.authorization const claims = await getClaimsByToken(token) + httpContext.set('claims', claims) + async function setClaims( em: EntityManager, uuid?: string, diff --git a/packages/api/src/entity/link.ts b/packages/api/src/entity/link.ts deleted file mode 100644 index f53d6e8d7..000000000 --- a/packages/api/src/entity/link.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Table "omnivore.links" -// Column | Type | Collation | Nullable | Default -// ---------------------------------------+--------------------------+-----------+----------+---------------------- -// article_url | text | | not null | -// article_hash | text | | not null | -// created_at | timestamp with time zone | | not null | CURRENT_TIMESTAMP -// shared_comment | text | | | -// article_reading_progress | real | | not null | 0 -// article_reading_progress_anchor_index | integer | | not null | 0 -// shared_with_highlights | boolean | | | false - -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - OneToOne, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm' - -import { User } from './user' -import { Page } from './page' -import { Label } from './label' - -@Entity({ name: 'links' }) -export class Link { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column('text') - slug!: string - - @OneToOne(() => User) - @JoinColumn({ name: 'user_id' }) - user!: User - - @OneToOne(() => Page) - @JoinColumn({ name: 'article_id' }) - page!: Page - - @Column('timestamp') - savedAt!: Date - - @Column('timestamp') - sharedAt!: Date | null - - @Column('timestamp') - archivedAt?: Date | null - - @Column('text') - articleUrl!: string - - @Column('text') - articleHash!: string - - @CreateDateColumn() - createdAt?: Date - - @UpdateDateColumn() - updatedAt?: Date - - @ManyToMany(() => Label) - @JoinTable({ - name: 'link_labels', - joinColumn: { name: 'link_id' }, - inverseJoinColumn: { name: 'label_id' }, - }) - labels?: Label[] -} diff --git a/packages/api/src/entity/page.ts b/packages/api/src/entity/page.ts deleted file mode 100644 index 2ac8abbee..000000000 --- a/packages/api/src/entity/page.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm' - -@Entity({ name: 'pages' }) -export class Page { - @PrimaryGeneratedColumn('uuid') - id!: string - - @Column('text') - url!: string - - @Column('text') - hash!: string - - @Column('text') - title!: string - - @Column('text', { nullable: true }) - uploadFileId!: string - - @Column('text', { nullable: true }) - author!: string - - @Column('text', { nullable: true }) - description!: string - - @Column('text', { nullable: true }) - image!: string - - @Column('text') - content!: string - - @Column('text', { name: 'page_type' }) - type!: string - - @Column('text', { nullable: true }) - originalHtml!: string - - @Column('timestamp') - publishedAt?: Date - - @CreateDateColumn() - createdAt?: Date - - @UpdateDateColumn() - updatedAt?: Date -} diff --git a/packages/api/src/entity/reports/abuse_report.ts b/packages/api/src/entity/reports/abuse_report.ts index 8da8c1444..63847f109 100644 --- a/packages/api/src/entity/reports/abuse_report.ts +++ b/packages/api/src/entity/reports/abuse_report.ts @@ -13,7 +13,7 @@ export class AbuseReport { id?: string @Column('text') - pageId?: string + libraryItemId?: string @Column('text') sharedBy!: string @@ -35,7 +35,4 @@ export class AbuseReport { @UpdateDateColumn() updatedAt?: Date - - @Column('text') - elasticPageId?: string } diff --git a/packages/api/src/entity/reports/content_display_report.ts b/packages/api/src/entity/reports/content_display_report.ts index 627a83870..cb7fb968f 100644 --- a/packages/api/src/entity/reports/content_display_report.ts +++ b/packages/api/src/entity/reports/content_display_report.ts @@ -19,7 +19,7 @@ export class ContentDisplayReport { user!: User @Column('text') - pageId?: string + libraryItemId?: string @Column('text') content!: string @@ -38,7 +38,4 @@ export class ContentDisplayReport { @UpdateDateColumn() updatedAt?: Date - - @Column('text') - elasticPageId?: string } diff --git a/packages/api/src/repository/api_key.ts b/packages/api/src/repository/api_key.ts deleted file mode 100644 index 335af43d8..000000000 --- a/packages/api/src/repository/api_key.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { entityManager } from '.' -import { ApiKey } from '../entity/api_key' - -export const apiKeyRepository = entityManager.getRepository(ApiKey) diff --git a/packages/api/src/repository/group.ts b/packages/api/src/repository/group.ts new file mode 100644 index 000000000..6a163c7fe --- /dev/null +++ b/packages/api/src/repository/group.ts @@ -0,0 +1,4 @@ +import { entityManager } from '.' +import { Group } from '../entity/groups/group' + +export const groupRepository = entityManager.getRepository(Group) diff --git a/packages/api/src/repository/index.ts b/packages/api/src/repository/index.ts index 831a50c25..b541cb210 100644 --- a/packages/api/src/repository/index.ts +++ b/packages/api/src/repository/index.ts @@ -1,5 +1,7 @@ +import * as httpContext from 'express-http-context2' import { EntityManager } from 'typeorm' import { appDataSource } from '../data_source' +import { Claims } from '../resolvers/types' export const setClaims = async ( manager: EntityManager, @@ -14,11 +16,19 @@ export const setClaims = async ( export const authTrx = async ( fn: (manager: EntityManager) => Promise, - uid = '00000000-0000-0000-0000-000000000000', - dbRole = 'omnivore_user' + em = entityManager, + uid?: string, + userRole?: string ): Promise => { - return entityManager.transaction(async (tx) => { - await setClaims(tx, uid, dbRole) + // if uid and dbRole are not passed in, then get them from the claims + if (!uid && !userRole) { + const claims: Claims | undefined = httpContext.get('claims') + uid = claims?.uid + userRole = claims?.userRole + } + + return em.transaction(async (tx) => { + await setClaims(tx, uid, userRole) return fn(tx) }) } diff --git a/packages/api/src/repository/upload_file.ts b/packages/api/src/repository/upload_file.ts deleted file mode 100644 index 77cc839d2..000000000 --- a/packages/api/src/repository/upload_file.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { entityManager } from '.' -import { UploadFile } from '../entity/upload_file' - -export const uploadFileRepository = entityManager - .getRepository(UploadFile) - .extend({ - findById(id: string) { - return this.findOneBy({ id }) - }, - }) diff --git a/packages/api/src/resolvers/filters/index.ts b/packages/api/src/resolvers/filters/index.ts index 66889d4ea..dc2bc46e3 100644 --- a/packages/api/src/resolvers/filters/index.ts +++ b/packages/api/src/resolvers/filters/index.ts @@ -23,7 +23,6 @@ import { UpdateFilterSuccess, UpdateFilterErrorCode, } from '../../generated/graphql' -import { entityManager, getRepository, setClaims } from '../../repository' import { analytics } from '../../utils/analytics' import { env } from '../../env' import { isNil, mergeWith } from 'lodash' @@ -44,36 +43,24 @@ export const saveFilterResolver = authorized< }) try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [SaveFilterErrorCode.Unauthorized], - } - } - - const filter = await getRepository(Filter).save({ - user: { id: uid }, - name: input.name, - category: 'Search', - description: '', - position: input.position ?? 0, - filter: input.filter, - defaultFilter: false, - visible: true, + const filter = await authTrx(async (t) => { + return t.withRepository(filterRepository).save({ + user: { id: uid }, + name: input.name, + category: 'Search', + description: '', + position: input.position ?? 0, + filter: input.filter, + defaultFilter: false, + visible: true, + }) }) return { filter, } } catch (error) { - log.error('Error saving filters', { - error, - labels: { - source: 'resolver', - resolver: 'saveFilterResolver', - uid, - }, - }) + log.error('Error saving filters', error) return { errorCodes: [SaveFilterErrorCode.BadRequest], @@ -85,7 +72,7 @@ export const deleteFilterResolver = authorized< DeleteFilterSuccess, DeleteFilterError, MutationDeleteFilterArgs ->(async (_, { id }, { claims, log }) => { +>(async (_, { id }, { authTrx, uid, log }) => { log.info('Deleting filters', { id, labels: { @@ -96,37 +83,16 @@ export const deleteFilterResolver = authorized< }) try { - const user = await getRepository(User).findOneBy({ id: claims.uid }) - if (!user) { - return { - errorCodes: [DeleteFilterErrorCode.Unauthorized], - } - } - - const filter = await getRepository(Filter).findOneBy({ - id, - user: { id: claims.uid }, - }) - if (!filter) { - return { - errorCodes: [DeleteFilterErrorCode.NotFound], - } - } - - await getRepository(Filter).delete({ id }) + const filter = await authTrx(async (t) => { + const filter = await t.withRepository(filterRepository).findOne({ return { filter, } } catch (error) { - log.error('Error deleting filters', { - error, - labels: { - source: 'resolver', - resolver: 'deleteFilterResolver', - uid: claims.uid, - }, - }) + log.error('Error deleting filters', + error + ) return { errorCodes: [DeleteFilterErrorCode.BadRequest], diff --git a/packages/api/src/routers/svc/newsletters.ts b/packages/api/src/routers/svc/newsletters.ts index 48038d21a..6fb1ff2e6 100644 --- a/packages/api/src/routers/svc/newsletters.ts +++ b/packages/api/src/routers/svc/newsletters.ts @@ -11,7 +11,7 @@ import { saveNewsletter, } from '../../services/save_newsletter_email' import { saveUrlFromEmail } from '../../services/save_url' -import { getSubscriptionByNameAndUserId } from '../../services/subscriptions' +import { getSubscriptionByName } from '../../services/subscriptions' import { isUrl } from '../../utils/helpers' import { logger } from '../../utils/logger' @@ -127,7 +127,7 @@ export function newsletterServiceRouter() { } } else { // do not subscribe if subscription already exists and is unsubscribed - const existingSubscription = await getSubscriptionByNameAndUserId( + const existingSubscription = await getSubscriptionByName( data.author, newsletterEmail.user.id ) diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 159757326..1ae3b2c47 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -1,11 +1,11 @@ import { EntityManager } from 'typeorm' -import { appDataSource } from '../data_source' import { GroupMembership } from '../entity/groups/group_membership' import { Invite } from '../entity/groups/invite' import { Profile } from '../entity/profile' import { StatusType, User } from '../entity/user' import { SignupErrorCode } from '../generated/graphql' -import { getRepository } from '../repository' +import { authTrx, entityManager } from '../repository' +import { profileRepository } from '../repository/profile' import { userRepository } from '../repository/user' import { AuthProvider } from '../routers/auth/auth_types' import { logger } from '../utils/logger' @@ -48,7 +48,7 @@ export const createUser = async (input: { } // create profile if user exists but profile does not exist - const profile = await getRepository(Profile).save({ + const profile = await profileRepository.save({ username: input.username, pictureUrl: input.pictureUrl, bio: input.bio, @@ -72,7 +72,7 @@ export const createUser = async (input: { return Promise.reject({ errorCode: SignupErrorCode.InvalidUsername }) } - const [user, profile] = await appDataSource.transaction<[User, Profile]>( + const [user, profile] = await entityManager.transaction<[User, Profile]>( async (t) => { let hasInvite = false let invite: Invite | null = null @@ -168,8 +168,11 @@ const validateInvite = async ( logger.info('rejecting invite, expired', invite) return false } - const membershipRepo = entityManager.getRepository(GroupMembership) - const numMembers = await membershipRepo.countBy({ invite: { id: invite.id } }) + const numMembers = await authTrx( + (t) => + t.getRepository(GroupMembership).countBy({ invite: { id: invite.id } }), + entityManager + ) if (numMembers >= invite.maxMembers) { logger.info('rejecting invite, too many users', invite, numMembers) return false diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 716aac9da..538fd3f2c 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -1,9 +1,8 @@ import * as jwt from 'jsonwebtoken' import { IsNull, Not } from 'typeorm' -import { appDataSource } from '../data_source' import { Feature } from '../entity/feature' import { env } from '../env' -import { getRepository } from '../repository' +import { authTrx, entityManager } from '../repository' import { logger } from '../utils/logger' export enum FeatureName { @@ -26,14 +25,15 @@ export const optInFeature = async ( } const optInUltraRealisticVoice = async (uid: string): Promise => { - const feature = await getRepository(Feature).findOne({ - where: { - user: { id: uid }, - name: FeatureName.UltraRealisticVoice, - grantedAt: Not(IsNull()), - }, - relations: ['user'], - }) + const feature = await authTrx((t) => + t.getRepository(Feature).findOne({ + where: { + name: FeatureName.UltraRealisticVoice, + grantedAt: Not(IsNull()), + }, + relations: ['user'], + }) + ) if (feature) { // already opted in logger.info('already opted in') @@ -42,7 +42,7 @@ const optInUltraRealisticVoice = async (uid: string): Promise => { const MAX_USERS = 1500 // opt in to feature for the first 1500 users - const optedInFeatures = (await appDataSource.query( + const optedInFeatures = (await entityManager.query( `insert into omnivore.features (user_id, name, granted_at) select $1, $2, $3 from omnivore.features where name = $2 and granted_at is not null @@ -63,10 +63,9 @@ const optInUltraRealisticVoice = async (uid: string): Promise => { name: FeatureName.UltraRealisticVoice, grantedAt: null, } - const result = await getRepository(Feature).upsert(optInRecord, [ - 'user', - 'name', - ]) + const result = await authTrx((t) => + t.getRepository(Feature).upsert(optInRecord, ['user', 'name']) + ) if (result.generatedMaps.length === 0) { throw new Error('failed to update opt-in record') } @@ -100,25 +99,23 @@ export const signFeatureToken = ( ) } -export const isOptedIn = async ( - name: FeatureName, - uid: string -): Promise => { - const feature = await getRepository(Feature).findOneBy({ - user: { id: uid }, - name, - grantedAt: Not(IsNull()), - }) +export const isOptedIn = async (name: FeatureName): Promise => { + const feature = await authTrx((t) => + t.getRepository(Feature).findOneBy({ + name, + grantedAt: Not(IsNull()), + }) + ) return !!feature } export const getFeature = async ( - name: FeatureName, - uid: string + name: FeatureName ): Promise => { - return getRepository(Feature).findOneBy({ - user: { id: uid }, - name, - }) + return authTrx((t) => + t.getRepository(Feature).findOneBy({ + name, + }) + ) } diff --git a/packages/api/src/services/followers.ts b/packages/api/src/services/followers.ts index 95ee89dc6..858e5c53b 100644 --- a/packages/api/src/services/followers.ts +++ b/packages/api/src/services/followers.ts @@ -1,33 +1,29 @@ -import { Follower } from '../entity/follower' -import { User } from '../entity/user' -import { getRepository } from '../repository' +// export const getUserFollowers = async ( +// user: User, +// offset?: number, +// count?: number +// ): Promise => { +// return ( +// await getRepository(Follower).find({ +// where: { user: { id: user.id } }, +// relations: ['user', 'followee'], +// skip: offset, +// take: count, +// }) +// ).map((f: Follower) => f.followee) +// } -export const getUserFollowers = async ( - user: User, - offset?: number, - count?: number -): Promise => { - return ( - await getRepository(Follower).find({ - where: { user: { id: user.id } }, - relations: ['user', 'followee'], - skip: offset, - take: count, - }) - ).map((f: Follower) => f.followee) -} - -export const getUserFollowing = async ( - user: User, - offset?: number, - count?: number -): Promise => { - return ( - await getRepository(Follower).find({ - where: { followee: { id: user.id } }, - relations: ['user', 'followee'], - skip: offset, - take: count, - }) - ).map((f: Follower) => f.user) -} +// export const getUserFollowing = async ( +// user: User, +// offset?: number, +// count?: number +// ): Promise => { +// return ( +// await getRepository(Follower).find({ +// where: { followee: { id: user.id } }, +// relations: ['user', 'followee'], +// skip: offset, +// take: count, +// }) +// ).map((f: Follower) => f.user) +// } diff --git a/packages/api/src/services/groups.ts b/packages/api/src/services/groups.ts index 531511331..399af9ff6 100644 --- a/packages/api/src/services/groups.ts +++ b/packages/api/src/services/groups.ts @@ -7,7 +7,8 @@ import { RuleActionType } from '../entity/rule' import { User } from '../entity/user' import { homePageURL } from '../env' import { RecommendationGroup, User as GraphqlUser } from '../generated/graphql' -import { getRepository } from '../repository' +import { authTrx } from '../repository' +import { groupRepository } from '../repository/group' import { userDataToUser } from '../utils/helpers' import { createLabel, getLabelByName } from './labels' import { createRule } from './rules' @@ -26,7 +27,7 @@ export const createGroup = async (input: { async (t) => { // Max number of groups a user can create const maxGroups = 3 - const groupCount = await getRepository(Group).countBy({ + const groupCount = await t.getRepository(Group).countBy({ createdBy: { id: input.admin.id }, }) if (groupCount >= maxGroups) { @@ -71,10 +72,12 @@ export const createGroup = async (input: { export const getRecommendationGroups = async ( user: User ): Promise => { - const groupMembers = await getRepository(GroupMembership).find({ - where: { user: { id: user.id } }, - relations: ['invite', 'group.members.user.profile'], - }) + const groupMembers = await authTrx((t) => + t.getRepository(GroupMembership).find({ + where: { user: { id: user.id } }, + relations: ['invite', 'group.members.user.profile'], + }) + ) return groupMembers.map((gm) => { const admins: GraphqlUser[] = [] @@ -144,7 +147,7 @@ having count(*) < $4`, return invite }) - const group = await getRepository(Group).findOneOrFail({ + const group = await groupRepository.findOneOrFail({ where: { id: invite.group.id }, relations: ['members', 'members.user.profile'], }) @@ -175,7 +178,7 @@ export const leaveGroup = async ( user: User, groupId: string ): Promise => { - return appDataSource.transaction(async (t) => { + return authTrx(async (t) => { const group = await t .getRepository(Group) .createQueryBuilder('group') @@ -275,7 +278,7 @@ export const getGroupsWhereUserCanPost = async ( userId: string, groupIds: string[] ): Promise => { - return getRepository(Group) + return groupRepository .createQueryBuilder('group') .innerJoin('group.members', 'members1') .whereInIds(groupIds) diff --git a/packages/api/src/services/highlights.ts b/packages/api/src/services/highlights.ts index e4285fbf6..77c1f2f82 100644 --- a/packages/api/src/services/highlights.ts +++ b/packages/api/src/services/highlights.ts @@ -3,7 +3,7 @@ import { DeepPartial } from 'typeorm' import { Highlight } from '../entity/highlight' import { homePageURL } from '../env' import { createPubSubClient, EntityType } from '../pubsub' -import { entityManager, setClaims } from '../repository' +import { authTrx, setClaims } from '../repository' import { highlightRepository } from '../repository/highlight' type HighlightEvent = Highlight & { pageId: string } @@ -20,10 +20,9 @@ export const getHighlightUrl = (slug: string, highlightId: string): string => export const saveHighlight = async ( highlight: DeepPartial, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ) => { - const newHighlight = await em.transaction(async (tx) => { + const newHighlight = await authTrx(async (tx) => { await setClaims(tx, userId) return tx @@ -44,12 +43,9 @@ export const mergeHighlights = async ( highlightsToRemove: string[], highlightToAdd: DeepPartial, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ) => { - const newHighlight = await em.transaction(async (tx) => { - await setClaims(tx, userId) - + const newHighlight = await authTrx(async (tx) => { const highlightRepo = tx.withRepository(highlightRepository) await highlightRepo.delete(highlightsToRemove) @@ -66,13 +62,8 @@ export const mergeHighlights = async ( return newHighlight } -export const deleteHighlightById = async ( - highlightId: string, - userId: string -) => { - return entityManager.transaction(async (tx) => { - await setClaims(tx, userId) - +export const deleteHighlightById = async (highlightId: string) => { + return authTrx(async (tx) => { const highlightRepo = tx.withRepository(highlightRepository) const highlight = await highlightRepo.findById(highlightId) if (!highlight) { diff --git a/packages/api/src/services/labels.ts b/packages/api/src/services/labels.ts index 449cc2aa5..99a9cffdb 100644 --- a/packages/api/src/services/labels.ts +++ b/packages/api/src/services/labels.ts @@ -2,7 +2,7 @@ import { In } from 'typeorm' import { Label } from '../entity/label' import { LibraryItem } from '../entity/library_item' import { createPubSubClient, EntityType } from '../pubsub' -import { entityManager, setClaims } from '../repository' +import { authTrx } from '../repository' import { highlightRepository } from '../repository/highlight' import { CreateLabelInput, labelRepository } from '../repository/label' import { libraryItemRepository } from '../repository/library_item' @@ -24,12 +24,9 @@ import { libraryItemRepository } from '../repository/library_item' export const getLabelsAndCreateIfNotExist = async ( labels: CreateLabelInput[], - userId: string, - em = entityManager + userId: string ): Promise => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - + return authTrx(async (tx) => { const labelRepo = tx.withRepository(labelRepository) // find existing labels const labelEntities = await labelRepo.findByNames(labels.map((l) => l.name)) @@ -55,11 +52,9 @@ export const saveLabelsInLibraryItem = async ( labels: Label[], libraryItemId: string, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ) => { - await em.transaction(async (tx) => { - await setClaims(tx, userId) + await authTrx(async (tx) => { await tx .withRepository(libraryItemRepository) .update(libraryItemId, { labels }) @@ -77,11 +72,9 @@ export const addLabelsToLibraryItem = async ( labels: Label[], libraryItemId: string, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ) => { - await em.transaction(async (tx) => { - await setClaims(tx, userId) + await authTrx(async (tx) => { await tx .withRepository(libraryItemRepository) .createQueryBuilder() @@ -102,12 +95,9 @@ export const saveLabelsInHighlight = async ( labels: Label[], highlightId: string, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ) => { - await em.transaction(async (tx) => { - await setClaims(tx, userId) - + await authTrx(async (tx) => { await tx.withRepository(highlightRepository).update(highlightId, { labels }) }) @@ -119,14 +109,8 @@ export const saveLabelsInHighlight = async ( ) } -export const findLabelsByIds = async ( - ids: string[], - userId: string, - em = entityManager -): Promise => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - +export const findLabelsByIds = async (ids: string[]): Promise => { + return authTrx(async (tx) => { return tx.withRepository(labelRepository).findBy({ id: In(ids), }) diff --git a/packages/api/src/services/library_item.ts b/packages/api/src/services/library_item.ts index 385093a11..52363398a 100644 --- a/packages/api/src/services/library_item.ts +++ b/packages/api/src/services/library_item.ts @@ -7,7 +7,7 @@ import { LibraryItemType, } from '../entity/library_item' import { createPubSubClient, EntityType } from '../pubsub' -import { entityManager, setClaims } from '../repository' +import { authTrx, setClaims } from '../repository' import { libraryItemRepository } from '../repository/library_item' import { DateFilter, @@ -18,9 +18,9 @@ import { LabelFilterType, NoFilter, ReadFilter, + Sort, SortBy, SortOrder, - Sort, } from '../utils/search' export interface SearchArgs { @@ -225,8 +225,7 @@ const buildWhereClause = ( export const searchLibraryItems = async ( args: SearchArgs, - userId: string, - em = entityManager + userId: string ): Promise<{ libraryItems: LibraryItem[]; count: number }> => { const { from = 0, size = 10, sort } = args @@ -236,9 +235,7 @@ export const searchLibraryItems = async ( const sortField = sort?.by || SortBy.SAVED // add pagination and sorting - return em.transaction(async (tx) => { - await setClaims(tx, userId) - + return authTrx(async (tx) => { const queryBuilder = tx .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') @@ -262,12 +259,9 @@ export const searchLibraryItems = async ( export const findLibraryItemById = async ( id: string, - userId: string, - em = entityManager + userId: string ): Promise => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - + return authTrx(async (tx) => { return tx .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') @@ -280,12 +274,9 @@ export const findLibraryItemById = async ( export const findLibraryItemByUrl = async ( url: string, - userId: string, - em = entityManager + userId: string ): Promise => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - + return authTrx(async (tx) => { return tx .createQueryBuilder(LibraryItem, 'library_item') .leftJoinAndSelect('library_item.labels', 'labels') @@ -300,12 +291,9 @@ export const updateLibraryItem = async ( id: string, libraryItem: DeepPartial, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ): Promise => { - const updatedLibraryItem = await em.transaction(async (tx) => { - await setClaims(tx, userId) - + const updatedLibraryItem = await authTrx(async (tx) => { return tx.withRepository(libraryItemRepository).save({ id, ...libraryItem }) }) @@ -321,10 +309,9 @@ export const updateLibraryItem = async ( export const createLibraryItem = async ( libraryItem: DeepPartial, userId: string, - pubsub = createPubSubClient(), - em = entityManager + pubsub = createPubSubClient() ): Promise => { - const newLibraryItem = await em.transaction(async (tx) => { + const newLibraryItem = await authTrx(async (tx) => { await setClaims(tx, userId) return tx.withRepository(libraryItemRepository).save(libraryItem) @@ -341,13 +328,9 @@ export const createLibraryItem = async ( export const findLibraryItemsByPrefix = async ( prefix: string, - userId: string, - limit = 5, - em = entityManager + limit = 5 ): Promise => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - + return authTrx(async (tx) => { return tx .createQueryBuilder(LibraryItem, 'library_item') .where('library_item.title ILIKE :prefix', { prefix: `${prefix}%` }) diff --git a/packages/api/src/services/received_emails.ts b/packages/api/src/services/received_emails.ts index ae3db8258..7c29d573f 100644 --- a/packages/api/src/services/received_emails.ts +++ b/packages/api/src/services/received_emails.ts @@ -1,5 +1,5 @@ import { ReceivedEmail } from '../entity/received_email' -import { entityManager, getRepository } from '../repository' +import { authTrx } from '../repository' export const saveReceivedEmail = async ( from: string, @@ -10,21 +10,22 @@ export const saveReceivedEmail = async ( userId: string, type: 'article' | 'non-article' = 'non-article' ): Promise => { - return getRepository(ReceivedEmail).save({ - from, - to, - subject, - text, - html, - type, - user: { id: userId }, - }) + return authTrx((t) => + t.getRepository(ReceivedEmail).save({ + from, + to, + subject, + text, + html, + type, + user: { id: userId }, + }) + ) } export const updateReceivedEmail = async ( id: string, - type: 'article' | 'non-article', - em = entityManager + type: 'article' | 'non-article' ) => { - await em.getRepository(ReceivedEmail).update(id, { type }) + return authTrx((t) => t.getRepository(ReceivedEmail).update(id, { type })) } diff --git a/packages/api/src/services/reminders.ts b/packages/api/src/services/reminders.ts index 47ab2d990..6e317c96a 100644 --- a/packages/api/src/services/reminders.ts +++ b/packages/api/src/services/reminders.ts @@ -1,74 +1,74 @@ -import { EntityManager, IsNull, Not } from 'typeorm' -import { getPageById } from '../elastic/pages' -import { Reminder } from '../entity/reminder' -import { getRepository } from '../repository' -import { logger } from '../utils/logger' +// import { EntityManager, IsNull, Not } from 'typeorm' +// import { getPageById } from '../elastic/pages' +// import { Reminder } from '../entity/reminder' +// import { getRepository } from '../repository' +// import { logger } from '../utils/logger' -export interface PageReminder { - pageId: string - reminderId: string - url: string - slug: string - title: string - description?: string - author?: string - image?: string - sendNotification?: boolean -} +// export interface PageReminder { +// pageId: string +// reminderId: string +// url: string +// slug: string +// title: string +// description?: string +// author?: string +// image?: string +// sendNotification?: boolean +// } -export const getPagesWithReminder = async ( - userId: string, - remindAt: Date -): Promise => { - const reminders = await getRepository(Reminder).findBy({ - user: { id: userId }, - status: 'CREATED', - remindAt, - elasticPageId: Not(IsNull()), - }) +// export const getPagesWithReminder = async ( +// userId: string, +// remindAt: Date +// ): Promise => { +// const reminders = await getRepository(Reminder).findBy({ +// user: { id: userId }, +// status: 'CREATED', +// remindAt, +// elasticPageId: Not(IsNull()), +// }) - const results: PageReminder[] = [] - for (const reminder of reminders) { - if (reminder.elasticPageId) { - const page = await getPageById(reminder.elasticPageId) - if (!page) { - logger.info(`Reminder ${reminder.id} has invalid elasticPageId`) - continue - } +// const results: PageReminder[] = [] +// for (const reminder of reminders) { +// if (reminder.elasticPageId) { +// const page = await getPageById(reminder.elasticPageId) +// if (!page) { +// logger.info(`Reminder ${reminder.id} has invalid elasticPageId`) +// continue +// } - results.push({ - pageId: page.id, - reminderId: reminder.id, - url: page.url, - slug: page.slug, - title: page.title, - description: page.description, - author: page.author, - image: page.image, - sendNotification: reminder.sendNotification, - }) - } - } +// results.push({ +// pageId: page.id, +// reminderId: reminder.id, +// url: page.url, +// slug: page.slug, +// title: page.title, +// description: page.description, +// author: page.author, +// image: page.image, +// sendNotification: reminder.sendNotification, +// }) +// } +// } - return results -} +// return results +// } -export const setRemindersComplete = async ( - tx: EntityManager, - userId: string, - remindAt: Date -): Promise => { - const updateResult = await tx - .createQueryBuilder() - .where({ userId, remindAt }) - .update(Reminder) - .set({ status: 'COMPLETED' }) - .returning('*') - .execute() +// export const setRemindersComplete = async ( +// tx: EntityManager, +// userId: string, +// remindAt: Date +// ): Promise => { +// const updateResult = await tx +// .createQueryBuilder() +// .where({ userId, remindAt }) +// .update(Reminder) +// .set({ status: 'COMPLETED' }) +// .returning('*') +// .execute() - if (updateResult.generatedMaps.length === 0) { - return null - } +// if (updateResult.generatedMaps.length === 0) { +// return null +// } - return updateResult.generatedMaps[0] as Reminder -} +// return updateResult.generatedMaps[0] as Reminder +// } diff --git a/packages/api/src/services/reports.ts b/packages/api/src/services/reports.ts index 783169615..ad7572191 100644 --- a/packages/api/src/services/reports.ts +++ b/packages/api/src/services/reports.ts @@ -4,14 +4,13 @@ import { ContentDisplayReport } from '../entity/reports/content_display_report' import { ReportItemInput, ReportType } from '../generated/graphql' import { getRepository } from '../repository' import { logger } from '../utils/logger' +import { findLibraryItemById } from './library_item' export const saveContentDisplayReport = async ( uid: string, input: ReportItemInput ): Promise => { - const repo = getRepository(ContentDisplayReport) - - const page = await getPageById(input.pageId) + const page = await findLibraryItemById(input.pageId) if (!page) { logger.info('unable to submit report, page not found', input) @@ -23,7 +22,6 @@ export const saveContentDisplayReport = async ( // what the user saw. const result = await repo.save({ user: { id: uid }, - elasticPageId: input.pageId, content: page.content, originalHtml: page.originalHtml || undefined, originalUrl: page.url, diff --git a/packages/api/src/services/rules.ts b/packages/api/src/services/rules.ts index fd0a783f5..47975eea4 100644 --- a/packages/api/src/services/rules.ts +++ b/packages/api/src/services/rules.ts @@ -1,6 +1,6 @@ import { ILike } from 'typeorm' import { Rule, RuleAction } from '../entity/rule' -import { getRepository } from '../repository' +import { authTrx } from '../repository' export const createRule = async ( userId: string, @@ -11,17 +11,20 @@ export const createRule = async ( filter: string } ): Promise => { - const existingRule = await getRepository(Rule).findOneBy({ - user: { id: userId }, - name: ILike(rule.name), - }) + const existingRule = await authTrx((t) => + t.getRepository(Rule).findOneBy({ + name: ILike(rule.name), + }) + ) if (existingRule) { return existingRule } - return getRepository(Rule).save({ - ...rule, - user: { id: userId }, - }) + return authTrx((t) => + t.getRepository(Rule).save({ + ...rule, + user: { id: userId }, + }) + ) } diff --git a/packages/api/src/services/save_file.ts b/packages/api/src/services/save_file.ts index ab93b3ff8..57a5132f9 100644 --- a/packages/api/src/services/save_file.ts +++ b/packages/api/src/services/save_file.ts @@ -12,6 +12,7 @@ import { WithDataSourcesContext } from '../resolvers/types' import { logger } from '../utils/logger' import { getStorageFileDetails } from '../utils/uploads' import { getLabelsAndCreateIfNotExist } from './labels' +import { setFileUploadComplete } from './upload_file' export const saveFile = async ( ctx: WithDataSourcesContext, @@ -32,9 +33,7 @@ export const saveFile = async ( await getStorageFileDetails(input.uploadFileId, uploadFile.fileName) - const uploadFileData = await ctx.authTrx(async (tx) => { - return setFileUploadComplete(input.uploadFileId, tx) - }) + const uploadFileData = await setFileUploadComplete(input.uploadFileId) if (!uploadFileData) { return { diff --git a/packages/api/src/services/search_history.ts b/packages/api/src/services/search_history.ts index 14ddf4ef1..86f3503b7 100644 --- a/packages/api/src/services/search_history.ts +++ b/packages/api/src/services/search_history.ts @@ -1,43 +1,46 @@ import { SearchHistory } from '../entity/search_history' -import { getRepository } from '../repository' +import { authTrx } from '../repository' -export const getRecentSearches = async ( - userId: string -): Promise => { +export const getRecentSearches = async (): Promise => { // get top 10 recent searches - return getRepository(SearchHistory).find({ - where: { user: { id: userId } }, - order: { createdAt: 'DESC' }, - take: 10, - }) + return authTrx((t) => + t.getRepository(SearchHistory).find({ + order: { createdAt: 'DESC' }, + take: 10, + }) + ) } export const saveSearchHistory = async ( userId: string, term: string ): Promise => { - await getRepository(SearchHistory).upsert( - { - user: { id: userId }, - term, - createdAt: new Date(), - }, - { - conflictPaths: ['user', 'term'], - } + await authTrx((t) => + t.getRepository(SearchHistory).upsert( + { + user: { id: userId }, + term, + createdAt: new Date(), + }, + { + conflictPaths: ['user', 'term'], + } + ) ) } export const deleteSearchHistory = async (userId: string): Promise => { - await getRepository(SearchHistory).delete({ user: { id: userId } }) + await authTrx((t) => + t.getRepository(SearchHistory).delete({ user: { id: userId } }) + ) } export const deleteSearchHistoryById = async ( - userId: string, searchHistoryId: string ): Promise => { - await getRepository(SearchHistory).delete({ - user: { id: userId }, - id: searchHistoryId, - }) + await authTrx((t) => + t.getRepository(SearchHistory).delete({ + id: searchHistoryId, + }) + ) } diff --git a/packages/api/src/services/speech.ts b/packages/api/src/services/speech.ts deleted file mode 100644 index d36fe66ab..000000000 --- a/packages/api/src/services/speech.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Page } from '../elastic/types' -import { ContentReader } from '../generated/graphql' -import { contentReaderForPage } from '../utils/uploads' -import { FeatureName, isOptedIn } from './features' - -/* - * We should synthesize the page when user is opted in to the feature - */ -export const shouldSynthesize = async ( - userId: string, - page: Page -): Promise => { - if ( - contentReaderForPage(page.pageType, page.uploadFileId) !== - ContentReader.Web || - !page.content - ) { - // we don't synthesize files for now - return false - } - - return isOptedIn(FeatureName.UltraRealisticVoice, userId) -} diff --git a/packages/api/src/services/subscriptions.ts b/packages/api/src/services/subscriptions.ts index c6771ff05..5f2a4ea3f 100644 --- a/packages/api/src/services/subscriptions.ts +++ b/packages/api/src/services/subscriptions.ts @@ -2,10 +2,9 @@ import axios from 'axios' import { NewsletterEmail } from '../entity/newsletter_email' import { Subscription } from '../entity/subscription' import { SubscriptionStatus, SubscriptionType } from '../generated/graphql' -import { getRepository } from '../repository' +import { authTrx } from '../repository' import { logger } from '../utils/logger' import { sendEmail } from '../utils/sendEmail' -import { createNewsletterEmail } from './newsletters' interface SaveSubscriptionInput { userId: string @@ -80,15 +79,15 @@ const sendUnsubscribeHttpRequest = async (url: string): Promise => { } } -export const getSubscriptionByNameAndUserId = async ( - name: string, - userId: string +export const getSubscriptionByName = async ( + name: string ): Promise => { - return getRepository(Subscription).findOneBy({ - name, - user: { id: userId }, - type: SubscriptionType.Newsletter, - }) + return authTrx((tx) => + tx.getRepository(Subscription).findOneBy({ + name, + type: SubscriptionType.Newsletter, + }) + ) } export const saveSubscription = async ({ @@ -106,26 +105,24 @@ export const saveSubscription = async ({ lastFetchedAt: new Date(), } - const existingSubscription = await getSubscriptionByNameAndUserId( - name, - userId - ) - if (existingSubscription) { - // update subscription if already exists - await getRepository(Subscription).update( - existingSubscription.id, - subscriptionData - ) + const existingSubscription = await getSubscriptionByName(name) + const result = await authTrx(async (tx) => { + if (existingSubscription) { + // update subscription if already exists + await tx + .getRepository(Subscription) + .update(existingSubscription.id, subscriptionData) - return existingSubscription.id - } + return existingSubscription + } - const result = await getRepository(Subscription).save({ - ...subscriptionData, - name, - newsletterEmail: { id: newsletterEmail.id }, - user: { id: userId }, - type: SubscriptionType.Newsletter, + return tx.getRepository(Subscription).save({ + ...subscriptionData, + name, + newsletterEmail: { id: newsletterEmail.id }, + user: { id: userId }, + type: SubscriptionType.Newsletter, + }) }) return result.id @@ -150,22 +147,26 @@ export const unsubscribe = async (subscription: Subscription) => { // because it often requires clicking a button on the page to unsubscribe } - return getRepository(Subscription).update(subscription.id, { - status: SubscriptionStatus.Unsubscribed, - }) + return authTrx((tx) => + tx.getRepository(Subscription).update(subscription.id, { + status: SubscriptionStatus.Unsubscribed, + }) + ) } export const unsubscribeAll = async ( newsletterEmail: NewsletterEmail ): Promise => { try { - const subscriptions = await getRepository(Subscription).find({ - where: { - user: { id: newsletterEmail.user.id }, - newsletterEmail: { id: newsletterEmail.id }, - }, - relations: ['newsletterEmail'], - }) + const subscriptions = await authTrx((t) => + t.getRepository(Subscription).find({ + where: { + user: { id: newsletterEmail.user.id }, + newsletterEmail: { id: newsletterEmail.id }, + }, + relations: ['newsletterEmail'], + }) + ) for await (const subscription of subscriptions) { try { @@ -178,117 +179,3 @@ export const unsubscribeAll = async ( logger.info('Failed to unsubscribe all', error) } } - -export const getSubscribeHandler = (name: string): SubscribeHandler | null => { - switch (name.toLowerCase()) { - case 'axios_essentials': - return new AxiosEssentialsHandler() - case 'morning_brew': - return new MorningBrewHandler() - case 'milk_road': - return new MilkRoadHandler() - case 'money_stuff': - return new MoneyStuffHandler() - default: - return null - } -} - -export class SubscribeHandler { - async handleSubscribe( - userId: string, - name: string - ): Promise { - try { - const newsletterEmail = - (await getRepository(NewsletterEmail).findOneBy({ - user: { id: userId }, - })) || (await createNewsletterEmail(userId)) - - // subscribe to newsletter service - const subscribedNames = await this._subscribe(newsletterEmail.address) - if (subscribedNames.length === 0) { - logger.info('Failed to get subscribe response', name) - return null - } - - // create new subscriptions in db - const newSubscriptions = subscribedNames.map( - (name: string): Promise => { - return getRepository(Subscription).save({ - name, - newsletterEmail: { id: newsletterEmail.id }, - user: { id: userId }, - status: SubscriptionStatus.Active, - }) - } - ) - - return Promise.all(newSubscriptions) - } catch (error) { - logger.info('Failed to handleSubscribe', error) - return null - } - } - - async _subscribe(email: string): Promise { - return Promise.all([]) - } -} - -class AxiosEssentialsHandler extends SubscribeHandler { - async _subscribe(email: string): Promise { - await axios.post('https://api.axios.com/api/render/readers/unauth-sub/', { - headers: { - 'content-type': 'application/json', - }, - body: `{"lists":["newsletter_axiosam","newsletter_axiospm","newsletter_axiosfinishline"],"user_vars":{"source":"axios","medium":null,"campaign":null,"term":null,"content":null,"page":"webflow-newsletters-all"},"email":"${email}"`, - }) - - return ['Axios AM', 'Axios PM', 'Axios Finish Line'] - } -} - -class MorningBrewHandler extends SubscribeHandler { - async _subscribe(email: string): Promise { - await axios.post('https://singularity.morningbrew.com/graphql', { - headers: { - 'content-type': 'application/json', - }, - body: `{"operationName":"CreateUserSubscription","variables":{"signupCreateInput":{"email":"${email}","kid":null,"gclid":null,"utmCampaign":"mb","utmMedium":"website","utmSource":"hero-module","utmContent":null,"utmTerm":null,"requestPath":"https://www.morningbrew.com/daily","uiModule":"hero-module"},"signupCreateVerticalSlug":"daily"},"query":"mutation CreateUserSubscription($signupCreateInput: SignupCreateInput!, $signupCreateVerticalSlug: String!) {\\n signupCreate(input: $signupCreateInput, verticalSlug: $signupCreateVerticalSlug) {\\n user {\\n accessToken\\n email\\n hasSeenOnboarding\\n referralCode\\n verticalSubscriptions {\\n isActive\\n vertical {\\n slug\\n __typename\\n }\\n __typename\\n }\\n __typename\\n }\\n isNewSubscription\\n fromAffiliate\\n subscriptionId\\n __typename\\n }\\n}\\n"}`, - }) - - return ['Morning Brew'] - } -} - -class MilkRoadHandler extends SubscribeHandler { - async _subscribe(email: string): Promise { - await axios.post('https://www.milkroad.com/subscriptions', { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - body: `email=${encodeURIComponent(email)}&commit=Subscribe`, - }) - - return ['Milk Road'] - } -} - -class MoneyStuffHandler extends SubscribeHandler { - async _subscribe(email: string): Promise { - await axios.put( - `https://login.bloomberg.com/api/newsletters/update?email=${encodeURIComponent( - email - )}&source=¬ify=true&optIn=false`, - { - headers: { - 'content-type': 'application/json', - }, - body: '{"Money Stuff":true}', - } - ) - - return ['Money Stuff'] - } -} diff --git a/packages/api/src/services/upload_file.ts b/packages/api/src/services/upload_file.ts index 3d6d72f26..8c7f39fb7 100644 --- a/packages/api/src/services/upload_file.ts +++ b/packages/api/src/services/upload_file.ts @@ -1,30 +1,12 @@ -import { entityManager, setClaims } from '../repository' -import { uploadFileRepository } from '../repository/upload_file' +import { UploadFile } from '../entity/upload_file' +import { authTrx } from '../repository' -export const findUploadFileById = async ( - id: string, - userId: string, - em = entityManager -) => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - const uploadFile = await tx - .withRepository(uploadFileRepository) - .findById(id) - - return uploadFile - }) +export const findUploadFileById = async (id: string) => { + return authTrx(async (tx) => tx.getRepository(UploadFile).findBy({ id })) } -export const setFileUploadComplete = async ( - id: string, - userId: string, - em = entityManager -) => { - return em.transaction(async (tx) => { - await setClaims(tx, userId) - return tx - .withRepository(uploadFileRepository) - .save({ id, status: 'COMPLETED' }) - }) +export const setFileUploadComplete = async (id: string) => { + return authTrx(async (tx) => + tx.getRepository(UploadFile).save({ id, status: 'COMPLETED' }) + ) } diff --git a/packages/api/src/services/user_device_tokens.ts b/packages/api/src/services/user_device_tokens.ts index 9f9e3b63c..23cbc8178 100644 --- a/packages/api/src/services/user_device_tokens.ts +++ b/packages/api/src/services/user_device_tokens.ts @@ -1,42 +1,34 @@ -import { appDataSource } from '../data_source' -import { User } from '../entity/user' import { UserDeviceToken } from '../entity/user_device_tokens' import { env } from '../env' -import { SetDeviceTokenErrorCode } from '../generated/graphql' -import { getRepository, setClaims } from '../repository' +import { authTrx } from '../repository' import { analytics } from '../utils/analytics' export const getDeviceToken = async ( id: string ): Promise => { - return getRepository(UserDeviceToken).findOneBy({ id }) + return authTrx((t) => t.getRepository(UserDeviceToken).findOneBy({ id })) } export const getDeviceTokenByToken = async ( token: string ): Promise => { - return getRepository(UserDeviceToken).findOneBy({ token }) + return authTrx((t) => t.getRepository(UserDeviceToken).findOneBy({ token })) } export const getDeviceTokensByUserId = async ( userId: string ): Promise => { - return getRepository(UserDeviceToken).find({ - where: { user: { id: userId } }, - }) + return authTrx((t) => + t.getRepository(UserDeviceToken).findBy({ + user: { id: userId }, + }) + ) } export const createDeviceToken = async ( userId: string, token: string ): Promise => { - const user = await getRepository(User).findOneBy({ id: userId }) - if (!user) { - return Promise.reject({ - errorCode: SetDeviceTokenErrorCode.Unauthorized, - }) - } - analytics.track({ userId: userId, event: 'device_token_created', @@ -45,23 +37,18 @@ export const createDeviceToken = async ( }, }) - return getRepository(UserDeviceToken).save({ - token: token, - user: user, - }) + return authTrx((t) => + t.getRepository(UserDeviceToken).save({ + token, + user: { id: userId }, + }) + ) } export const deleteDeviceToken = async ( id: string, userId: string ): Promise => { - const user = await getRepository(User).findOneBy({ id: userId }) - if (!user) { - return Promise.reject({ - errorCode: SetDeviceTokenErrorCode.Unauthorized, - }) - } - analytics.track({ userId: userId, event: 'device_token_deleted', @@ -70,8 +57,7 @@ export const deleteDeviceToken = async ( }, }) - return appDataSource.transaction(async (t) => { - await setClaims(t, userId) + return authTrx(async (t) => { const result = await t.getRepository(UserDeviceToken).delete(id) return !!result.affected diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts index ef44a6863..55ea96ba6 100644 --- a/packages/api/src/utils/auth.ts +++ b/packages/api/src/utils/auth.ts @@ -4,9 +4,9 @@ import express from 'express' import * as jwt from 'jsonwebtoken' 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 { apiKeyRepository } from '../repository/api_key' import { Claims, ClaimsToSet } from '../resolvers/types' import { logger } from './logger' @@ -33,34 +33,39 @@ export const hashApiKey = (apiKey: string) => { export const claimsFromApiKey = async (key: string): Promise => { const hashedKey = hashApiKey(key) - return authTrx(async (tx) => { - const apiKeyRepo = tx.withRepository(apiKeyRepository) + 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 apiKey = await apiKeyRepo.findOne({ + where: { + key: hashedKey, + }, + 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') - } + 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() }) + // update last used + await apiKeyRepo.update(apiKey.id, { usedAt: new Date() }) - return { - uid: apiKey.user.id, - iat, - exp, - } - }) + return { + uid: apiKey.user.id, + iat, + exp, + } + }, + undefined, + undefined, + 'omnivore_admin' + ) } // verify jwt token first diff --git a/packages/db/migrations/0121.undo.update_highlight.sql b/packages/db/migrations/0121.undo.update_highlight.sql index f7a989984..5bc050127 100755 --- a/packages/db/migrations/0121.undo.update_highlight.sql +++ b/packages/db/migrations/0121.undo.update_highlight.sql @@ -7,6 +7,11 @@ BEGIN; DROP TRIGGER IF EXISTS library_item_highlight_annotations_update ON omnivore.highlight; DROP FUNCTION IF EXISTS update_library_item_highlight_annotations(); +ALTER POLICY read_highlight on omnivore.highlight USING (true); +ALTER POLICY create_highlight on omnivore.highlight WITH CHECK (true); +DROP POLICY delete_highlight on omnivore.highlight; +REVOKE DELETE ON omnivore.highlight FROM omnivore_user; + ALTER TABLE omnivore.highlight ADD COLUMN article_id uuid, ADD COLUMN elastic_page_id uuid, diff --git a/packages/db/migrations/0123.do.add_rls.sql b/packages/db/migrations/0123.do.add_rls.sql index 8f8e8b6be..1e5e50056 100755 --- a/packages/db/migrations/0123.do.add_rls.sql +++ b/packages/db/migrations/0123.do.add_rls.sql @@ -206,4 +206,55 @@ CREATE POLICY delete_webhooks on omnivore.webhooks 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()); + +ALTER TABLE omnivore.search_history ENABLE ROW LEVEL SECURITY; + +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; + COMMIT; diff --git a/packages/db/migrations/0123.undo.add_rls.sql b/packages/db/migrations/0123.undo.add_rls.sql index f3bb964f2..11538d92a 100755 --- a/packages/db/migrations/0123.undo.add_rls.sql +++ b/packages/db/migrations/0123.undo.add_rls.sql @@ -4,4 +4,97 @@ 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; + +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; + +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; + +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; + +ALTER POLICY read_labels on omnivore.labels USING (true); +ALTER POLICY create_labels on omnivore.labels WITH CHECK (true); + +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; + +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; + +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; + +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); + +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; + +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; + COMMIT;