mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Server: Add task to automate deletion of disabled accounts
This commit is contained in:
parent
68469bc1a5
commit
1afcb27601
@ -106,6 +106,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
|||||||
const supportEmail = env.SUPPORT_EMAIL;
|
const supportEmail = env.SUPPORT_EMAIL;
|
||||||
|
|
||||||
config_ = {
|
config_ = {
|
||||||
|
...env,
|
||||||
appVersion: packageJson.version,
|
appVersion: packageJson.version,
|
||||||
appName,
|
appName,
|
||||||
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com') || apiBaseUrl.includes('.joplincloud.local'),
|
isJoplinCloud: apiBaseUrl.includes('.joplincloud.com') || apiBaseUrl.includes('.joplincloud.local'),
|
||||||
|
@ -89,6 +89,13 @@ const defaultEnvValues: EnvVariables = {
|
|||||||
|
|
||||||
STRIPE_SECRET_KEY: '',
|
STRIPE_SECRET_KEY: '',
|
||||||
STRIPE_WEBHOOK_SECRET: '',
|
STRIPE_WEBHOOK_SECRET: '',
|
||||||
|
|
||||||
|
// ==================================================
|
||||||
|
// User data deletion
|
||||||
|
// ==================================================
|
||||||
|
|
||||||
|
USER_DATA_AUTO_DELETE_ENABLED: false,
|
||||||
|
USER_DATA_AUTO_DELETE_AFTER_DAYS: 90,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EnvVariables {
|
export interface EnvVariables {
|
||||||
@ -138,6 +145,9 @@ export interface EnvVariables {
|
|||||||
|
|
||||||
STRIPE_SECRET_KEY: string;
|
STRIPE_SECRET_KEY: string;
|
||||||
STRIPE_WEBHOOK_SECRET: string;
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
|
||||||
|
USER_DATA_AUTO_DELETE_ENABLED: boolean;
|
||||||
|
USER_DATA_AUTO_DELETE_AFTER_DAYS: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseBoolean = (s: string): boolean => {
|
const parseBoolean = (s: string): boolean => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { beforeAllDb, afterAllTests, beforeEachDb, models, createUser, expectThrow } from '../utils/testing/testUtils';
|
import { beforeAllDb, afterAllTests, beforeEachDb, models, createUser, expectThrow } from '../utils/testing/testUtils';
|
||||||
|
import { Day } from '../utils/time';
|
||||||
|
|
||||||
describe('UserDeletionModel', function() {
|
describe('UserDeletionModel', function() {
|
||||||
|
|
||||||
@ -143,4 +144,39 @@ describe('UserDeletionModel', function() {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should auto-add users for deletion', async function() {
|
||||||
|
jest.useFakeTimers('modern');
|
||||||
|
|
||||||
|
const t0 = new Date('2022-02-22').getTime();
|
||||||
|
jest.setSystemTime(t0);
|
||||||
|
|
||||||
|
await createUser(1);
|
||||||
|
const user2 = await createUser(2);
|
||||||
|
|
||||||
|
await models().user().save({
|
||||||
|
id: user2.id,
|
||||||
|
enabled: 0,
|
||||||
|
disabled_time: t0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await models().userDeletion().autoAdd(10, 90 * Day, 3 * Day);
|
||||||
|
|
||||||
|
expect(await models().userDeletion().count()).toBe(0);
|
||||||
|
|
||||||
|
const t1 = new Date('2022-05-30').getTime();
|
||||||
|
jest.setSystemTime(t1);
|
||||||
|
|
||||||
|
await models().userDeletion().autoAdd(10, 90 * Day, 3 * Day);
|
||||||
|
|
||||||
|
expect(await models().userDeletion().count()).toBe(1);
|
||||||
|
const d = (await models().userDeletion().all())[0];
|
||||||
|
expect(d.user_id).toBe(user2.id);
|
||||||
|
|
||||||
|
// Shouldn't add it again if running autoAdd() again
|
||||||
|
await models().userDeletion().autoAdd(10, 90 * Day, 3 * Day);
|
||||||
|
expect(await models().userDeletion().count()).toBe(1);
|
||||||
|
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { UserDeletion, Uuid } from '../services/database/types';
|
import { User, UserDeletion, Uuid } from '../services/database/types';
|
||||||
import { errorToString } from '../utils/errors';
|
import { errorToString } from '../utils/errors';
|
||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
|
|
||||||
@ -7,6 +7,14 @@ export interface AddOptions {
|
|||||||
processAccount?: boolean;
|
processAccount?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultAddOptions = () => {
|
||||||
|
const d: AddOptions = {
|
||||||
|
processAccount: true,
|
||||||
|
processData: true,
|
||||||
|
};
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
export default class UserDeletionModel extends BaseModel<UserDeletion> {
|
export default class UserDeletionModel extends BaseModel<UserDeletion> {
|
||||||
|
|
||||||
protected get tableName(): string {
|
protected get tableName(): string {
|
||||||
@ -28,8 +36,7 @@ export default class UserDeletionModel extends BaseModel<UserDeletion> {
|
|||||||
|
|
||||||
public async add(userId: Uuid, scheduledTime: number, options: AddOptions = null): Promise<UserDeletion> {
|
public async add(userId: Uuid, scheduledTime: number, options: AddOptions = null): Promise<UserDeletion> {
|
||||||
options = {
|
options = {
|
||||||
processAccount: true,
|
...defaultAddOptions(),
|
||||||
processData: true,
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -91,4 +98,26 @@ export default class UserDeletionModel extends BaseModel<UserDeletion> {
|
|||||||
.where('id', deletionId);
|
.where('id', deletionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async autoAdd(maxAutoAddedAccounts: number, ttl: number, scheduledTime: number, options: AddOptions = null): Promise<Uuid[]> {
|
||||||
|
const cutOffTime = Date.now() - ttl;
|
||||||
|
|
||||||
|
const disabledUsers: User[] = await this.db('users')
|
||||||
|
.select(['users.id'])
|
||||||
|
.leftJoin('user_deletions', 'users.id', 'user_deletions.user_id')
|
||||||
|
.where('users.enabled', '=', 0)
|
||||||
|
.where('users.disabled_time', '<', cutOffTime)
|
||||||
|
.whereNull('user_deletions.user_id') // Only add users not already in the user_deletions table
|
||||||
|
.limit(maxAutoAddedAccounts);
|
||||||
|
|
||||||
|
const userIds = disabledUsers.map(d => d.id);
|
||||||
|
|
||||||
|
await this.withTransaction(async () => {
|
||||||
|
for (const userId of userIds) {
|
||||||
|
await this.add(userId, scheduledTime, options);
|
||||||
|
}
|
||||||
|
}, 'UserDeletionModel::autoAdd');
|
||||||
|
|
||||||
|
return userIds;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { Config, Env } from '../utils/types';
|
|||||||
import BaseService from './BaseService';
|
import BaseService from './BaseService';
|
||||||
import { Event, EventType } from './database/types';
|
import { Event, EventType } from './database/types';
|
||||||
import { Services } from './types';
|
import { Services } from './types';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
const cron = require('node-cron');
|
const cron = require('node-cron');
|
||||||
|
|
||||||
const logger = Logger.create('TaskService');
|
const logger = Logger.create('TaskService');
|
||||||
@ -17,6 +18,7 @@ export enum TaskId {
|
|||||||
DeleteExpiredSessions = 6,
|
DeleteExpiredSessions = 6,
|
||||||
CompressOldChanges = 7,
|
CompressOldChanges = 7,
|
||||||
ProcessUserDeletions = 8,
|
ProcessUserDeletions = 8,
|
||||||
|
AutoAddDisabledAccountsForDeletion = 9,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RunType {
|
export enum RunType {
|
||||||
@ -24,6 +26,25 @@ export enum RunType {
|
|||||||
Manual = 2,
|
Manual = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const taskIdToLabel = (taskId: TaskId): string => {
|
||||||
|
const strings: Record<TaskId, string> = {
|
||||||
|
[TaskId.DeleteExpiredTokens]: _('Delete expired tokens'),
|
||||||
|
[TaskId.UpdateTotalSizes]: _('Update total sizes'),
|
||||||
|
[TaskId.HandleOversizedAccounts]: _('Process oversized accounts'),
|
||||||
|
[TaskId.HandleBetaUserEmails]: 'Process beta user emails',
|
||||||
|
[TaskId.HandleFailedPaymentSubscriptions]: _('Process failed payment subscriptions'),
|
||||||
|
[TaskId.DeleteExpiredSessions]: _('Delete expired sessions'),
|
||||||
|
[TaskId.CompressOldChanges]: _('Compress old changes'),
|
||||||
|
[TaskId.ProcessUserDeletions]: _('Process user deletions'),
|
||||||
|
[TaskId.AutoAddDisabledAccountsForDeletion]: _('Auto-add disabled accounts for deletion'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const s = strings[taskId];
|
||||||
|
if (!s) throw new Error(`No such task: ${taskId}`);
|
||||||
|
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
const runTypeToString = (runType: RunType) => {
|
const runTypeToString = (runType: RunType) => {
|
||||||
if (runType === RunType.Scheduled) return 'scheduled';
|
if (runType === RunType.Scheduled) return 'scheduled';
|
||||||
if (runType === RunType.Manual) return 'manual';
|
if (runType === RunType.Manual) return 'manual';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Logger from '@joplin/lib/Logger';
|
import Logger from '@joplin/lib/Logger';
|
||||||
import { Pagination } from '../models/utils/pagination';
|
import { Pagination } from '../models/utils/pagination';
|
||||||
import { msleep } from '../utils/time';
|
import { Day, msleep } from '../utils/time';
|
||||||
import BaseService from './BaseService';
|
import BaseService from './BaseService';
|
||||||
import { UserDeletion, UserFlagType, Uuid } from './database/types';
|
import { BackupItemType, UserDeletion, UserFlagType, Uuid } from './database/types';
|
||||||
|
|
||||||
const logger = Logger.create('UserDeletionService');
|
const logger = Logger.create('UserDeletionService');
|
||||||
|
|
||||||
@ -59,6 +59,21 @@ export default class UserDeletionService extends BaseService {
|
|||||||
private async deleteUserAccount(userId: Uuid, _options: DeletionJobOptions = null) {
|
private async deleteUserAccount(userId: Uuid, _options: DeletionJobOptions = null) {
|
||||||
logger.info(`Deleting user account: ${userId}`);
|
logger.info(`Deleting user account: ${userId}`);
|
||||||
|
|
||||||
|
const user = await this.models.user().load(userId);
|
||||||
|
if (!user) throw new Error(`No such user: ${userId}`);
|
||||||
|
|
||||||
|
const flags = await this.models.userFlag().allByUserId(userId);
|
||||||
|
|
||||||
|
await this.models.backupItem().add(
|
||||||
|
BackupItemType.UserAccount,
|
||||||
|
user.email,
|
||||||
|
JSON.stringify({
|
||||||
|
user,
|
||||||
|
flags,
|
||||||
|
}),
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
await this.models.userFlag().add(userId, UserFlagType.UserDeletionInProgress);
|
await this.models.userFlag().add(userId, UserFlagType.UserDeletionInProgress);
|
||||||
|
|
||||||
await this.models.session().deleteByUserId(userId);
|
await this.models.session().deleteByUserId(userId);
|
||||||
@ -93,6 +108,24 @@ export default class UserDeletionService extends BaseService {
|
|||||||
logger.info('Completed user deletion: ', deletion.id);
|
logger.info('Completed user deletion: ', deletion.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async autoAddForDeletion() {
|
||||||
|
const addedUserIds = await this.models.userDeletion().autoAdd(
|
||||||
|
10,
|
||||||
|
this.config.USER_DATA_AUTO_DELETE_AFTER_DAYS * Day,
|
||||||
|
3 * Day,
|
||||||
|
{
|
||||||
|
processAccount: true,
|
||||||
|
processData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addedUserIds.length) {
|
||||||
|
logger.info(`autoAddForDeletion: Queued ${addedUserIds.length} users for deletions: ${addedUserIds.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
logger.info('autoAddForDeletion: No users were queued for deletion');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async processNextDeletionJob() {
|
public async processNextDeletionJob() {
|
||||||
const deletion = await this.models.userDeletion().next();
|
const deletion = await this.models.userDeletion().next();
|
||||||
if (!deletion) return;
|
if (!deletion) return;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Models } from '../models/factory';
|
import { Models } from '../models/factory';
|
||||||
import TaskService, { Task, TaskId } from '../services/TaskService';
|
import TaskService, { Task, TaskId, taskIdToLabel } from '../services/TaskService';
|
||||||
import { Services } from '../services/types';
|
import { Services } from '../services/types';
|
||||||
import { Config, Env } from './types';
|
import { Config, Env } from './types';
|
||||||
|
|
||||||
@ -9,28 +9,28 @@ export default function(env: Env, models: Models, config: Config, services: Serv
|
|||||||
let tasks: Task[] = [
|
let tasks: Task[] = [
|
||||||
{
|
{
|
||||||
id: TaskId.DeleteExpiredTokens,
|
id: TaskId.DeleteExpiredTokens,
|
||||||
description: 'Delete expired tokens',
|
description: taskIdToLabel(TaskId.DeleteExpiredTokens),
|
||||||
schedule: '0 */6 * * *',
|
schedule: '0 */6 * * *',
|
||||||
run: (models: Models) => models.token().deleteExpiredTokens(),
|
run: (models: Models) => models.token().deleteExpiredTokens(),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: TaskId.UpdateTotalSizes,
|
id: TaskId.UpdateTotalSizes,
|
||||||
description: 'Update total sizes',
|
description: taskIdToLabel(TaskId.UpdateTotalSizes),
|
||||||
schedule: '0 * * * *',
|
schedule: '0 * * * *',
|
||||||
run: (models: Models) => models.item().updateTotalSizes(),
|
run: (models: Models) => models.item().updateTotalSizes(),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: TaskId.CompressOldChanges,
|
id: TaskId.CompressOldChanges,
|
||||||
description: 'Compress old changes',
|
description: taskIdToLabel(TaskId.CompressOldChanges),
|
||||||
schedule: '0 0 */2 * *',
|
schedule: '0 0 */2 * *',
|
||||||
run: (models: Models) => models.change().compressOldChanges(),
|
run: (models: Models) => models.change().compressOldChanges(),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
id: TaskId.ProcessUserDeletions,
|
id: TaskId.ProcessUserDeletions,
|
||||||
description: 'Process user deletions',
|
description: taskIdToLabel(TaskId.ProcessUserDeletions),
|
||||||
schedule: '0 */6 * * *',
|
schedule: '0 */6 * * *',
|
||||||
run: (_models: Models, services: Services) => services.userDeletion.runMaintenance(),
|
run: (_models: Models, services: Services) => services.userDeletion.runMaintenance(),
|
||||||
},
|
},
|
||||||
@ -41,30 +41,39 @@ export default function(env: Env, models: Models, config: Config, services: Serv
|
|||||||
// the UpdateTotalSizes task being run.
|
// the UpdateTotalSizes task being run.
|
||||||
{
|
{
|
||||||
id: TaskId.HandleOversizedAccounts,
|
id: TaskId.HandleOversizedAccounts,
|
||||||
description: 'Process oversized accounts',
|
description: taskIdToLabel(TaskId.HandleOversizedAccounts),
|
||||||
schedule: '30 */2 * * *',
|
schedule: '30 */2 * * *',
|
||||||
run: (models: Models) => models.user().handleOversizedAccounts(),
|
run: (models: Models) => models.user().handleOversizedAccounts(),
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// id: TaskId.DeleteExpiredSessions,
|
// id: TaskId.DeleteExpiredSessions,
|
||||||
// description: 'Delete expired sessions',
|
// description: taskIdToLabel(TaskId.DeleteExpiredSessions),
|
||||||
// schedule: '0 */6 * * *',
|
// schedule: '0 */6 * * *',
|
||||||
// run: (models: Models) => models.session().deleteExpiredSessions(),
|
// run: (models: Models) => models.session().deleteExpiredSessions(),
|
||||||
// },
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (config.USER_DATA_AUTO_DELETE_ENABLED) {
|
||||||
|
tasks.push({
|
||||||
|
id: TaskId.AutoAddDisabledAccountsForDeletion,
|
||||||
|
description: taskIdToLabel(TaskId.AutoAddDisabledAccountsForDeletion),
|
||||||
|
schedule: '0 14 * * *',
|
||||||
|
run: (_models: Models, services: Services) => services.userDeletion.autoAddForDeletion(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (config.isJoplinCloud) {
|
if (config.isJoplinCloud) {
|
||||||
tasks = tasks.concat([
|
tasks = tasks.concat([
|
||||||
{
|
{
|
||||||
id: TaskId.HandleBetaUserEmails,
|
id: TaskId.HandleBetaUserEmails,
|
||||||
description: 'Process beta user emails',
|
description: taskIdToLabel(TaskId.HandleBetaUserEmails),
|
||||||
schedule: '0 12 * * *',
|
schedule: '0 12 * * *',
|
||||||
run: (models: Models) => models.user().handleBetaUserEmails(),
|
run: (models: Models) => models.user().handleBetaUserEmails(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: TaskId.HandleFailedPaymentSubscriptions,
|
id: TaskId.HandleFailedPaymentSubscriptions,
|
||||||
description: 'Process failed payment subscriptions',
|
description: taskIdToLabel(TaskId.HandleFailedPaymentSubscriptions),
|
||||||
schedule: '0 13 * * *',
|
schedule: '0 13 * * *',
|
||||||
run: (models: Models) => models.user().handleFailedPaymentSubscriptions(),
|
run: (models: Models) => models.user().handleFailedPaymentSubscriptions(),
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,7 @@ import { Account } from '../models/UserModel';
|
|||||||
import { Services } from '../services/types';
|
import { Services } from '../services/types';
|
||||||
import { Routers } from './routeUtils';
|
import { Routers } from './routeUtils';
|
||||||
import { DbConnection } from '../db';
|
import { DbConnection } from '../db';
|
||||||
import { MailerSecurity } from '../env';
|
import { EnvVariables, MailerSecurity } from '../env';
|
||||||
|
|
||||||
export enum Env {
|
export enum Env {
|
||||||
Dev = 'dev',
|
Dev = 'dev',
|
||||||
@ -130,7 +130,7 @@ export interface StorageDriverConfig {
|
|||||||
bucket?: string;
|
bucket?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Config {
|
export interface Config extends EnvVariables {
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
appName: string;
|
appName: string;
|
||||||
env: Env;
|
env: Env;
|
||||||
|
Loading…
Reference in New Issue
Block a user