1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-03-29 21:21:15 +02:00

Server: Temporarily save user info before deleting account

This commit is contained in:
Laurent Cozic 2022-02-01 15:39:25 +00:00
parent a4e279270b
commit 68469bc1a5
8 changed files with 101 additions and 1 deletions

Binary file not shown.

View File

@ -0,0 +1,17 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
await db.schema.createTable('backup_items', (table: Knex.CreateTableBuilder) => {
table.increments('id').unique().primary().notNullable();
table.integer('type').notNullable();
table.text('key', 'mediumtext').notNullable();
table.string('user_id', 32).defaultTo('').notNullable();
table.binary('content').notNullable();
table.bigInteger('created_time').notNullable();
});
}
export async function down(db: DbConnection): Promise<any> {
await db.schema.dropTable('backup_items');
}

View File

@ -0,0 +1,29 @@
import { BackupItem, BackupItemType } from '../services/database/types';
import BaseModel from './BaseModel';
export default class BackupItemModel extends BaseModel<BackupItem> {
protected get tableName(): string {
return 'backup_items';
}
protected hasUuid(): boolean {
return false;
}
protected hasUpdatedTime(): boolean {
return false;
}
public async add(type: BackupItemType, key: string, content: any, userId: string = ''): Promise<BackupItem> {
const item: BackupItem = {
user_id: userId,
key,
type,
content,
};
return this.save(item);
}
}

View File

@ -166,6 +166,10 @@ export default abstract class BaseModel<T> {
return true;
}
protected hasUpdatedTime(): boolean {
return this.autoTimestampEnabled();
}
protected get hasParentId(): boolean {
return false;
}
@ -315,7 +319,7 @@ export default abstract class BaseModel<T> {
if (isNew) {
(toSave as WithDates).created_time = timestamp;
}
(toSave as WithDates).updated_time = timestamp;
if (this.hasUpdatedTime()) (toSave as WithDates).updated_time = timestamp;
}
if (options.skipValidation !== true) object = await this.validate(object, { isNew: isNew, rules: options.validationRules ? options.validationRules : {} });

View File

@ -75,6 +75,7 @@ import { Config } from '../utils/types';
import LockModel from './LockModel';
import StorageModel from './StorageModel';
import UserDeletionModel from './UserDeletionModel';
import BackupItemModel from './BackupItemModel';
export type NewModelFactoryHandler = (db: DbConnection)=> Models;
@ -170,6 +171,10 @@ export class Models {
return new UserDeletionModel(this.db_, this.newModelFactory, this.config_);
}
public backupItem() {
return new BackupItemModel(this.db_, this.newModelFactory, this.config_);
}
}
export default function newModelFactory(db: DbConnection, config: Config): Models {

View File

@ -2,6 +2,7 @@ import config from '../config';
import { shareFolderWithUser } from '../utils/testing/shareApiUtils';
import { afterAllTests, beforeAllDb, beforeEachDb, createNote, createUserAndSession, models } from '../utils/testing/testUtils';
import { Env } from '../utils/types';
import { BackupItemType } from './database/types';
import UserDeletionService from './UserDeletionService';
const newService = () => {
@ -70,6 +71,8 @@ describe('UserDeletionService', function() {
expect(await models().user().count()).toBe(2);
expect(await models().session().count()).toBe(2);
const beforeTime = Date.now();
const service = newService();
await service.processDeletionJob(job, { sleepBetweenOperations: 0 });
@ -78,6 +81,18 @@ describe('UserDeletionService', function() {
const user = (await models().user().all())[0];
expect(user.id).toBe(user2.id);
const backupItems = await models().backupItem().all();
expect(backupItems.length).toBe(1);
const backupItem = backupItems[0];
expect(backupItem.key).toBe(user1.email);
expect(backupItem.type).toBe(BackupItemType.UserAccount);
expect(backupItem.created_time).toBeGreaterThanOrEqual(beforeTime);
const content = JSON.parse(backupItem.content.toString());
expect(content.user.id).toBe(user1.id);
expect(content.user.email).toBe(user1.email);
expect(content.flags.length).toBe(0);
});
test('should not delete notebooks that are not owned', async function() {

View File

@ -33,6 +33,10 @@ export enum EventType {
TaskCompleted = 2,
}
export enum BackupItemType {
UserAccount = 1,
}
export enum UserFlagType {
FailedPaymentWarning = 1,
FailedPaymentFinal = 2,
@ -87,6 +91,10 @@ export interface WithDates {
created_time?: number;
}
export interface WithCreatedDate {
created_time?: number;
}
export interface WithUuid {
id?: Uuid;
}
@ -283,6 +291,14 @@ export interface Email extends WithDates {
key?: string;
}
export interface BackupItem extends WithCreatedDate {
id?: number;
type?: number;
key?: string;
user_id?: Uuid;
content?: Buffer;
}
export const databaseSchema: DatabaseTables = {
sessions: {
id: { type: 'string' },
@ -478,5 +494,13 @@ export const databaseSchema: DatabaseTables = {
created_time: { type: 'string' },
key: { type: 'string' },
},
backup_items: {
id: { type: 'number' },
type: { type: 'number' },
key: { type: 'string' },
user_id: { type: 'string' },
content: { type: 'any' },
created_time: { type: 'string' },
},
};
// AUTO-GENERATED-TYPES

View File

@ -36,6 +36,7 @@ const config = {
'main.users': 'WithDates, WithUuid',
'main.events': 'WithUuid',
'main.user_deletions': 'WithDates',
'main.backup_items': 'WithCreatedDate',
},
};
@ -63,6 +64,7 @@ const propertyTypes: Record<string, string> = {
'user_deletions.end_time': 'number',
'user_deletions.scheduled_time': 'number',
'users.disabled_time': 'number',
'backup_items.content': 'Buffer',
};
function insertContentIntoFile(filePath: string, markerOpen: string, markerClose: string, contentToInsert: string): void {
@ -93,6 +95,10 @@ function createTypeString(table: any) {
if (['created_time', 'updated_time'].includes(name)) continue;
}
if (table.extends && table.extends.indexOf('WithCreatedDate') >= 0) {
if (['created_time'].includes(name)) continue;
}
if (table.extends && table.extends.indexOf('WithUuid') >= 0) {
if (['id'].includes(name)) continue;
}