1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-30 10:36:35 +02:00

Server: Set a timestamp when disabling a user

So that it can be used later on to decide if user data
should be automatically deleted or not.
This commit is contained in:
Laurent Cozic 2022-01-31 19:52:30 +00:00
parent 46202beb9b
commit 2f7ab7e92e
7 changed files with 213 additions and 29 deletions

Binary file not shown.

View File

@ -0,0 +1,54 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
// It's assumed that the input user IDs are disabled.
// The disabled_time will be set to the first flag created_time
export const setUserAccountDisabledTimes = async (db: DbConnection, userIds: string[]) => {
// FailedPaymentFinal = 2,
// SubscriptionCancelled = 5,
// ManuallyDisabled = 6,
// UserDeletionInProgress = 7,
interface UserFlag {
user_id: string;
created_time: number;
}
const flags: UserFlag[] = await db('user_flags')
.select(['user_id', 'created_time'])
.whereIn('user_id', userIds)
.whereIn('type', [2, 5, 6, 7])
.orderBy('created_time', 'asc');
for (const userId of userIds) {
const flag = flags.find(f => f.user_id === userId);
if (!flag) {
console.warn(`Found a disabled account without an associated flag. Setting disabled timestamp to current time: ${userId}`);
}
await db('users')
.update({ disabled_time: flag ? flag.created_time : Date.now() })
.where('id', '=', userId);
}
};
export const disabledUserIds = async (db: DbConnection): Promise<string[]> => {
const users = await db('users').select(['id']).where('enabled', '=', 0);
return users.map(u => u.id);
};
export async function up(db: DbConnection): Promise<any> {
await db.schema.alterTable('users', (table: Knex.CreateTableBuilder) => {
table.bigInteger('disabled_time').defaultTo(0).notNullable();
});
const userIds = await disabledUserIds(db);
await setUserAccountDisabledTimes(db, userIds);
}
export async function down(db: DbConnection): Promise<any> {
await db.schema.alterTable('users', (table: Knex.CreateTableBuilder) => {
table.dropColumn('disabled_time');
});
}

View File

@ -0,0 +1,72 @@
import { UserFlagType } from '../../services/database/types';
import { beforeAllDb, afterAllTests, beforeEachDb, createUser, models, db } from '../../utils/testing/testUtils';
import { disabledUserIds, setUserAccountDisabledTimes } from '../20220131185922_account_disabled_timestamp';
describe('20220131185922_account_disabled_timestamp', function() {
beforeAll(async () => {
await beforeAllDb('20220131185922_account_disabled_timestamp');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
});
test('should set the user account disabled time', async function() {
const user1 = await createUser(1);
const user2 = await createUser(2);
const user3 = await createUser(3);
const user4 = await createUser(4);
jest.useFakeTimers('modern');
// -------------------------------------------------
// User 1
// -------------------------------------------------
const t0 = new Date('2021-12-14').getTime();
jest.setSystemTime(t0);
await models().userFlag().add(user1.id, UserFlagType.AccountOverLimit);
// -------------------------------------------------
// User 2
// -------------------------------------------------
const t1 = new Date('2021-12-15').getTime();
jest.setSystemTime(t1);
await models().userFlag().add(user2.id, UserFlagType.FailedPaymentFinal);
const t2 = new Date('2021-12-16').getTime();
jest.setSystemTime(t2);
await models().userFlag().add(user2.id, UserFlagType.ManuallyDisabled);
// -------------------------------------------------
// User 3
// -------------------------------------------------
const t3 = new Date('2021-12-17').getTime();
jest.setSystemTime(t3);
await models().userFlag().add(user3.id, UserFlagType.SubscriptionCancelled);
const userIds = await disabledUserIds(db());
expect(userIds.sort()).toEqual([user2.id, user3.id].sort());
await setUserAccountDisabledTimes(db(), userIds);
expect((await models().user().load(user1.id)).disabled_time).toBe(0);
expect((await models().user().load(user2.id)).disabled_time).toBe(t1);
expect((await models().user().load(user3.id)).disabled_time).toBe(t3);
expect((await models().user().load(user4.id)).disabled_time).toBe(0);
jest.useRealTimers();
});
});

View File

