From 669cddeec892d3b9071914ba435a872b72c90dd6 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Thu, 7 Sep 2023 12:35:05 +0800 Subject: [PATCH] fix some rls issue --- packages/api/src/resolvers/api_key/index.ts | 10 +- .../api/src/resolvers/integrations/index.ts | 91 ++++-------- .../src/resolvers/user_device_tokens/index.ts | 12 +- .../api/src/routers/notification_router.ts | 4 +- .../api/src/routers/svc/email_attachment.ts | 6 +- packages/api/src/routers/svc/emails.ts | 6 +- packages/api/src/routers/svc/newsletters.ts | 4 +- packages/api/src/services/api_key.ts | 41 ++++++ packages/api/src/services/features.ts | 51 ++++++- .../api/src/services/integrations/index.ts | 72 +++++++++- packages/api/src/services/labels.ts | 26 +++- packages/api/src/services/library_item.ts | 20 +-- packages/api/src/services/newsletters.ts | 19 ++- packages/api/src/services/profile.ts | 7 + packages/api/src/services/subscriptions.ts | 19 +++ packages/api/src/services/user.ts | 25 ++++ .../api/src/services/user_device_tokens.ts | 20 ++- packages/api/src/services/webhook.ts | 27 ++++ packages/api/test/db.ts | 134 +++--------------- .../api/test/gql/sanitize-directive.test.ts | 11 +- packages/api/test/resolvers/api_key.test.ts | 12 +- packages/api/test/resolvers/article.test.ts | 71 ++-------- .../resolvers/article_saving_request.test.ts | 5 +- packages/api/test/resolvers/features.test.ts | 39 +++-- packages/api/test/resolvers/highlight.test.ts | 7 +- .../api/test/resolvers/integrations.test.ts | 72 +++++----- packages/api/test/resolvers/labels.test.ts | 76 +++++----- .../api/test/resolvers/newsletters.test.ts | 40 +++--- .../api/test/resolvers/popular_reads.test.ts | 5 +- .../api/test/resolvers/recent_emails.test.ts | 9 +- .../test/resolvers/recent_searches.test.ts | 5 +- packages/api/test/resolvers/reminders.test.ts | 18 +-- packages/api/test/resolvers/report.test.ts | 7 +- packages/api/test/resolvers/rules.test.ts | 5 +- .../send_install_instructions.test.ts | 7 +- .../api/test/resolvers/subscriptions.test.ts | 36 ++--- packages/api/test/resolvers/update.test.ts | 7 +- .../resolvers/upload_file_request.test.ts | 5 +- .../upload_import_file_resolver.test.ts | 5 +- packages/api/test/resolvers/user.test.ts | 20 +-- .../resolvers/user_delete_account.test.ts | 5 +- .../test/resolvers/user_device_tokens.test.ts | 20 ++- packages/api/test/resolvers/webhooks.test.ts | 36 ++--- packages/api/test/routers/article.test.ts | 13 +- packages/api/test/routers/auth.test.ts | 29 ++-- packages/api/test/routers/emails.test.ts | 12 +- .../api/test/routers/integrations.test.ts | 12 +- .../api/test/routers/pdf_attachments.test.ts | 12 +- packages/api/test/routers/webhooks.test.ts | 21 ++- .../create_content_display_report.test.ts | 8 +- .../api/test/services/create_user.test.ts | 11 +- packages/api/test/services/save_email.test.ts | 27 ++-- .../services/save_newsletter_email.test.ts | 29 ++-- packages/api/test/util.ts | 20 --- packages/api/test/utils/parser.test.ts | 5 +- 55 files changed, 689 insertions(+), 627 deletions(-) create mode 100644 packages/api/src/services/api_key.ts create mode 100644 packages/api/src/services/profile.ts create mode 100644 packages/api/src/services/user.ts create mode 100644 packages/api/src/services/webhook.ts diff --git a/packages/api/src/resolvers/api_key/index.ts b/packages/api/src/resolvers/api_key/index.ts index 86055b042..2188d1ea0 100644 --- a/packages/api/src/resolvers/api_key/index.ts +++ b/packages/api/src/resolvers/api_key/index.ts @@ -14,6 +14,7 @@ import { RevokeApiKeySuccess, } from '../../generated/graphql' import { getRepository } from '../../repository' +import { findApiKeys } from '../../services/api_key' import { analytics } from '../../utils/analytics' import { generateApiKey, hashApiKey } from '../../utils/auth' import { authorized } from '../../utils/helpers' @@ -21,14 +22,7 @@ import { authorized } from '../../utils/helpers' export const apiKeysResolver = authorized( async (_, __, { log, uid }) => { try { - 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', - }, - }) + const apiKeys = await findApiKeys(uid) return { apiKeys, diff --git a/packages/api/src/resolvers/integrations/index.ts b/packages/api/src/resolvers/integrations/index.ts index 1deacfcb0..9fbe6327a 100644 --- a/packages/api/src/resolvers/integrations/index.ts +++ b/packages/api/src/resolvers/integrations/index.ts @@ -1,5 +1,5 @@ +import { DeepPartial } from 'typeorm' import { Integration, IntegrationType } from '../../entity/integration' -import { User } from '../../entity/user' import { env } from '../../env' import { DeleteIntegrationError, @@ -18,8 +18,14 @@ import { SetIntegrationErrorCode, SetIntegrationSuccess, } from '../../generated/graphql' -import { getRepository } from '../../repository' -import { getIntegrationService } from '../../services/integrations' +import { + createIntegration, + findIntegration, + findIntegrations, + getIntegrationService, + removeIntegration, + updateIntegration, +} from '../../services/integrations' import { analytics } from '../../utils/analytics' import { deleteTask, @@ -32,29 +38,17 @@ export const setIntegrationResolver = authorized< SetIntegrationSuccess, SetIntegrationError, MutationSetIntegrationArgs ->(async (_, { input }, { claims: { uid }, log }) => { - log.info('setIntegrationResolver') - +>(async (_, { input }, { uid, log }) => { try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [SetIntegrationErrorCode.Unauthorized], - } - } - - const integrationToSave: Partial = { + const integrationToSave: DeepPartial = { ...input, - user, + user: { id: uid }, id: input.id || undefined, type: input.type || IntegrationType.Export, } if (input.id) { // Update - const existingIntegration = await getRepository(Integration).findOne({ - where: { id: input.id }, - relations: ['user'], - }) + const existingIntegration = await findIntegration({ id: input.id }, uid) if (!existingIntegration) { return { errorCodes: [SetIntegrationErrorCode.NotFound], @@ -82,18 +76,18 @@ export const setIntegrationResolver = authorized< } // save integration - const integration = await getRepository(Integration).save(integrationToSave) + const integration = await createIntegration(integrationToSave, uid) if ( integrationToSave.type === IntegrationType.Export && (!integrationToSave.id || integrationToSave.enabled) ) { // create a task to sync all the pages if new integration or enable integration (export type) - const taskName = await enqueueSyncWithIntegration(user.id, input.name) + const taskName = await enqueueSyncWithIntegration(uid, input.name) log.info('enqueued task', taskName) // update task name in integration - await getRepository(Integration).update(integration.id, { taskName }) + await updateIntegration(integration.id, { taskName }, uid) integration.taskName = taskName } else if (integrationToSave.taskName) { // delete the task if disable integration and task exists @@ -101,9 +95,13 @@ export const setIntegrationResolver = authorized< log.info('task deleted', integrationToSave.taskName) // update task name in integration - await getRepository(Integration).update(integration.id, { - taskName: null, - }) + await updateIntegration( + integration.id, + { + taskName: null, + }, + uid + ) integration.taskName = null } @@ -131,19 +129,9 @@ export const setIntegrationResolver = authorized< export const integrationsResolver = authorized< IntegrationsSuccess, IntegrationsError ->(async (_, __, { claims: { uid }, log }) => { - log.info('integrationsResolver') - +>(async (_, __, { uid, log }) => { try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [IntegrationsErrorCode.Unauthorized], - } - } - const integrations = await getRepository(Integration).findBy({ - user: { id: uid }, - }) + const integrations = await findIntegrations(uid) return { integrations, @@ -165,17 +153,7 @@ export const deleteIntegrationResolver = authorized< log.info('deleteIntegrationResolver') try { - const user = await getRepository(User).findOneBy({ id: uid }) - if (!user) { - return { - errorCodes: [DeleteIntegrationErrorCode.Unauthorized], - } - } - - const integration = await getRepository(Integration).findOne({ - where: { id }, - relations: ['user'], - }) + const integration = await findIntegration({ id }, uid) if (!integration) { return { @@ -183,21 +161,13 @@ export const deleteIntegrationResolver = authorized< } } - if (integration.user.id !== uid) { - return { - errorCodes: [DeleteIntegrationErrorCode.Unauthorized], - } - } - if (integration.taskName) { // delete the task if task exists await deleteTask(integration.taskName) log.info('task deleted', integration.taskName) } - const deletedIntegration = await getRepository(Integration).remove( - integration - ) + const deletedIntegration = await removeIntegration(integration, uid) deletedIntegration.id = id analytics.track({ @@ -229,10 +199,7 @@ export const importFromIntegrationResolver = authorized< log.info('importFromIntegrationResolver') try { - const integration = await getRepository(Integration).findOne({ - where: { id: integrationId, user: { id: uid } }, - relations: ['user'], - }) + const integration = await findIntegration({ id: integrationId }, uid) if (!integration) { return { @@ -251,7 +218,7 @@ export const importFromIntegrationResolver = authorized< authToken ) // update task name in integration - await getRepository(Integration).update(integration.id, { taskName }) + await updateIntegration(integration.id, { taskName }, uid) analytics.track({ userId: uid, diff --git a/packages/api/src/resolvers/user_device_tokens/index.ts b/packages/api/src/resolvers/user_device_tokens/index.ts index a262a2570..483a917f0 100644 --- a/packages/api/src/resolvers/user_device_tokens/index.ts +++ b/packages/api/src/resolvers/user_device_tokens/index.ts @@ -15,9 +15,9 @@ import { import { createDeviceToken, deleteDeviceToken, - getDeviceToken, - getDeviceTokenByToken, - getDeviceTokensByUserId, + findDeviceTokenById, + findDeviceTokenByToken, + findDeviceTokensByUserId, } from '../../services/user_device_tokens' import { analytics } from '../../utils/analytics' import { authorized } from '../../utils/helpers' @@ -44,7 +44,7 @@ export const setDeviceTokenResolver = authorized< try { // when token is null, we are deleting it if (!token && id) { - const deviceToken = await getDeviceToken(id) + const deviceToken = await findDeviceTokenById(id) if (!deviceToken) { log.error('device token not found', id) @@ -107,7 +107,7 @@ export const setDeviceTokenResolver = authorized< token ) { // duplicate token - const deviceToken = await getDeviceTokenByToken(token) + const deviceToken = await findDeviceTokenByToken(token) if (!deviceToken) { return { @@ -139,7 +139,7 @@ export const deviceTokensResolver = authorized< }, }) - const deviceTokens = await getDeviceTokensByUserId(uid) + const deviceTokens = await findDeviceTokensByUserId(uid) log.info('deviceTokens', deviceTokens) return { diff --git a/packages/api/src/routers/notification_router.ts b/packages/api/src/routers/notification_router.ts index 372bad305..0a8f02cfd 100644 --- a/packages/api/src/routers/notification_router.ts +++ b/packages/api/src/routers/notification_router.ts @@ -3,7 +3,7 @@ import express from 'express' import * as jwt from 'jsonwebtoken' import { env } from '../env' import { Claims } from '../resolvers/types' -import { getDeviceTokensByUserId } from '../services/user_device_tokens' +import { findDeviceTokensByUserId } from '../services/user_device_tokens' import { corsConfig } from '../utils/corsConfig' import { PushNotificationType, @@ -42,7 +42,7 @@ export function notificationRouter() { return res.status(400).send({ errorCode: 'BAD_DATA' }) } - const tokens = await getDeviceTokensByUserId(userId) + const tokens = await findDeviceTokensByUserId(userId) if (tokens.length === 0) { return res.status(400).send({ errorCode: 'NO_DEVICE_TOKENS' }) } diff --git a/packages/api/src/routers/svc/email_attachment.ts b/packages/api/src/routers/svc/email_attachment.ts index ae9ddaf7d..84dea297b 100644 --- a/packages/api/src/routers/svc/email_attachment.ts +++ b/packages/api/src/routers/svc/email_attachment.ts @@ -10,7 +10,7 @@ import { env } from '../../env' import { UploadFileStatus } from '../../generated/graphql' import { authTrx } from '../../repository' import { createLibraryItem } from '../../services/library_item' -import { getNewsletterEmail } from '../../services/newsletters' +import { findNewsletterEmail } from '../../services/newsletters' import { updateReceivedEmail } from '../../services/received_emails' import { findUploadFileById, @@ -45,7 +45,7 @@ export function emailAttachmentRouter() { return res.status(401).send('UNAUTHORIZED') } - const newsletterEmail = await getNewsletterEmail(email) + const newsletterEmail = await findNewsletterEmail(email) if (!newsletterEmail || !newsletterEmail.user) { return res.status(401).send('UNAUTHORIZED') } @@ -109,7 +109,7 @@ export function emailAttachmentRouter() { return res.status(401).send('UNAUTHORIZED') } - const newsletterEmail = await getNewsletterEmail(email) + const newsletterEmail = await findNewsletterEmail(email) if (!newsletterEmail || !newsletterEmail.user) { return res.status(401).send('UNAUTHORIZED') } diff --git a/packages/api/src/routers/svc/emails.ts b/packages/api/src/routers/svc/emails.ts index fab0bb213..010b83494 100644 --- a/packages/api/src/routers/svc/emails.ts +++ b/packages/api/src/routers/svc/emails.ts @@ -2,7 +2,7 @@ import cors from 'cors' import express from 'express' import { env } from '../../env' import { readPushSubscription } from '../../pubsub' -import { getNewsletterEmail } from '../../services/newsletters' +import { findNewsletterEmail } from '../../services/newsletters' import { saveReceivedEmail } from '../../services/received_emails' import { saveNewsletter } from '../../services/save_newsletter_email' import { analytics } from '../../utils/analytics' @@ -62,7 +62,7 @@ export function emailsServiceRouter() { } // get user from newsletter email - const newsletterEmail = await getNewsletterEmail(data.to) + const newsletterEmail = await findNewsletterEmail(data.to) if (!newsletterEmail) { logger.info('newsletter email not found', { email: data.to }) @@ -150,7 +150,7 @@ export function emailsServiceRouter() { try { // get user from newsletter email - const newsletterEmail = await getNewsletterEmail(req.body.to) + const newsletterEmail = await findNewsletterEmail(req.body.to) if (!newsletterEmail) { logger.info('newsletter email not found', { email: req.body.to }) diff --git a/packages/api/src/routers/svc/newsletters.ts b/packages/api/src/routers/svc/newsletters.ts index 6fb1ff2e6..a5668d3bc 100644 --- a/packages/api/src/routers/svc/newsletters.ts +++ b/packages/api/src/routers/svc/newsletters.ts @@ -2,7 +2,7 @@ import express from 'express' import { SubscriptionStatus } from '../../generated/graphql' import { createPubSubClient, readPushSubscription } from '../../pubsub' import { - getNewsletterEmail, + findNewsletterEmail, updateConfirmationCode, } from '../../services/newsletters' import { updateReceivedEmail } from '../../services/received_emails' @@ -105,7 +105,7 @@ export function newsletterServiceRouter() { } // get user from newsletter email - const newsletterEmail = await getNewsletterEmail(data.email) + const newsletterEmail = await findNewsletterEmail(data.email) if (!newsletterEmail) { logger.info(`newsletter email not found: ${data.email}`) return res.status(200).send('Not Found') diff --git a/packages/api/src/services/api_key.ts b/packages/api/src/services/api_key.ts new file mode 100644 index 000000000..8f004a748 --- /dev/null +++ b/packages/api/src/services/api_key.ts @@ -0,0 +1,41 @@ +import { FindOptionsWhere } from 'typeorm' +import { ApiKey } from '../entity/api_key' +import { authTrx } from '../repository' + +export const findApiKeys = async ( + userId: string, + where?: FindOptionsWhere[] | FindOptionsWhere, + select?: (keyof ApiKey)[] +) => { + return authTrx( + (t) => + t.getRepository(ApiKey).find({ + select: select || [ + 'id', + 'name', + 'scopes', + 'expiresAt', + 'createdAt', + 'usedAt', + ], + where, + order: { + usedAt: { direction: 'DESC', nulls: 'last' }, + createdAt: 'DESC', + }, + }), + undefined, + userId + ) +} + +export const deleteApiKey = async ( + criteria: string[] | FindOptionsWhere, + userId: string +) => { + return authTrx( + async (t) => t.getRepository(ApiKey).delete(criteria), + undefined, + userId + ) +} diff --git a/packages/api/src/services/features.ts b/packages/api/src/services/features.ts index 538fd3f2c..f062cfbd5 100644 --- a/packages/api/src/services/features.ts +++ b/packages/api/src/services/features.ts @@ -1,5 +1,5 @@ import * as jwt from 'jsonwebtoken' -import { IsNull, Not } from 'typeorm' +import { DeepPartial, FindOptionsWhere, IsNull, Not } from 'typeorm' import { Feature } from '../entity/feature' import { env } from '../env' import { authTrx, entityManager } from '../repository' @@ -110,12 +110,49 @@ export const isOptedIn = async (name: FeatureName): Promise => { return !!feature } -export const getFeature = async ( - name: FeatureName +export const findFeatureByName = async ( + name: FeatureName, + userId?: string ): Promise => { - return authTrx((t) => - t.getRepository(Feature).findOneBy({ - name, - }) + return authTrx( + (t) => + t.getRepository(Feature).findOneBy({ + name, + }), + undefined, + userId + ) +} + +export const deleteFeature = async ( + criteria: string[] | FindOptionsWhere, + userId: string +) => { + return authTrx( + (t) => t.getRepository(Feature).delete(criteria), + undefined, + userId + ) +} + +export const createFeature = async ( + feature: DeepPartial, + userId: string +) => { + return authTrx( + (t) => t.getRepository(Feature).save(feature), + undefined, + userId + ) +} + +export const createFeatures = async ( + features: DeepPartial[], + userId: string +) => { + return authTrx( + (t) => t.getRepository(Feature).save(features), + undefined, + userId ) } diff --git a/packages/api/src/services/integrations/index.ts b/packages/api/src/services/integrations/index.ts index c5946ceed..1a7f92816 100644 --- a/packages/api/src/services/integrations/index.ts +++ b/packages/api/src/services/integrations/index.ts @@ -1,6 +1,9 @@ -import { ReadwiseIntegration } from './readwise' +import { DeepPartial, FindOptionsWhere } from 'typeorm' +import { Integration } from '../../entity/integration' +import { authTrx } from '../../repository' import { IntegrationService } from './integration' import { PocketIntegration } from './pocket' +import { ReadwiseIntegration } from './readwise' const integrations: IntegrationService[] = [ new ReadwiseIntegration(), @@ -14,3 +17,70 @@ export const getIntegrationService = (name: string): IntegrationService => { } return service } + +export const deleteIntegrations = async ( + userId: string, + criteria: string[] | FindOptionsWhere +) => { + return authTrx( + async (t) => t.getRepository(Integration).delete(criteria), + undefined, + userId + ) +} + +export const removeIntegration = async ( + integration: Integration, + userId: string +) => { + return authTrx( + async (t) => t.getRepository(Integration).remove(integration), + undefined, + userId + ) +} + +export const findIntegration = async ( + where: FindOptionsWhere | FindOptionsWhere[], + userId: string +) => { + return authTrx( + async (t) => t.getRepository(Integration).findOneBy(where), + undefined, + userId + ) +} + +export const findIntegrations = async ( + userId: string, + where?: FindOptionsWhere | FindOptionsWhere[] +) => { + return authTrx( + async (t) => t.getRepository(Integration).find({ where }), + undefined, + userId + ) +} + +export const createIntegration = async ( + integration: DeepPartial, + userId: string +) => { + return authTrx( + async (t) => t.getRepository(Integration).save(integration), + undefined, + userId + ) +} + +export const updateIntegration = async ( + id: string, + integration: DeepPartial, + userId: string +) => { + return authTrx( + async (t) => t.getRepository(Integration).update(id, integration), + undefined, + userId + ) +} diff --git a/packages/api/src/services/labels.ts b/packages/api/src/services/labels.ts index 4066b9f8a..85881d27e 100644 --- a/packages/api/src/services/labels.ts +++ b/packages/api/src/services/labels.ts @@ -1,4 +1,4 @@ -import { In } from 'typeorm' +import { FindOptionsWhere, In } from 'typeorm' import { EntityLabel } from '../entity/entity_label' import { Label } from '../entity/label' import { createPubSubClient, EntityType } from '../pubsub' @@ -148,3 +148,27 @@ export const findLabelsByIds = async (ids: string[]): Promise => { }) }) } + +export const createLabel = async ( + name: string, + color: string, + userId: string +): Promise