Merge pull request #4350 from omnivore-app/feature/archive-account
feature/archive account
This commit is contained in:
2
.github/workflows/lint-migrations.yml
vendored
2
.github/workflows/lint-migrations.yml
vendored
@ -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:
|
||||
|
||||
@ -23,6 +23,7 @@ export enum StatusType {
|
||||
Active = 'ACTIVE',
|
||||
Pending = 'PENDING',
|
||||
Deleted = 'DELETED',
|
||||
Archived = 'ARCHIVED',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
`,
|
||||
|
||||
@ -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'
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
@ -132,7 +132,6 @@ export async function handleGoogleWebAuth(
|
||||
const user = await userRepository.findOneBy({
|
||||
email,
|
||||
source: 'GOOGLE',
|
||||
status: StatusType.Active,
|
||||
})
|
||||
const userId = user?.id
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
DELETE FROM omnivore.user
|
||||
WHERE id IN (${userSubQuery});
|
||||
|
||||
-- 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;
|
||||
EXIT WHEN NOT FOUND;
|
||||
|
||||
-- Delete the batch of users
|
||||
DELETE FROM omnivore.user WHERE id = ANY(user_ids);
|
||||
-- Avoid overwhelming the server
|
||||
PERFORM pg_sleep(0.1);
|
||||
END LOOP;
|
||||
END $$
|
||||
`
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export const DEFAULT_HOME_PATH = '/home'
|
||||
export const ARCHIVE_ACCOUNT_PATH = '/account-archived'
|
||||
|
||||
@ -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 () => {
|
||||
@ -65,14 +65,8 @@ describe('Newsletters API', () => {
|
||||
|
||||
before(async () => {
|
||||
// create test newsletter emails
|
||||
const newsletterEmail1 = await createNewsletterEmail(
|
||||
user.id,
|
||||
'Test_email_address_1@omnivore.app'
|
||||
)
|
||||
const newsletterEmail2 = await createNewsletterEmail(
|
||||
user.id,
|
||||
'Test_email_address_2@omnivore.app'
|
||||
)
|
||||
const newsletterEmail1 = await createNewsletterEmail(user.id)
|
||||
const newsletterEmail2 = await createNewsletterEmail(user.id)
|
||||
newsletterEmails = [newsletterEmail1, newsletterEmail2]
|
||||
|
||||
// create testing subscriptions
|
||||
@ -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:
|
||||
@ -124,10 +120,7 @@ describe('Newsletters API', () => {
|
||||
|
||||
before(async () => {
|
||||
// create test newsletter emails
|
||||
newsletterEmail = await createNewsletterEmail(
|
||||
user.id,
|
||||
'Test_email_address_1@omnivore.app'
|
||||
)
|
||||
newsletterEmail = await createNewsletterEmail(user.id)
|
||||
|
||||
// create unsubscribed subscriptions
|
||||
await createSubscription(
|
||||
@ -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
|
||||
@ -239,10 +232,7 @@ describe('Newsletters API', () => {
|
||||
context('when newsletter email exists', () => {
|
||||
before(async () => {
|
||||
// create test newsletter emails
|
||||
const newsletterEmail = await createNewsletterEmail(
|
||||
user.id,
|
||||
'Test_email_address_1@omnivore.app'
|
||||
)
|
||||
const newsletterEmail = await createNewsletterEmail(user.id)
|
||||
newsletterEmailId = newsletterEmail.id
|
||||
})
|
||||
|
||||
@ -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
|
||||
})
|
||||
|
||||
14
packages/db/migrations/0187.do.allow_admin_to_delete_filters.sql
Executable file
14
packages/db/migrations/0187.do.allow_admin_to_delete_filters.sql
Executable file
@ -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
packages/db/migrations/0187.undo.allow_admin_to_delete_filters.sql
Executable file
11
packages/db/migrations/0187.undo.allow_admin_to_delete_filters.sql
Executable file
@ -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;
|
||||
9
packages/db/migrations/0188.do.add_archived_status_to_user.sql
Executable file
9
packages/db/migrations/0188.do.add_archived_status_to_user.sql
Executable file
@ -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;
|
||||
9
packages/db/migrations/0188.undo.add_archived_status_to_user.sql
Executable file
9
packages/db/migrations/0188.undo.add_archived_status_to_user.sql
Executable file
@ -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;
|
||||
Reference in New Issue
Block a user