Merge pull request #2942 from omnivore-app/fix/rules
improvement on subscriptions and delete user api
This commit is contained in:
@ -22,6 +22,7 @@ export enum RegistrationType {
|
||||
export enum StatusType {
|
||||
Active = 'ACTIVE',
|
||||
Pending = 'PENDING',
|
||||
Deleted = 'DELETED',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -62,11 +62,11 @@ import {
|
||||
} from '../../services/labels'
|
||||
import {
|
||||
createLibraryItem,
|
||||
findLibraryItemById,
|
||||
findLibraryItemByUrl,
|
||||
findLibraryItemsByPrefix,
|
||||
searchLibraryItems,
|
||||
updateLibraryItem,
|
||||
updateLibraryItemReadingProgress,
|
||||
updateLibraryItems,
|
||||
} from '../../services/library_item'
|
||||
import { parsedContentToLibraryItem } from '../../services/save_page'
|
||||
@ -572,14 +572,8 @@ export const saveArticleReadingProgressResolver = authorized<
|
||||
readingProgressTopPercent,
|
||||
},
|
||||
},
|
||||
{ uid, pubsub }
|
||||
{ log, pubsub, uid }
|
||||
) => {
|
||||
const libraryItem = await findLibraryItemById(id, uid)
|
||||
|
||||
if (!libraryItem) {
|
||||
return { errorCodes: [SaveArticleReadingProgressErrorCode.NotFound] }
|
||||
}
|
||||
|
||||
if (
|
||||
readingProgressPercent < 0 ||
|
||||
readingProgressPercent > 100 ||
|
||||
@ -590,40 +584,26 @@ export const saveArticleReadingProgressResolver = authorized<
|
||||
) {
|
||||
return { errorCodes: [SaveArticleReadingProgressErrorCode.BadData] }
|
||||
}
|
||||
// If we have a top percent, we only save it if it's greater than the current top percent
|
||||
// or set to zero if the top percent is zero.
|
||||
const readingProgressTopPercentToSave = readingProgressTopPercent
|
||||
? Math.max(
|
||||
readingProgressTopPercent,
|
||||
libraryItem.readingProgressTopPercent || 0
|
||||
)
|
||||
: readingProgressTopPercent === 0
|
||||
? 0
|
||||
: undefined
|
||||
// If setting to zero we accept the update, otherwise we require it
|
||||
// be greater than the current reading progress.
|
||||
const updatedPart: QueryDeepPartialEntity<LibraryItem> = {
|
||||
readingProgressBottomPercent:
|
||||
readingProgressPercent === 0
|
||||
? 0
|
||||
: Math.max(
|
||||
readingProgressPercent,
|
||||
libraryItem.readingProgressBottomPercent
|
||||
),
|
||||
readingProgressHighestReadAnchor:
|
||||
readingProgressAnchorIndex === 0
|
||||
? 0
|
||||
: Math.max(
|
||||
readingProgressAnchorIndex || 0,
|
||||
libraryItem.readingProgressHighestReadAnchor
|
||||
),
|
||||
readingProgressTopPercent: readingProgressTopPercentToSave,
|
||||
readAt: new Date(),
|
||||
}
|
||||
const updatedItem = await updateLibraryItem(id, updatedPart, uid, pubsub)
|
||||
try {
|
||||
const updatedItem = await updateLibraryItemReadingProgress(
|
||||
id,
|
||||
uid,
|
||||
readingProgressPercent,
|
||||
readingProgressTopPercent,
|
||||
readingProgressAnchorIndex,
|
||||
pubsub
|
||||
)
|
||||
if (!updatedItem) {
|
||||
return { errorCodes: [SaveArticleReadingProgressErrorCode.BadData] }
|
||||
}
|
||||
|
||||
return {
|
||||
updatedArticle: libraryItemToArticle(updatedItem),
|
||||
return {
|
||||
updatedArticle: libraryItemToArticle(updatedItem),
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('saveArticleReadingProgressResolver error', error)
|
||||
|
||||
return { errorCodes: [SaveArticleReadingProgressErrorCode.Unauthorized] }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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<GroupsSuccess, GroupsError>(
|
||||
async (_, __, { uid, log }) => {
|
||||
try {
|
||||
const user = await userRepository.findOneBy({
|
||||
id: uid,
|
||||
})
|
||||
const user = await userRepository.findById(uid)
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [GroupsErrorCode.Unauthorized],
|
||||
|
||||
@ -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] }
|
||||
}
|
||||
|
||||
@ -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] }
|
||||
|
||||
@ -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 {
|
||||
@ -184,7 +185,6 @@ export const subscribeResolver = authorized<
|
||||
url: input.url || undefined,
|
||||
name: input.name || undefined,
|
||||
user: { id: uid },
|
||||
status: SubscriptionStatus.Active,
|
||||
type: input.subscriptionType || SubscriptionType.Rss, // default to rss
|
||||
})
|
||||
)
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import * as jwt from 'jsonwebtoken'
|
||||
import { RegistrationType, User as UserEntity } from '../../entity/user'
|
||||
import {
|
||||
RegistrationType,
|
||||
StatusType,
|
||||
User as UserEntity,
|
||||
} from '../../entity/user'
|
||||
import { env } from '../../env'
|
||||
import {
|
||||
DeleteAccountError,
|
||||
@ -38,6 +42,7 @@ import {
|
||||
import { userRepository } from '../../repository/user'
|
||||
import { createUser } from '../../services/create_user'
|
||||
import { sendVerificationEmail } from '../../services/send_emails'
|
||||
import { updateUser } from '../../services/user'
|
||||
import { authorized, userDataToUser } from '../../utils/helpers'
|
||||
import { validateUsername } from '../../utils/usernamePolicy'
|
||||
import { WithDataSourcesContext } from '../types'
|
||||
@ -47,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] }
|
||||
}
|
||||
@ -87,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] }
|
||||
}
|
||||
@ -112,6 +113,7 @@ export const updateUserProfileResolver = authorized<
|
||||
profile: {
|
||||
username: lowerCasedUsername,
|
||||
},
|
||||
status: StatusType.Active,
|
||||
})
|
||||
if (existingUser?.id) {
|
||||
return {
|
||||
@ -156,6 +158,7 @@ export const googleLoginResolver: ResolverFn<
|
||||
|
||||
const user = await userRepository.findOneBy({
|
||||
email,
|
||||
status: StatusType.Active,
|
||||
})
|
||||
if (!user?.id) {
|
||||
return { errorCodes: [LoginErrorCode.UserNotFound] }
|
||||
@ -251,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
|
||||
}
|
||||
@ -277,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] }
|
||||
}
|
||||
@ -313,9 +319,10 @@ export const deleteAccountResolver = authorized<
|
||||
DeleteAccountSuccess,
|
||||
DeleteAccountError,
|
||||
MutationDeleteAccountArgs
|
||||
>(async (_, { userID }, { authTrx, log }) => {
|
||||
const result = await authTrx(async (t) => {
|
||||
return t.withRepository(userRepository).delete(userID)
|
||||
>(async (_, { userID }, { log }) => {
|
||||
// soft delete user
|
||||
const result = await updateUser(userID, {
|
||||
status: StatusType.Deleted,
|
||||
})
|
||||
if (!result.affected) {
|
||||
log.error('Error deleting user account')
|
||||
@ -334,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 {
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
suggestedUsername,
|
||||
} from './jwt_helpers'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { StatusType } from '../../entity/user'
|
||||
|
||||
const appleBaseURL = 'https://appleid.apple.com'
|
||||
const audienceName = 'app.omnivore.app'
|
||||
@ -122,6 +123,7 @@ export async function handleAppleWebAuth(
|
||||
const user = await userRepository.findOneBy({
|
||||
sourceUserId: decodedTokenResult.sourceUserId,
|
||||
source: 'APPLE',
|
||||
status: StatusType.Active,
|
||||
})
|
||||
const userId = user?.id
|
||||
|
||||
|
||||
@ -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`)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { google, oauth2_v2 as oauthV2 } from 'googleapis'
|
||||
import { OAuth2Client } from 'googleapis-common'
|
||||
import url from 'url'
|
||||
import { StatusType } from '../../entity/user'
|
||||
import { env, homePageURL } from '../../env'
|
||||
import { LoginErrorCode } from '../../generated/graphql'
|
||||
import { userRepository } from '../../repository/user'
|
||||
@ -130,6 +131,7 @@ export async function handleGoogleWebAuth(
|
||||
const user = await userRepository.findOneBy({
|
||||
email,
|
||||
source: 'GOOGLE',
|
||||
status: StatusType.Active,
|
||||
})
|
||||
const userId = user?.id
|
||||
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -438,6 +438,75 @@ export const updateLibraryItem = async (
|
||||
return updatedLibraryItem
|
||||
}
|
||||
|
||||
export const updateLibraryItemReadingProgress = async (
|
||||
id: string,
|
||||
userId: string,
|
||||
bottomPercent: number,
|
||||
topPercent: number | null = null,
|
||||
anchorIndex: number | null = null,
|
||||
pubsub = createPubSubClient()
|
||||
): Promise<LibraryItem | null> => {
|
||||
// If we have a top percent, we only save it if it's greater than the current top percent
|
||||
// or set to zero if the top percent is zero.
|
||||
const result = (await authTrx(
|
||||
async (tx) =>
|
||||
tx.getRepository(LibraryItem).query(
|
||||
`
|
||||
UPDATE omnivore.library_item
|
||||
SET reading_progress_top_percent = CASE
|
||||
WHEN reading_progress_top_percent < $2 THEN $2
|
||||
WHEN $2 = 0 THEN 0
|
||||
ELSE reading_progress_top_percent
|
||||
END,
|
||||
reading_progress_bottom_percent = CASE
|
||||
WHEN reading_progress_bottom_percent < $3 THEN $3
|
||||
WHEN $3 = 0 THEN 0
|
||||
ELSE reading_progress_bottom_percent
|
||||
END,
|
||||
reading_progress_highest_read_anchor = CASE
|
||||
WHEN reading_progress_top_percent < $4 THEN $4
|
||||
WHEN $4 = 0 THEN 0
|
||||
ELSE reading_progress_highest_read_anchor
|
||||
END,
|
||||
read_at = now()
|
||||
WHERE id = $1 AND (
|
||||
(reading_progress_top_percent < $2 OR $2 = 0) OR
|
||||
(reading_progress_bottom_percent < $3 OR $3 = 0) OR
|
||||
(reading_progress_highest_read_anchor < $4 OR $4 = 0)
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
reading_progress_top_percent as "readingProgressTopPercent",
|
||||
reading_progress_bottom_percent as "readingProgressBottomPercent",
|
||||
reading_progress_highest_read_anchor as "readingProgressHighestReadAnchor",
|
||||
read_at as "readAt"
|
||||
`,
|
||||
[id, topPercent, bottomPercent, anchorIndex]
|
||||
),
|
||||
undefined,
|
||||
userId
|
||||
)) as [LibraryItem[], number]
|
||||
if (result[1] === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const updatedItem = result[0][0]
|
||||
await pubsub.entityUpdated<QueryDeepPartialEntity<LibraryItem>>(
|
||||
EntityType.PAGE,
|
||||
{
|
||||
id,
|
||||
readingProgressBottomPercent: updatedItem.readingProgressBottomPercent,
|
||||
readingProgressTopPercent: updatedItem.readingProgressTopPercent,
|
||||
readingProgressHighestReadAnchor:
|
||||
updatedItem.readingProgressHighestReadAnchor,
|
||||
readAt: updatedItem.readAt,
|
||||
},
|
||||
userId
|
||||
)
|
||||
|
||||
return updatedItem
|
||||
}
|
||||
|
||||
export const createLibraryItems = async (
|
||||
libraryItems: DeepPartial<LibraryItem>[],
|
||||
userId: string
|
||||
|
||||
@ -21,10 +21,7 @@ export const createNewsletterEmail = async (
|
||||
userId: string,
|
||||
confirmationCode?: string
|
||||
): Promise<NewsletterEmail> => {
|
||||
const user = await userRepository.findOne({
|
||||
where: { id: userId },
|
||||
relations: ['profile'],
|
||||
})
|
||||
const user = await userRepository.findById(userId)
|
||||
if (!user) {
|
||||
return Promise.reject({
|
||||
errorCode: CreateNewsletterEmailErrorCode.Unauthorized,
|
||||
|
||||
@ -42,9 +42,7 @@ export const saveUrlFromEmail = async (
|
||||
clientRequestId: string,
|
||||
userId: string
|
||||
): Promise<boolean> => {
|
||||
const user = await userRepository.findOneBy({
|
||||
id: userId,
|
||||
})
|
||||
const user = await userRepository.findById(userId)
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { User } from '../entity/user'
|
||||
import { StatusType, User } from '../entity/user'
|
||||
import { authTrx } from '../repository'
|
||||
import { userRepository } from '../repository/user'
|
||||
|
||||
@ -13,7 +13,7 @@ export const deleteUser = async (userId: string) => {
|
||||
}
|
||||
|
||||
export const updateUser = async (userId: string, update: Partial<User>) => {
|
||||
await authTrx(
|
||||
return authTrx(
|
||||
async (t) => t.getRepository(User).update(userId, update),
|
||||
undefined,
|
||||
userId
|
||||
@ -21,5 +21,5 @@ export const updateUser = async (userId: string, update: Partial<User>) => {
|
||||
}
|
||||
|
||||
export const findUser = async (id: string): Promise<User | null> => {
|
||||
return userRepository.findOneBy({ id })
|
||||
return userRepository.findOneBy({ id, status: StatusType.Active })
|
||||
}
|
||||
|
||||
@ -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'
|
||||
@ -465,6 +466,7 @@ export const isProbablyArticle = async (
|
||||
): Promise<boolean> => {
|
||||
const user = await userRepository.findOneBy({
|
||||
email: ILike(email),
|
||||
status: StatusType.Active,
|
||||
})
|
||||
return !!user || subject.includes(ARTICLE_PREFIX)
|
||||
}
|
||||
|
||||
@ -706,7 +706,6 @@ describe('Article API', () => {
|
||||
).to.eq(75)
|
||||
|
||||
// Now try to set to a lower value (50), value should not be updated
|
||||
// refresh index to ensure the reading progress is updated
|
||||
const secondQuery = saveArticleReadingProgressQuery(itemId, 50)
|
||||
const secondRes = await graphqlRequest(secondQuery, authToken).expect(200)
|
||||
expect(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -9,7 +9,7 @@ import { findProfile } from '../../src/services/profile'
|
||||
import { deleteUser, findUser } from '../../src/services/user'
|
||||
import { hashPassword } from '../../src/utils/auth'
|
||||
import { createTestUser } from '../db'
|
||||
import { graphqlRequest, request } from '../util'
|
||||
import { generateFakeUuid, graphqlRequest, request } from '../util'
|
||||
|
||||
describe('User API', () => {
|
||||
const correctPassword = 'fakePassword'
|
||||
@ -238,4 +238,58 @@ describe('User API', () => {
|
||||
return graphqlRequest(query, invalidAuthToken).expect(500)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Delete account', () => {
|
||||
const query = (userId: string) => `
|
||||
mutation {
|
||||
deleteAccount(
|
||||
userID: "${userId}"
|
||||
) {
|
||||
... on DeleteAccountSuccess {
|
||||
userID
|
||||
}
|
||||
... on DeleteAccountError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
let userId: string
|
||||
let authToken: string
|
||||
|
||||
before(async () => {
|
||||
const user = await createTestUser('to_delete_user')
|
||||
const res = await request
|
||||
.post('/local/debug/fake-user-login')
|
||||
.send({ fakeEmail: user.email })
|
||||
userId = user.id
|
||||
authToken = res.body.authToken
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await deleteUser(userId)
|
||||
})
|
||||
|
||||
context('when user id is valid', () => {
|
||||
it('deletes user and responds with 200', async () => {
|
||||
const response = await graphqlRequest(query(userId), authToken).expect(
|
||||
200
|
||||
)
|
||||
expect(response.body.data.deleteAccount.userID).to.eql(userId)
|
||||
})
|
||||
})
|
||||
|
||||
context('when user not found', () => {
|
||||
it('responds with error code UserNotFound', async () => {
|
||||
const response = await graphqlRequest(
|
||||
query(generateFakeUuid()),
|
||||
authToken
|
||||
).expect(200)
|
||||
expect(response.body.data.deleteAccount.errorCodes).to.eql([
|
||||
'USER_NOT_FOUND',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
9
packages/db/migrations/0135.do.alter_user_status_type.sql
Executable file
9
packages/db/migrations/0135.do.alter_user_status_type.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: DO
|
||||
-- Name: alter_user_status_type
|
||||
-- Description: Add DELETED to the user_status_type enum
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TYPE user_status_type ADD VALUE 'DELETED';
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0135.undo.alter_user_status_type.sql
Executable file
9
packages/db/migrations/0135.undo.alter_user_status_type.sql
Executable file
@ -0,0 +1,9 @@
|
||||
-- Type: UNDO
|
||||
-- Name: alter_user_status_type
|
||||
-- Description: Add DELETED to the user_status_type enum
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TYPE user_status_type DROP VALUE IF EXISTS 'DELETED';
|
||||
|
||||
COMMIT;
|
||||
18
packages/db/migrations/0136.do.add_unique_to_subscription_url.sql
Executable file
18
packages/db/migrations/0136.do.add_unique_to_subscription_url.sql
Executable file
@ -0,0 +1,18 @@
|
||||
-- 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 id, ROW_NUMBER() OVER (PARTITION BY user_id, url ORDER BY status, last_fetched_at DESC NULLS LAST) AS row_number
|
||||
FROM omnivore.subscriptions
|
||||
WHERE type = 'RSS'
|
||||
)
|
||||
DELETE FROM omnivore.subscriptions
|
||||
WHERE id IN (SELECT id FROM DuplicateCTE WHERE row_number > 1);
|
||||
|
||||
ALTER TABLE omnivore.subscriptions ADD CONSTRAINT subscriptions_user_id_url_key UNIQUE (user_id, url);
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql
Executable file
9
packages/db/migrations/0136.undo.add_unique_to_subscription_url.sql
Executable file
@ -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;
|
||||
15
packages/db/migrations/0137.do.alter_library_item_tsv_update_trigger.sql
Executable file
15
packages/db/migrations/0137.do.alter_library_item_tsv_update_trigger.sql
Executable file
@ -0,0 +1,15 @@
|
||||
-- Type: DO
|
||||
-- Name: alter_library_item_tsv_update_trigger
|
||||
-- Description: Alter library_item_tsv_update trigger on omnivore.library_item table to add conditions
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TRIGGER IF EXISTS library_item_tsv_update ON omnivore.library_item;
|
||||
|
||||
CREATE TRIGGER library_item_tsv_update
|
||||
BEFORE INSERT OR UPDATE OF readable_content, site_name, title, author, description, note, highlight_annotations
|
||||
ON omnivore.library_item
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_library_item_tsv();
|
||||
|
||||
COMMIT;
|
||||
15
packages/db/migrations/0137.undo.alter_library_item_tsv_update_trigger.sql
Executable file
15
packages/db/migrations/0137.undo.alter_library_item_tsv_update_trigger.sql
Executable file
@ -0,0 +1,15 @@
|
||||
-- Type: UNDO
|
||||
-- Name: alter_library_item_tsv_update_trigger
|
||||
-- Description: Alter library_item_tsv_update trigger on omnivore.library_item table to add conditions
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TRIGGER IF EXISTS library_item_tsv_update ON omnivore.library_item;
|
||||
|
||||
CREATE TRIGGER library_item_tsv_update
|
||||
BEFORE INSERT OR UPDATE
|
||||
ON omnivore.library_item
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE update_library_item_tsv();
|
||||
|
||||
COMMIT;
|
||||
@ -9,7 +9,6 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "~7.12.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"pg": "^8.3.0",
|
||||
"postgrator": "^4.1.1",
|
||||
|
||||
@ -6,6 +6,9 @@ interface SearchResponse {
|
||||
edges: Edge[]
|
||||
}
|
||||
}
|
||||
errors?: {
|
||||
message: string
|
||||
}[]
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
@ -28,7 +31,6 @@ interface Label {
|
||||
}
|
||||
|
||||
export const search = async (
|
||||
userId: string,
|
||||
apiEndpoint: string,
|
||||
auth: string,
|
||||
query: string
|
||||
@ -75,6 +77,12 @@ export const search = async (
|
||||
}
|
||||
)
|
||||
|
||||
if (response.data.errors) {
|
||||
console.error(response.data.errors)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const edges = response.data.data.search.edges
|
||||
if (edges.length === 0) {
|
||||
return []
|
||||
@ -89,14 +97,13 @@ export const search = async (
|
||||
}
|
||||
|
||||
export const filterPage = async (
|
||||
userId: string,
|
||||
apiEndpoint: string,
|
||||
auth: string,
|
||||
filter: string,
|
||||
pageId: string
|
||||
): Promise<Page | null> => {
|
||||
filter += ` includes:${pageId}`
|
||||
const pages = await search(userId, apiEndpoint, auth, filter)
|
||||
const pages = await search(apiEndpoint, auth, filter)
|
||||
|
||||
return pages.length > 0 ? pages[0] : null
|
||||
}
|
||||
|
||||
@ -93,7 +93,6 @@ export const triggerActions = async (
|
||||
}
|
||||
|
||||
const filteredPage = await filterPage(
|
||||
userId,
|
||||
apiEndpoint,
|
||||
authToken,
|
||||
rule.filter,
|
||||
|
||||
23
yarn.lock
23
yarn.lock
@ -2228,17 +2228,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||
|
||||
"@elastic/elasticsearch@~7.12.0":
|
||||
version "7.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.12.0.tgz#dbb51a2841f644b670a56d8c15899e860928856f"
|
||||
integrity sha512-GquUEytCijFRPEk3DKkkDdyhspB3qbucVQOwih9uNyz3iz804I+nGBUsFo2LwVvLQmQfEM0IY2+yoYfEz5wMug==
|
||||
dependencies:
|
||||
debug "^4.3.1"
|
||||
hpagent "^0.1.1"
|
||||
ms "^2.1.3"
|
||||
pump "^3.0.0"
|
||||
secure-json-parse "^2.3.1"
|
||||
|
||||
"@emotion/cache@^10.0.27":
|
||||
version "10.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||
@ -15835,11 +15824,6 @@ hpack.js@^2.1.6:
|
||||
readable-stream "^2.0.1"
|
||||
wbuf "^1.1.0"
|
||||
|
||||
hpagent@^0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9"
|
||||
integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==
|
||||
|
||||
html-encoding-sniffer@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
|
||||
@ -20177,7 +20161,7 @@ ms@2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
|
||||
ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
@ -24468,11 +24452,6 @@ search-query-parser@^1.6.0:
|
||||
resolved "https://registry.yarnpkg.com/search-query-parser/-/search-query-parser-1.6.0.tgz#d69ade33f3685cae25613a70189b7b18970b46f1"
|
||||
integrity sha512-bhf+phLlKF38nuniwLcVHWPArHGdzenlPhPi955CR3vm1QQifXIuPHwAffhjapojdVVzmv4hgIJ6NOX1d/w+Uw==
|
||||
|
||||
secure-json-parse@^2.3.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85"
|
||||
integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==
|
||||
|
||||
selderee@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.6.0.tgz#f3bee66cfebcb6f33df98e4a1df77388b42a96f7"
|
||||
|
||||
Reference in New Issue
Block a user