@ -39,4 +39,17 @@ describe('UserFlagModel', function() {
expect(flag.id).not.toBe(differentFlag.id);
});
test('should set the timestamp when disabling an account', async function() {
const { user } = await createUserAndSession(1);
const beforeTime = Date.now();
await models().userFlag().add(user.id, UserFlagType.FailedPaymentFinal);
expect((await models().user().load(user.id)).disabled_time).toBeGreaterThanOrEqual(beforeTime);
await models().userFlag().remove(user.id, UserFlagType.FailedPaymentFinal);
expect((await models().user().load(user.id)).disabled_time).toBe(0);
});
});

View File

@ -138,6 +138,10 @@ export default class UserFlagModels extends BaseModel<UserFlag> {
newProps.enabled = 0;
}
if (user.enabled !== newProps.enabled) {
newProps.disabled_time = !newProps.enabled ? Date.now() : 0;
}
if (user.can_upload !== newProps.can_upload || user.enabled !== newProps.enabled) {
await this.models().user().save({
id: userId,

View File

@ -186,20 +186,6 @@ export interface Change extends WithDates, WithUuid {
user_id?: Uuid;
}
export interface Email extends WithDates {
id?: number;
recipient_name?: string;
recipient_email?: string;
recipient_id?: Uuid;
sender_id?: EmailSender;
subject?: string;
body?: string;
sent_time?: number;
sent_success?: number;
error?: string;
key?: string;
}
export interface Token extends WithDates {
id?: number;
value?: string;
@ -233,6 +219,7 @@ export interface User extends WithDates, WithUuid {
max_total_item_size?: number | null;
total_item_size?: number;
enabled?: number;
disabled_time?: number;
}
export interface UserFlag extends WithDates {
@ -282,6 +269,40 @@ export interface UserDeletion extends WithDates {
error?: string;
}
export interface Organization {
id?: Uuid;
name?: string;
owner_id?: Uuid;
max_users?: number;
updated_time?: string;
created_time?: string;
}
export interface OrganizationUser {
id?: Uuid;
organization_id?: Uuid;
user_id?: Uuid;
invitation_email?: string;
invitation_status?: number;
is_admin?: number;
updated_time?: string;
created_time?: string;
}
export interface Email extends WithDates {
id?: number;
recipient_name?: string;
recipient_email?: string;
recipient_id?: Uuid;
sender_id?: EmailSender;
subject?: string;
body?: string;
sent_time?: number;
sent_success?: number;
error?: string;
key?: string;
}
export const databaseSchema: DatabaseTables = {
sessions: {
id: { type: 'string' },
@ -374,21 +395,6 @@ export const databaseSchema: DatabaseTables = {
previous_item: { type: 'string' },
user_id: { type: 'string' },
},
emails: {
id: { type: 'number' },
recipient_name: { type: 'string' },
recipient_email: { type: 'string' },
recipient_id: { type: 'string' },
sender_id: { type: 'number' },
subject: { type: 'string' },
body: { type: 'string' },
sent_time: { type: 'string' },
sent_success: { type: 'number' },
error: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
key: { type: 'string' },
},
tokens: {
id: { type: 'number' },
value: { type: 'string' },
@ -425,6 +431,7 @@ export const databaseSchema: DatabaseTables = {
max_total_item_size: { type: 'string' },
total_item_size: { type: 'string' },
enabled: { type: 'number' },
disabled_time: { type: 'string' },
},
user_flags: {
id: { type: 'number' },
@ -476,5 +483,38 @@ export const databaseSchema: DatabaseTables = {
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
organizations: {
id: { type: 'string' },
name: { type: 'string' },
owner_id: { type: 'string' },
max_users: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
organization_users: {
id: { type: 'string' },
organization_id: { type: 'string' },
user_id: { type: 'string' },
invitation_email: { type: 'string' },
invitation_status: { type: 'number' },
is_admin: { type: 'number' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
emails: {
id: { type: 'number' },
recipient_name: { type: 'string' },
recipient_email: { type: 'string' },
recipient_id: { type: 'string' },
sender_id: { type: 'number' },
subject: { type: 'string' },
body: { type: 'string' },
sent_time: { type: 'string' },
sent_success: { type: 'number' },
error: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
key: { type: 'string' },
},
};
// AUTO-GENERATED-TYPES

View File

@ -62,6 +62,7 @@ const propertyTypes: Record<string, string> = {
'user_deletions.start_time': 'number',
'user_deletions.end_time': 'number',
'user_deletions.scheduled_time': 'number',
'users.disabled_time': 'number',
};
function insertContentIntoFile(filePath: string, markerOpen: string, markerClose: string, contentToInsert: string): void {