diff --git a/packages/api/src/repository/user.ts b/packages/api/src/repository/user.ts index 408543c95..a8f327b0d 100644 --- a/packages/api/src/repository/user.ts +++ b/packages/api/src/repository/user.ts @@ -1,6 +1,6 @@ import { In } from 'typeorm' import { appDataSource } from '../data_source' -import { User } from './../entity/user' +import { StatusType, User } from './../entity/user' const TOP_USERS = [ 'jacksonh', @@ -16,7 +16,7 @@ export const MAX_RECORDS_LIMIT = 1000 export const userRepository = appDataSource.getRepository(User).extend({ findById(id: string) { - return this.findOneBy({ id }) + return this.findOneBy({ id, status: StatusType.Active }) }, findByEmail(email: string) { diff --git a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts index 4f327ce54..efa88f9b9 100644 --- a/packages/api/src/resolvers/importers/uploadImportFileResolver.ts +++ b/packages/api/src/resolvers/importers/uploadImportFileResolver.ts @@ -33,14 +33,14 @@ export const uploadImportFileResolver = authorized< UploadImportFileSuccess, UploadImportFileError, MutationUploadImportFileArgs ->(async (_, { type, contentType }, { claims: { uid }, log }) => { +>(async (_, { type, contentType }, { uid }) => { if (!VALID_CONTENT_TYPES.includes(contentType)) { return { errorCodes: [UploadImportFileErrorCode.BadRequest], } } - const user = await userRepository.findOneBy({ id: uid }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [UploadImportFileErrorCode.Unauthorized], diff --git a/packages/api/src/resolvers/recommendations/index.ts b/packages/api/src/resolvers/recommendations/index.ts index d3062dc5d..31f7da898 100644 --- a/packages/api/src/resolvers/recommendations/index.ts +++ b/packages/api/src/resolvers/recommendations/index.ts @@ -48,9 +48,7 @@ export const createGroupResolver = authorized< MutationCreateGroupArgs >(async (_, { input }, { uid, log }) => { try { - const userData = await userRepository.findOneBy({ - id: uid, - }) + const userData = await userRepository.findById(uid) if (!userData) { return { errorCodes: [CreateGroupErrorCode.Unauthorized], @@ -107,9 +105,7 @@ export const createGroupResolver = authorized< export const groupsResolver = authorized( async (_, __, { uid, log }) => { try { - const user = await userRepository.findOneBy({ - id: uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [GroupsErrorCode.Unauthorized], diff --git a/packages/api/src/resolvers/save/index.ts b/packages/api/src/resolvers/save/index.ts index 2acf0ce3b..f5508663d 100644 --- a/packages/api/src/resolvers/save/index.ts +++ b/packages/api/src/resolvers/save/index.ts @@ -18,9 +18,9 @@ export const savePageResolver = authorized< SaveSuccess, SaveError, MutationSavePageArgs ->(async (_, { input }, ctx) => { +>(async (_, { input }, { uid }) => { analytics.track({ - userId: ctx.uid, + userId: uid, event: 'link_saved', properties: { url: input.url, @@ -30,9 +30,7 @@ export const savePageResolver = authorized< }, }) - const user = await userRepository.findOneBy({ - id: ctx.uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [SaveErrorCode.Unauthorized] } } @@ -44,11 +42,7 @@ export const saveUrlResolver = authorized< SaveSuccess, SaveError, MutationSaveUrlArgs ->(async (_, { input }, ctx) => { - const { - claims: { uid }, - } = ctx - +>(async (_, { input }, { uid }) => { analytics.track({ userId: uid, event: 'link_saved', @@ -60,9 +54,7 @@ export const saveUrlResolver = authorized< }, }) - const user = await userRepository.findOneBy({ - id: uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [SaveErrorCode.Unauthorized] } } @@ -74,9 +66,9 @@ export const saveFileResolver = authorized< SaveSuccess, SaveError, MutationSaveFileArgs ->(async (_, { input }, ctx) => { +>(async (_, { input }, { uid }) => { analytics.track({ - userId: ctx.uid, + userId: uid, event: 'link_saved', properties: { url: input.url, @@ -86,9 +78,7 @@ export const saveFileResolver = authorized< }, }) - const user = await userRepository.findOneBy({ - id: ctx.uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [SaveErrorCode.Unauthorized] } } diff --git a/packages/api/src/resolvers/send_install_instructions/index.ts b/packages/api/src/resolvers/send_install_instructions/index.ts index 6435b807b..62d134fbc 100644 --- a/packages/api/src/resolvers/send_install_instructions/index.ts +++ b/packages/api/src/resolvers/send_install_instructions/index.ts @@ -14,11 +14,9 @@ const INSTALL_INSTRUCTIONS_EMAIL_TEMPLATE_ID = export const sendInstallInstructionsResolver = authorized< SendInstallInstructionsSuccess, SendInstallInstructionsError ->(async (_parent, _args, { claims, log }) => { +>(async (_parent, _args, { uid, log }) => { try { - const user = await userRepository.findOneBy({ - id: claims.uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [SendInstallInstructionsErrorCode.Unauthorized] } diff --git a/packages/api/src/resolvers/subscriptions/index.ts b/packages/api/src/resolvers/subscriptions/index.ts index db249b9b9..7076e8524 100644 --- a/packages/api/src/resolvers/subscriptions/index.ts +++ b/packages/api/src/resolvers/subscriptions/index.ts @@ -89,7 +89,8 @@ export const subscriptionsResolver = authorized< } const subscriptions = await queryBuilder - .orderBy(`subscription.${sortBy}`, sortOrder, 'NULLS LAST') + .orderBy('subscription.status', 'ASC') + .addOrderBy(`subscription.${sortBy}`, sortOrder, 'NULLS LAST') .getMany() return { diff --git a/packages/api/src/resolvers/user/index.ts b/packages/api/src/resolvers/user/index.ts index bf99a5c04..38b0111e2 100644 --- a/packages/api/src/resolvers/user/index.ts +++ b/packages/api/src/resolvers/user/index.ts @@ -52,9 +52,7 @@ export const updateUserResolver = authorized< UpdateUserError, MutationUpdateUserArgs >(async (_, { input: { name, bio } }, { uid, authTrx }) => { - const user = await userRepository.findOneBy({ - id: uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { errorCodes: [UpdateUserErrorCode.UserNotFound] } } @@ -92,9 +90,7 @@ export const updateUserProfileResolver = authorized< UpdateUserProfileError, MutationUpdateUserProfileArgs >(async (_, { input: { userId, username, pictureUrl } }, { uid, authTrx }) => { - const user = await userRepository.findOneBy({ - id: userId, - }) + const user = await userRepository.findById(userId) if (!user) { return { errorCodes: [UpdateUserProfileErrorCode.Unauthorized] } } @@ -117,6 +113,7 @@ export const updateUserProfileResolver = authorized< profile: { username: lowerCasedUsername, }, + status: StatusType.Active, }) if (existingUser?.id) { return { @@ -161,6 +158,7 @@ export const googleLoginResolver: ResolverFn< const user = await userRepository.findOneBy({ email, + status: StatusType.Active, }) if (!user?.id) { return { errorCodes: [LoginErrorCode.UserNotFound] } @@ -256,9 +254,7 @@ export const getMeUserResolver: ResolverFn< return undefined } - const user = await userRepository.findOneBy({ - id: claims.uid, - }) + const user = await userRepository.findById(claims.uid) if (!user) { return undefined } @@ -282,12 +278,17 @@ export const getUserResolver: ResolverFn< const userId = id || (username && - (await userRepository.findOneBy({ profile: { username } }))?.id) + ( + await userRepository.findOneBy({ + profile: { username }, + status: StatusType.Active, + }) + )?.id) if (!userId) { return { errorCodes: [UserErrorCode.UserNotFound] } } - const userRecord = await userRepository.findOneBy({ id: userId }) + const userRecord = await userRepository.findById(userId) if (!userRecord) { return { errorCodes: [UserErrorCode.UserNotFound] } } @@ -340,9 +341,7 @@ export const updateEmailResolver = authorized< MutationUpdateEmailArgs >(async (_, { input: { email } }, { authTrx, uid, log }) => { try { - const user = await userRepository.findOneBy({ - id: uid, - }) + const user = await userRepository.findById(uid) if (!user) { return { diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 366c10ada..333391d62 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -421,7 +421,7 @@ export function authRouter() { const { email, password } = req.body try { const user = await userRepository.findByEmail(email.trim()) - if (!user?.id) { + if (!user || user.status === StatusType.Deleted) { return res.redirect( `${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.UserNotFound}` ) @@ -610,7 +610,7 @@ export function authRouter() { try { const user = await userRepository.findByEmail(email) - if (!user) { + if (!user || user.status === StatusType.Deleted) { return res.redirect(`${env.client.url}/auth/reset-sent`) } diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index 40d4462bb..4af80f0a6 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -46,7 +46,7 @@ export async function createMobileEmailSignInResponse( } const user = await userRepository.findByEmail(email.trim()) - if (!user?.id || !user?.password) { + if (!user || !user.password || user.status === StatusType.Deleted) { throw new Error('user not found') } diff --git a/packages/api/src/routers/user_router.ts b/packages/api/src/routers/user_router.ts index cc830fc71..8e13ee184 100644 --- a/packages/api/src/routers/user_router.ts +++ b/packages/api/src/routers/user_router.ts @@ -39,7 +39,7 @@ export function userRouter() { return } try { - const user = await userRepository.findOneBy({ id: claims.uid }) + const user = await userRepository.findById(claims.uid) if (!user) { res.status(400).send('Bad Request') return diff --git a/packages/api/src/services/newsletters.ts b/packages/api/src/services/newsletters.ts index fb9e33cca..f492cf4b5 100644 --- a/packages/api/src/services/newsletters.ts +++ b/packages/api/src/services/newsletters.ts @@ -21,10 +21,7 @@ export const createNewsletterEmail = async ( userId: string, confirmationCode?: string ): Promise => { - const user = await userRepository.findOne({ - where: { id: userId }, - relations: ['profile'], - }) + const user = await userRepository.findById(userId) if (!user) { return Promise.reject({ errorCode: CreateNewsletterEmailErrorCode.Unauthorized, diff --git a/packages/api/src/services/save_url.ts b/packages/api/src/services/save_url.ts index 0f14b1658..3165bb8fb 100644 --- a/packages/api/src/services/save_url.ts +++ b/packages/api/src/services/save_url.ts @@ -42,9 +42,7 @@ export const saveUrlFromEmail = async ( clientRequestId: string, userId: string ): Promise => { - const user = await userRepository.findOneBy({ - id: userId, - }) + const user = await userRepository.findById(userId) if (!user) { return false } diff --git a/packages/api/src/services/user.ts b/packages/api/src/services/user.ts index 39babc270..870d9ce6b 100644 --- a/packages/api/src/services/user.ts +++ b/packages/api/src/services/user.ts @@ -1,4 +1,4 @@ -import { User } from '../entity/user' +import { StatusType, User } from '../entity/user' import { authTrx } from '../repository' import { userRepository } from '../repository/user' @@ -21,5 +21,5 @@ export const updateUser = async (userId: string, update: Partial) => { } export const findUser = async (id: string): Promise => { - return userRepository.findOneBy({ id }) + return userRepository.findOneBy({ id, status: StatusType.Active }) } diff --git a/packages/api/src/utils/parser.ts b/packages/api/src/utils/parser.ts index f8ca6ffa2..af7e40a9a 100644 --- a/packages/api/src/utils/parser.ts +++ b/packages/api/src/utils/parser.ts @@ -16,6 +16,7 @@ import { ILike } from 'typeorm' import { promisify } from 'util' import { v4 as uuid } from 'uuid' import { Highlight } from '../entity/highlight' +import { StatusType } from '../entity/user' import { env } from '../env' import { PageType, PreparedDocumentInput } from '../generated/graphql' import { userRepository } from '../repository/user' @@ -470,6 +471,7 @@ export const isProbablyArticle = async ( ): Promise => { const user = await userRepository.findOneBy({ email: ILike(email), + status: StatusType.Active, }) return !!user || subject.includes(ARTICLE_PREFIX) } diff --git a/packages/api/test/resolvers/subscriptions.test.ts b/packages/api/test/resolvers/subscriptions.test.ts index 07393fbdb..7a9db963d 100644 --- a/packages/api/test/resolvers/subscriptions.test.ts +++ b/packages/api/test/resolvers/subscriptions.test.ts @@ -146,7 +146,7 @@ describe('Subscriptions API', () => { undefined, SubscriptionType.Newsletter ) - const allSubscriptions = [sub5, ...subscriptions] + const allSubscriptions = [...subscriptions, sub5] const res = await graphqlRequest(query, authToken).expect(200) expect(res.body.data.subscriptions.subscriptions).to.eql( diff --git a/packages/db/migrations/0136.do.add_unique_to_subscription_url.sql b/packages/db/migrations/0136.do.add_unique_to_subscription_url.sql new file mode 100755 index 000000000..d8b83487d --- /dev/null +++ b/packages/db/migrations/0136.do.add_unique_to_subscription_url.sql @@ -0,0 +1,19 @@ +-- Type: DO +-- Name: add_unique_to_subscription_url +-- Description: Add unique constraint to the url field on the omnivore.subscription table + +BEGIN; + +-- Deleting duplicates first to avoid unique constraint violation +WITH DuplicateCTE AS ( + SELECT user_id, url, + ROW_NUMBER() OVER (PARTITION BY user_id, url ORDER BY (SELECT NULL)) AS RowNum + FROM omnivore.subscriptions + WHERE type = 'RSS' +) +DELETE FROM omnivore.subscriptions + WHERE (user_id, url) IN (SELECT user_id, url FROM DuplicateCTE WHERE RowNum > 1); + +ALTER TABLE omnivore.subscriptions ADD CONSTRAINT subscriptions_user_id_url_key UNIQUE (user_id, url); + +COMMIT; diff --git a/packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql b/packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql new file mode 100755 index 000000000..72071eab2 --- /dev/null +++ b/packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql @@ -0,0 +1,9 @@ +-- Type: UNDO +-- Name: add_unique_to_subscription_url +-- Description: Add unique constraint to the url field on the omnivore.subscription table + +BEGIN; + +ALTER TABLE omnivore.subscriptions DROP CONSTRAINT IF EXISTS subscriptions_user_id_url_key; + +COMMIT;