Skip to content

Commit

Permalink
Merge pull request #4350 from omnivore-app/feature/archive-account
Browse files Browse the repository at this point in the history
feature/archive account
  • Loading branch information
sywhb committed Aug 30, 2024
2 parents 49f9fb4 + f89e44d commit f3e3d57
Show file tree
Hide file tree
Showing 17 changed files with 101 additions and 69 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/lint-migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ jobs:
run: |
modified_migrations=$(git diff --diff-filter=d --name-only main 'packages/db/migrations/*.do.*.sql')
echo "$modified_migrations"
echo "text<<EOF" >> $GITHUB_OUTPUT
echo "file_names=$modified_migrations" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
id: modified-migrations
- uses: sbdchd/squawk-action@v1
with:
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/entity/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum StatusType {
Active = 'ACTIVE',
Pending = 'PENDING',
Deleted = 'DELETED',
Archived = 'ARCHIVED',
}

@Entity()
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/jobs/email/inbound_emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import { enqueueSendEmail } from '../../utils/createTask'
import { generateSlug, isUrl } from '../../utils/helpers'
import { logger } from '../../utils/logger'
import {
parseEmailAddress,
isProbablyArticle,
getTitleFromEmailSubject,
generateUniqueUrl,
getTitleFromEmailSubject,
isProbablyArticle,
parseEmailAddress,
} from '../../utils/parser'
import {
generateUploadFilePathName,
Expand Down
3 changes: 1 addition & 2 deletions packages/api/src/jobs/rss/refreshAllFeeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,11 @@ export const refreshAllFeeds = async (db: DataSource): Promise<boolean> => {
FROM
omnivore.subscriptions s
INNER JOIN
omnivore.user u ON u.id = s.user_id
omnivore.user u ON u.id = s.user_id AND u.status = $4
WHERE
s.type = $1
AND s.status = $2
AND (s.scheduled_at <= NOW() OR s.scheduled_at IS NULL)
AND u.status = $4
GROUP BY
url
`,
Expand Down
10 changes: 6 additions & 4 deletions packages/api/src/routers/auth/auth_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from '../../utils/auth'
import { corsConfig } from '../../utils/corsConfig'
import { logger } from '../../utils/logger'
import { DEFAULT_HOME_PATH } from '../../utils/navigation'
import { ARCHIVE_ACCOUNT_PATH, DEFAULT_HOME_PATH } from '../../utils/navigation'
import { hourlyLimiter } from '../../utils/rate_limit'
import { verifyChallengeRecaptcha } from '../../utils/recaptcha'
import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
Expand Down Expand Up @@ -378,9 +378,11 @@ export function authRouter() {
}
}

redirectUri = redirectUri
? redirectUri
: `${env.client.url}${DEFAULT_HOME_PATH}`
if (user.status === StatusType.Archived) {
redirectUri = `${env.client.url}${ARCHIVE_ACCOUNT_PATH}`
}

redirectUri = redirectUri ?? `${env.client.url}${DEFAULT_HOME_PATH}`

const message = res.get('Message')
if (message) {
Expand Down
20 changes: 11 additions & 9 deletions packages/api/src/routers/auth/google_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { env, homePageURL } from '../../env'
import { LoginErrorCode } from '../../generated/graphql'
import { userRepository } from '../../repository/user'
import { logger } from '../../utils/logger'
import { ARCHIVE_ACCOUNT_PATH, DEFAULT_HOME_PATH } from '../../utils/navigation'
import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
import { DecodeTokenResult } from './auth_types'
import { createPendingUserToken, createWebAuthToken } from './jwt_helpers'
import { DEFAULT_HOME_PATH } from '../../utils/navigation'

export const googleAuthMobile = (): OAuth2Client =>
new google.auth.OAuth2(env.google.auth.clientId, env.google.auth.secret)
Expand Down Expand Up @@ -132,7 +132,6 @@ export async function handleGoogleWebAuth(
const user = await userRepository.findOneBy({
email,
source: 'GOOGLE',
status: StatusType.Active,
})
const userId = user?.id

Expand All @@ -158,15 +157,18 @@ export async function handleGoogleWebAuth(
}
}

let redirectURL = `${baseURL()}${
user.status === StatusType.Archived
? ARCHIVE_ACCOUNT_PATH
: DEFAULT_HOME_PATH
}`

const authToken = await createWebAuthToken(userId)
if (authToken) {
const ssoToken = createSsoToken(
authToken,
`${baseURL()}${DEFAULT_HOME_PATH}`
)
const redirectURL = isVercel
? ssoRedirectURL(ssoToken)
: `${baseURL()}${DEFAULT_HOME_PATH}`
if (isVercel) {
const ssoToken = createSsoToken(authToken, redirectURL)
redirectURL = ssoRedirectURL(ssoToken)
}

return {
authToken,
Expand Down
1 change: 0 additions & 1 deletion packages/api/src/routers/export_router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import express, { Router } from 'express'
import { TaskState } from '../generated/graphql'
import { jobStateToTaskState } from '../queue-processor'
import { countExportsWithin24Hours, saveExport } from '../services/export'
import { sendExportJobEmail } from '../services/send_emails'
import { getClaimsByToken, getTokenByRequest } from '../utils/auth'
import { corsConfig } from '../utils/corsConfig'
import { queueExportJob } from '../utils/createTask'
Expand Down
6 changes: 5 additions & 1 deletion packages/api/src/services/library_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,11 @@ export const updateLibraryItemReadingProgress = async (
}

const updatedItem = result[0][0]
await pubsub.entityUpdated<ItemEvent>(EntityType.ITEM, updatedItem, userId)
const readingProgress = updatedItem.readingProgressBottomPercent
if (readingProgress === 0 || readingProgress === 100) {
// only send PAGE_UPDATED event if users mark item as read or unread
await pubsub.entityUpdated<ItemEvent>(EntityType.ITEM, updatedItem, userId)
}

return updatedItem
}
Expand Down
8 changes: 7 additions & 1 deletion packages/api/src/services/newsletters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid'
import { NewsletterEmail } from '../entity/newsletter_email'
import { StatusType } from '../entity/user'
import { env } from '../env'
import {
CreateNewsletterEmailErrorCode,
Expand Down Expand Up @@ -91,7 +92,12 @@ export const findNewsletterEmailByAddress = async (
const address = parsedAddress(emailAddress)
return getRepository(NewsletterEmail)
.createQueryBuilder('newsletter_email')
.innerJoinAndSelect('newsletter_email.user', 'user')
.innerJoinAndSelect(
'newsletter_email.user',
'user',
'user.status = :status',
{ status: StatusType.Active }
)
.where('LOWER(address) = :address', { address: address.toLowerCase() })
.getOne()
}
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/services/rules.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ArrayContains, ILike, IsNull, Not } from 'typeorm'
import { Rule, RuleAction, RuleEventType } from '../entity/rule'
import { StatusType } from '../entity/user'
import { authTrx, getRepository } from '../repository'

export const createRule = async (
Expand Down Expand Up @@ -62,7 +63,7 @@ export const findEnabledRules = async (
eventType: RuleEventType
) => {
return getRepository(Rule).findBy({
user: { id: userId },
user: { id: userId, status: StatusType.Active },
enabled: true,
eventTypes: ArrayContains([eventType]),
failedAt: IsNull(), // only rules that have not failed
Expand Down
38 changes: 10 additions & 28 deletions packages/api/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,38 +81,20 @@ export const createUsers = async (users: DeepPartial<User>[]) => {

export const batchDelete = async (criteria: FindOptionsWhere<User>) => {
const userQb = getRepository(User).createQueryBuilder().where(criteria)
const userCountSql = queryBuilderToRawSql(userQb.select('COUNT(1)'))
const userSubQuery = queryBuilderToRawSql(
userQb.select('array_agg(id::UUID) into user_ids')
)
const batchSize = 100
const userSubQuery = queryBuilderToRawSql(userQb.select('id').take(batchSize))

const batchSize = 1000
const sql = `
-- Set batch size
DO $$
DECLARE
batch_size INT := ${batchSize};
user_ids UUID[];
BEGIN
-- Loop through batches of users
FOR i IN 0..CEIL((${userCountSql}) * 1.0 / batch_size) - 1 LOOP
-- GET batch of user ids
${userSubQuery} LIMIT batch_size;
-- Loop through batches of items
FOR j IN 0..CEIL((SELECT COUNT(1) FROM omnivore.library_item WHERE user_id = ANY(user_ids)) * 1.0 / batch_size) - 1 LOOP
-- Delete batch of items
DELETE FROM omnivore.library_item
WHERE id = ANY(
SELECT id
FROM omnivore.library_item
WHERE user_id = ANY(user_ids)
LIMIT batch_size
);
END LOOP;
-- Delete the batch of users
DELETE FROM omnivore.user WHERE id = ANY(user_ids);
LOOP
DELETE FROM omnivore.user
WHERE id IN (${userSubQuery});
EXIT WHEN NOT FOUND;
-- Avoid overwhelming the server
PERFORM pg_sleep(0.1);
END LOOP;
END $$
`
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/utils/navigation.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const DEFAULT_HOME_PATH = '/home'
export const ARCHIVE_ACCOUNT_PATH = '/account-archived'
28 changes: 9 additions & 19 deletions packages/api/test/resolvers/newsletters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('Newsletters API', () => {
.post('/local/debug/fake-user-login')
.send({ fakeEmail: user.email })

authToken = res.body.authToken
authToken = res.body.authToken as string
})

after(async () => {
Expand Down Expand Up @@ -65,14 +65,8 @@ describe('Newsletters API', () => {

before(async () => {
// create test newsletter emails
const newsletterEmail1 = await createNewsletterEmail(
user.id,
'[email protected]'
)
const newsletterEmail2 = await createNewsletterEmail(
user.id,
'[email protected]'
)
const newsletterEmail1 = await createNewsletterEmail(user.id)
const newsletterEmail2 = await createNewsletterEmail(user.id)
newsletterEmails = [newsletterEmail1, newsletterEmail2]

// create testing subscriptions
Expand All @@ -89,7 +83,9 @@ describe('Newsletters API', () => {
it('responds with newsletter emails sort by created_at desc', async () => {
const response = await graphqlRequest(query, authToken).expect(200)
expect(
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
response.body.data.newsletterEmails.newsletterEmails.map((e: any) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return {
...e,
createdAt:
Expand Down Expand Up @@ -124,10 +120,7 @@ describe('Newsletters API', () => {

before(async () => {
// create test newsletter emails
newsletterEmail = await createNewsletterEmail(
user.id,
'[email protected]'
)
newsletterEmail = await createNewsletterEmail(user.id)

// create unsubscribed subscriptions
await createSubscription(
Expand Down Expand Up @@ -190,7 +183,7 @@ describe('Newsletters API', () => {
const response = await graphqlRequest(query, authToken, {
input: {
folder,
}
},
}).expect(200)
const newsletterEmail = await findNewsletterEmailById(
response.body.data.createNewsletterEmail.newsletterEmail.id
Expand Down Expand Up @@ -239,10 +232,7 @@ describe('Newsletters API', () => {
context('when newsletter email exists', () => {
before(async () => {
// create test newsletter emails
const newsletterEmail = await createNewsletterEmail(
user.id,
'[email protected]'
)
const newsletterEmail = await createNewsletterEmail(user.id)
newsletterEmailId = newsletterEmail.id
})

Expand All @@ -254,7 +244,7 @@ describe('Newsletters API', () => {
it('responds with status code 200', async () => {
const response = await graphqlRequest(query, authToken).expect(200)
const newsletterEmail = await findNewsletterEmailByAddress(
response.body.data.deleteNewsletterEmail.newsletterEmail.id
response.body.data.deleteNewsletterEmail.newsletterEmail.address
)
expect(newsletterEmail).to.be.null
})
Expand Down
14 changes: 14 additions & 0 deletions packages/db/migrations/0187.do.allow_admin_to_delete_filters.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Type: DO
-- Name: allow_admin_to_delete_filters
-- Description: Add permissions to delete data from filters table to the omnivore_admin role

BEGIN;

GRANT SELECT, INSERT, UPDATE, DELETE ON omnivore.filters TO omnivore_admin;

CREATE POLICY filters_admin_policy on omnivore.filters
FOR ALL
TO omnivore_admin
USING (true);

COMMIT;
11 changes: 11 additions & 0 deletions packages/db/migrations/0187.undo.allow_admin_to_delete_filters.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Type: UNDO
-- Name: allow_admin_to_delete_filters
-- Description: Add permissions to delete data from filters table to the omnivore_admin role

BEGIN;

DROP POLICY filters_admin_policy on omnivore.filters;

REVOKE SELECT, INSERT, UPDATE, DELETE ON omnivore.filters FROM omnivore_admin;

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Type: DO
-- Name: add_archived_status_to_user
-- Description: Add ARCHIVED status to the user table

BEGIN;

ALTER TYPE user_status_type ADD VALUE IF NOT EXISTS 'ARCHIVED';

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: add_archived_status_to_user
-- Description: Add ARCHIVED status to the user table

BEGIN;

ALTER TYPE user_status_type DROP VALUE IF EXISTS 'ARCHIVED';

COMMIT;

0 comments on commit f3e3d57

Please sign in to comment.