mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-30 10:36:35 +02:00
Server: Add support for user flags
This commit is contained in:
parent
2ae51acd29
commit
82b157b491
Binary file not shown.
74
packages/server/src/db.test.ts
Normal file
74
packages/server/src/db.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
it('should pass', async function() {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
// import { afterAllTests, beforeAllDb, beforeEachDb, db } from "./utils/testing/testUtils";
|
||||
// import sqlts from '@rmp135/sql-ts';
|
||||
// import config from "./config";
|
||||
// import { connectDb, DbConnection, disconnectDb, migrateDown, migrateList, migrateUp, nextMigration } from "./db";
|
||||
|
||||
// async function dbSchemaSnapshot(db:DbConnection):Promise<any> {
|
||||
// return sqlts.toObject({
|
||||
// client: 'sqlite',
|
||||
// knex: db,
|
||||
// // 'connection': {
|
||||
// // 'filename': config().database.name,
|
||||
// // },
|
||||
// useNullAsDefault: true,
|
||||
// } as any)
|
||||
|
||||
// // return JSON.stringify(definitions);
|
||||
// }
|
||||
|
||||
// describe('db', function() {
|
||||
|
||||
// beforeAll(async () => {
|
||||
// await beforeAllDb('db', { autoMigrate: false });
|
||||
// });
|
||||
|
||||
// afterAll(async () => {
|
||||
// await afterAllTests();
|
||||
// });
|
||||
|
||||
// beforeEach(async () => {
|
||||
// await beforeEachDb();
|
||||
// });
|
||||
|
||||
// it('should allow downgrading schema', async function() {
|
||||
// const ignoreAllBefore = '20210819165350_user_flags';
|
||||
// let startProcessing = false;
|
||||
|
||||
// //console.info(await dbSchemaSnapshot());
|
||||
|
||||
// while (true) {
|
||||
// await migrateUp(db());
|
||||
|
||||
// if (!startProcessing) {
|
||||
// const next = await nextMigration(db());
|
||||
// if (next === ignoreAllBefore) {
|
||||
// startProcessing = true;
|
||||
// } else {
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!(await nextMigration(db()))) break;
|
||||
|
||||
// // await disconnectDb(db());
|
||||
// // const beforeSchema = await dbSchemaSnapshot(db());
|
||||
// // console.info(beforeSchema);
|
||||
// // await connectDb(db());
|
||||
|
||||
// // await migrateUp(db());
|
||||
// // await migrateDown(db());
|
||||
|
||||
// // const afterSchema = await dbSchemaSnapshot(db());
|
||||
|
||||
// // // console.info(beforeSchema);
|
||||
// // // console.info(afterSchema);
|
||||
|
||||
// // expect(beforeSchema).toEqual(afterSchema);
|
||||
// }
|
||||
// });
|
||||
|
||||
// });
|
@ -52,6 +52,11 @@ export interface ConnectionCheckResult {
|
||||
connection: DbConnection;
|
||||
}
|
||||
|
||||
export interface Migration {
|
||||
name: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export function makeKnexConfig(dbConfig: DatabaseConfig): KnexDatabaseConfig {
|
||||
const connection: DbConfigConnection = {};
|
||||
|
||||
@ -167,8 +172,6 @@ export async function migrateList(db: DbConnection, asString: boolean = true) {
|
||||
// ]
|
||||
// ]
|
||||
|
||||
if (!asString) return migrations;
|
||||
|
||||
const formatName = (migrationInfo: any) => {
|
||||
const name = migrationInfo.file ? migrationInfo.file : migrationInfo;
|
||||
|
||||
@ -177,32 +180,43 @@ export async function migrateList(db: DbConnection, asString: boolean = true) {
|
||||
return s.join('.');
|
||||
};
|
||||
|
||||
interface Line {
|
||||
text: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
const output: Line[] = [];
|
||||
const output: Migration[] = [];
|
||||
|
||||
for (const s of migrations[0]) {
|
||||
output.push({
|
||||
text: formatName(s),
|
||||
name: formatName(s),
|
||||
done: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const s of migrations[1]) {
|
||||
output.push({
|
||||
text: formatName(s),
|
||||
name: formatName(s),
|
||||
done: false,
|
||||
});
|
||||
}
|
||||
|
||||
output.sort((a, b) => {
|
||||
return a.text < b.text ? -1 : +1;
|
||||
return a.name < b.name ? -1 : +1;
|
||||
});
|
||||
|
||||
return output.map(l => `${l.done ? '✓' : '✗'} ${l.text}`).join('\n');
|
||||
if (!asString) return output;
|
||||
|
||||
return output.map(l => `${l.done ? '✓' : '✗'} ${l.name}`).join('\n');
|
||||
}
|
||||
|
||||
export async function nextMigration(db: DbConnection): Promise<string> {
|
||||
const list = await migrateList(db, false) as Migration[];
|
||||
|
||||
let nextMigration: Migration = null;
|
||||
|
||||
while (list.length) {
|
||||
const migration = list.pop();
|
||||
if (migration.done) return nextMigration ? nextMigration.name : '';
|
||||
nextMigration = migration;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function allTableNames(): string[] {
|
||||
@ -321,6 +335,15 @@ export enum ChangeType {
|
||||
Delete = 3,
|
||||
}
|
||||
|
||||
export enum UserFlagType {
|
||||
FailedPaymentWarning = 1,
|
||||
FailedPaymentFinal = 2,
|
||||
AccountOverLimit = 3,
|
||||
AccountWithoutSubscription = 4,
|
||||
SubscriptionCancelled = 5,
|
||||
ManuallyDisabled = 6,
|
||||
}
|
||||
|
||||
export enum FileContentType {
|
||||
Any = 1,
|
||||
JoplinItem = 2,
|
||||
@ -508,6 +531,12 @@ export interface User extends WithDates, WithUuid {
|
||||
enabled?: number;
|
||||
}
|
||||
|
||||
export interface UserFlag extends WithDates {
|
||||
id?: number;
|
||||
user_id?: Uuid;
|
||||
type?: UserFlagType;
|
||||
}
|
||||
|
||||
export const databaseSchema: DatabaseTables = {
|
||||
sessions: {
|
||||
id: { type: 'string' },
|
||||
@ -665,5 +694,12 @@ export const databaseSchema: DatabaseTables = {
|
||||
total_item_size: { type: 'string' },
|
||||
enabled: { type: 'number' },
|
||||
},
|
||||
user_flags: {
|
||||
id: { type: 'number' },
|
||||
user_id: { type: 'string' },
|
||||
type: { type: 'number' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
};
|
||||
// AUTO-GENERATED-TYPES
|
||||
|
20
packages/server/src/migrations/20210819165350_user_flags.ts
Normal file
20
packages/server/src/migrations/20210819165350_user_flags.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.createTable('user_flags', (table: Knex.CreateTableBuilder) => {
|
||||
table.increments('id').unique().primary().notNullable();
|
||||
table.string('user_id', 32).notNullable();
|
||||
table.integer('type').defaultTo(0).notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.alterTable('user_flags', (table: Knex.CreateTableBuilder) => {
|
||||
table.unique(['user_id', 'type']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.dropTable('user_flags');
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { EmailSender, Subscription, User, Uuid } from '../db';
|
||||
import { EmailSender, Subscription, User, UserFlagType, Uuid } from '../db';
|
||||
import { ErrorNotFound } from '../utils/errors';
|
||||
import { Day } from '../utils/time';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
@ -81,13 +81,10 @@ export default class SubscriptionModel extends BaseModel<Subscription> {
|
||||
const user = await this.models().user().load(sub.user_id);
|
||||
|
||||
await this.withTransaction(async () => {
|
||||
if (!user.enabled || !user.can_upload) {
|
||||
await this.models().user().save({
|
||||
id: sub.user_id,
|
||||
enabled: 1,
|
||||
can_upload: 1,
|
||||
});
|
||||
}
|
||||
await this.models().userFlag().removeMulti(user.id, [
|
||||
UserFlagType.FailedPaymentWarning,
|
||||
UserFlagType.FailedPaymentFinal,
|
||||
]);
|
||||
|
||||
await this.save({
|
||||
id: sub.id,
|
||||
|
42
packages/server/src/models/UserFlagModel.test.ts
Normal file
42
packages/server/src/models/UserFlagModel.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { UserFlagType } from '../db';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models, createUserAndSession } from '../utils/testing/testUtils';
|
||||
|
||||
describe('UserFlagModel', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('UserFlagModel');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should create no more than one flag per type', async function() {
|
||||
const { user } = await createUserAndSession(1);
|
||||
|
||||
const beforeTime = Date.now();
|
||||
await models().userFlag().add(user.id, UserFlagType.AccountOverLimit);
|
||||
const flag = await models().userFlag().byUserId(user.id, UserFlagType.AccountOverLimit);
|
||||
|
||||
expect(flag.user_id).toBe(user.id);
|
||||
expect(flag.type).toBe(UserFlagType.AccountOverLimit);
|
||||
expect(flag.created_time).toBeGreaterThanOrEqual(beforeTime);
|
||||
expect(flag.updated_time).toBeGreaterThanOrEqual(beforeTime);
|
||||
|
||||
const flagCountBefore = (await models().userFlag().all()).length;
|
||||
await models().userFlag().add(user.id, UserFlagType.AccountOverLimit);
|
||||
const flagCountAfter = (await models().userFlag().all()).length;
|
||||
expect(flagCountBefore).toBe(flagCountAfter);
|
||||
|
||||
await models().userFlag().add(user.id, UserFlagType.FailedPaymentFinal);
|
||||
const flagCountAfter2 = (await models().userFlag().all()).length;
|
||||
expect(flagCountAfter2).toBe(flagCountBefore + 1);
|
||||
const differentFlag = await models().userFlag().byUserId(user.id, UserFlagType.FailedPaymentFinal);
|
||||
expect(flag.id).not.toBe(differentFlag.id);
|
||||
});
|
||||
|
||||
});
|
130
packages/server/src/models/UserFlagModel.ts
Normal file
130
packages/server/src/models/UserFlagModel.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { isUniqueConstraintError, User, UserFlag, UserFlagType, Uuid } from '../db';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
interface AddRemoveOptions {
|
||||
updateUser?: boolean;
|
||||
}
|
||||
|
||||
function defaultAddRemoveOptions(): AddRemoveOptions {
|
||||
return {
|
||||
updateUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
export default class UserFlagModels extends BaseModel<UserFlag> {
|
||||
|
||||
public get tableName(): string {
|
||||
return 'user_flags';
|
||||
}
|
||||
|
||||
protected hasUuid(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async add(userId: Uuid, type: UserFlagType, options: AddRemoveOptions = {}): Promise<void> {
|
||||
options = {
|
||||
...defaultAddRemoveOptions(),
|
||||
...options,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.save({
|
||||
user_id: userId,
|
||||
type,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isUniqueConstraintError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.updateUser) await this.updateUserFromFlags(userId);
|
||||
}
|
||||
|
||||
public async remove(userId: Uuid, type: UserFlagType, options: AddRemoveOptions = null) {
|
||||
options = {
|
||||
...defaultAddRemoveOptions(),
|
||||
...options,
|
||||
};
|
||||
|
||||
await this.db(this.tableName)
|
||||
.where('user_id', '=', userId)
|
||||
.where('type', '=', type)
|
||||
.delete();
|
||||
|
||||
if (options.updateUser) await this.updateUserFromFlags(userId);
|
||||
}
|
||||
|
||||
public async toggle(userId: Uuid, type: UserFlagType, apply: boolean, options: AddRemoveOptions = null) {
|
||||
if (apply) {
|
||||
await this.add(userId, type, options);
|
||||
} else {
|
||||
await this.remove(userId, type, options);
|
||||
}
|
||||
}
|
||||
|
||||
public async addMulti(userId: Uuid, flagTypes: UserFlagType[]) {
|
||||
await this.withTransaction(async () => {
|
||||
for (const flagType of flagTypes) {
|
||||
await this.add(userId, flagType, { updateUser: false });
|
||||
}
|
||||
await this.updateUserFromFlags(userId);
|
||||
});
|
||||
}
|
||||
|
||||
public async removeMulti(userId: Uuid, flagTypes: UserFlagType[]) {
|
||||
await this.withTransaction(async () => {
|
||||
for (const flagType of flagTypes) {
|
||||
await this.remove(userId, flagType, { updateUser: false });
|
||||
}
|
||||
await this.updateUserFromFlags(userId);
|
||||
});
|
||||
}
|
||||
|
||||
// As a general rule the `enabled` and `can_upload` properties should not
|
||||
// be set directly (except maybe in tests) - instead the appropriate user
|
||||
// flags should be set, and this function will derive the enabled/can_upload
|
||||
// properties from them.
|
||||
private async updateUserFromFlags(userId: Uuid) {
|
||||
const flags = await this.allByUserId(userId);
|
||||
const user = await this.models().user().load(userId, { fields: ['id', 'can_upload', 'enabled'] });
|
||||
|
||||
const newProps: User = {
|
||||
can_upload: 1,
|
||||
enabled: 1,
|
||||
};
|
||||
|
||||
if (flags.find(f => f.type === UserFlagType.AccountWithoutSubscription)) {
|
||||
newProps.can_upload = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.AccountOverLimit)) {
|
||||
newProps.can_upload = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.FailedPaymentWarning)) {
|
||||
newProps.can_upload = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.FailedPaymentFinal)) {
|
||||
newProps.enabled = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.SubscriptionCancelled)) {
|
||||
newProps.enabled = 0;
|
||||
} else if (flags.find(f => f.type === UserFlagType.ManuallyDisabled)) {
|
||||
newProps.enabled = 0;
|
||||
}
|
||||
|
||||
if (user.can_upload !== newProps.can_upload || user.enabled !== newProps.enabled) {
|
||||
await this.models().user().save({
|
||||
id: userId,
|
||||
...newProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async byUserId(userId: Uuid, type: UserFlagType): Promise<UserFlag> {
|
||||
return this.db(this.tableName)
|
||||
.where('user_id', '=', userId)
|
||||
.where('type', '=', type)
|
||||
.first();
|
||||
}
|
||||
|
||||
public async allByUserId(userId: Uuid): Promise<UserFlag[]> {
|
||||
return this.db(this.tableName).where('user_id', '=', userId);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, checkThrowAsync, createItem } from '../utils/testing/testUtils';
|
||||
import { EmailSender, User } from '../db';
|
||||
import { EmailSender, User, UserFlagType } from '../db';
|
||||
import { ErrorUnprocessableEntity } from '../utils/errors';
|
||||
import { betaUserDateRange, stripeConfig } from '../utils/stripe';
|
||||
import { AccountType } from './UserModel';
|
||||
@ -160,6 +160,9 @@ describe('UserModel', function() {
|
||||
|
||||
const reloadedUser = await models().user().load(user1.id);
|
||||
expect(reloadedUser.can_upload).toBe(0);
|
||||
|
||||
const userFlag = await models().userFlag().byUserId(user1.id, UserFlagType.AccountWithoutSubscription);
|
||||
expect(userFlag).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should disable upload and send an email if payment failed', async function() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import BaseModel, { AclAction, SaveOptions, ValidateOptions } from './BaseModel';
|
||||
import { EmailSender, Item, User, Uuid } from '../db';
|
||||
import { EmailSender, Item, User, UserFlagType, Uuid } from '../db';
|
||||
import * as auth from '../utils/auth';
|
||||
import { ErrorUnprocessableEntity, ErrorForbidden, ErrorPayloadTooLarge, ErrorNotFound } from '../utils/errors';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
@ -245,16 +245,6 @@ export default class UserModel extends BaseModel<User> {
|
||||
return !!s[0].length && !!s[1].length;
|
||||
}
|
||||
|
||||
public async enable(id: Uuid, enabled: boolean) {
|
||||
const user = await this.load(id);
|
||||
if (!user) throw new ErrorNotFound(`No such user: ${id}`);
|
||||
await this.save({ id, enabled: enabled ? 1 : 0 });
|
||||
}
|
||||
|
||||
public async disable(id: Uuid) {
|
||||
await this.enable(id, false);
|
||||
}
|
||||
|
||||
public async delete(id: string): Promise<void> {
|
||||
const shares = await this.models().share().sharesByUser(id);
|
||||
|
||||
@ -314,10 +304,6 @@ export default class UserModel extends BaseModel<User> {
|
||||
await this.models().token().deleteByValue(user.id, token);
|
||||
}
|
||||
|
||||
// public async disableUnpaidAccounts() {
|
||||
|
||||
// }
|
||||
|
||||
public async handleBetaUserEmails() {
|
||||
if (!stripeConfig().enabled) return;
|
||||
|
||||
@ -355,7 +341,7 @@ export default class UserModel extends BaseModel<User> {
|
||||
}
|
||||
|
||||
if (remainingDays <= 0) {
|
||||
await this.save({ id: user.id, can_upload: 0 });
|
||||
await this.models().userFlag().add(user.id, UserFlagType.AccountWithoutSubscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -372,7 +358,7 @@ export default class UserModel extends BaseModel<User> {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.save({ id: user.id, can_upload: 0 });
|
||||
await this.models().userFlag().add(user.id, UserFlagType.FailedPaymentWarning);
|
||||
|
||||
await this.models().email().push({
|
||||
...paymentFailedUploadDisabledTemplate(),
|
||||
|
@ -69,6 +69,7 @@ import ShareUserModel from './ShareUserModel';
|
||||
import KeyValueModel from './KeyValueModel';
|
||||
import TokenModel from './TokenModel';
|
||||
import SubscriptionModel from './SubscriptionModel';
|
||||
import UserFlagModel from './UserFlagModel';
|
||||
import { Config } from '../utils/types';
|
||||
|
||||
export class Models {
|
||||
@ -137,6 +138,10 @@ export class Models {
|
||||
return new SubscriptionModel(this.db_, newModelFactory, this.config_);
|
||||
}
|
||||
|
||||
public userFlag() {
|
||||
return new UserFlagModel(this.db_, newModelFactory, this.config_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function newModelFactory(db: DbConnection, config: Config): Models {
|
||||
|
@ -844,7 +844,10 @@ describe('shares.folder', function() {
|
||||
test('should check permissions - cannot share with a disabled account', async function() {
|
||||
const { session: session1 } = await createUserAndSession(1);
|
||||
const { user: user2, session: session2 } = await createUserAndSession(2);
|
||||
await models().user().disable(user2.id);
|
||||
await models().user().save({
|
||||
id: user2.id,
|
||||
enabled: 0,
|
||||
});
|
||||
|
||||
await expectHttpError(async () =>
|
||||
shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [
|
||||
|
@ -174,7 +174,10 @@ describe('shares.link', function() {
|
||||
note_id: noteItem.jop_id,
|
||||
});
|
||||
|
||||
await models().user().disable(user.id);
|
||||
await models().user().save({
|
||||
id: user.id,
|
||||
enabled: 0,
|
||||
});
|
||||
|
||||
await expectHttpError(async () => getShareContent(share.id), ErrorForbidden.httpCode);
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { findPrice, PricePeriod } from '@joplin/lib/utils/joplinCloud';
|
||||
import { UserFlagType } from '../../db';
|
||||
import { AccountType } from '../../models/UserModel';
|
||||
import { betaUserTrialPeriodDays, isBetaUser, stripeConfig } from '../../utils/stripe';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models, koaAppContext, expectNotThrow } from '../../utils/testing/testUtils';
|
||||
@ -191,5 +192,26 @@ describe('index/stripe', function() {
|
||||
}
|
||||
});
|
||||
|
||||
test('should re-enable account if successful payment is made', async function() {
|
||||
const stripe = mockStripe();
|
||||
const ctx = await koaAppContext();
|
||||
|
||||
await createUserViaSubscription(ctx, 'toto@example.com', { stripe, subscriptionId: 'sub_init' });
|
||||
let user = (await models().user().all())[0];
|
||||
await models().user().save({
|
||||
id: user.id,
|
||||
enabled: 0,
|
||||
can_upload: 0,
|
||||
});
|
||||
|
||||
await models().userFlag().add(user.id, UserFlagType.FailedPaymentFinal);
|
||||
|
||||
await simulateWebhook(ctx, 'invoice.paid', { subscription: 'sub_init' });
|
||||
|
||||
user = await models().user().load(user.id);
|
||||
|
||||
expect(user.enabled).toBe(1);
|
||||
expect(user.can_upload).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -10,7 +10,7 @@ import Logger from '@joplin/lib/Logger';
|
||||
import getRawBody = require('raw-body');
|
||||
import { AccountType } from '../../models/UserModel';
|
||||
import { betaUserTrialPeriodDays, cancelSubscription, initStripe, isBetaUser, priceIdToAccountType, stripeConfig } from '../../utils/stripe';
|
||||
import { Subscription } from '../../db';
|
||||
import { Subscription, UserFlagType } from '../../db';
|
||||
import { findPrice, PricePeriod } from '@joplin/lib/utils/joplinCloud';
|
||||
|
||||
const logger = Logger.create('/stripe');
|
||||
@ -250,15 +250,21 @@ export const postHandlers: PostHandlers = {
|
||||
logger.info(`Setting up subscription for existing user: ${existingUser.email}`);
|
||||
|
||||
// First set the account type correctly (in case the
|
||||
// user also upgraded or downgraded their account). Also
|
||||
// re-enable upload if it was disabled.
|
||||
// user also upgraded or downgraded their account).
|
||||
await models.user().save({
|
||||
id: existingUser.id,
|
||||
account_type: accountType,
|
||||
can_upload: 1,
|
||||
enabled: 1,
|
||||
});
|
||||
|
||||
// Also clear any payment and subscription related flags
|
||||
// since if we're here it means payment was successful
|
||||
await models.userFlag().removeMulti(existingUser.id, [
|
||||
UserFlagType.FailedPaymentWarning,
|
||||
UserFlagType.FailedPaymentFinal,
|
||||
UserFlagType.SubscriptionCancelled,
|
||||
UserFlagType.AccountWithoutSubscription,
|
||||
]);
|
||||
|
||||
// Then save the subscription
|
||||
await models.subscription().save({
|
||||
user_id: existingUser.id,
|
||||
@ -319,8 +325,8 @@ export const postHandlers: PostHandlers = {
|
||||
// by the user. In that case, we disable the user.
|
||||
|
||||
const { sub } = await getSubscriptionInfo(event, ctx);
|
||||
await models.user().enable(sub.user_id, false);
|
||||
await models.subscription().toggleSoftDelete(sub.id, true);
|
||||
await models.userFlag().add(sub.user_id, UserFlagType.SubscriptionCancelled);
|
||||
},
|
||||
|
||||
'customer.subscription.updated': async () => {
|
||||
|
@ -4,7 +4,7 @@ import { RouteType } from '../../utils/types';
|
||||
import { AppContext, HttpMethod } from '../../utils/types';
|
||||
import { bodyFields, contextSessionId, formParse } from '../../utils/requestUtils';
|
||||
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||
import { User, Uuid } from '../../db';
|
||||
import { User, UserFlagType, Uuid } from '../../db';
|
||||
import config from '../../config';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
@ -273,40 +273,41 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
|
||||
if (userIsMe(path)) fields.id = userId;
|
||||
user = makeUser(isNew, fields);
|
||||
|
||||
const userModel = ctx.joplin.models.user();
|
||||
const models = ctx.joplin.models;
|
||||
|
||||
if (fields.post_button) {
|
||||
const userToSave: User = userModel.fromApiInput(user);
|
||||
await userModel.checkIfAllowed(ctx.joplin.owner, isNew ? AclAction.Create : AclAction.Update, userToSave);
|
||||
const userToSave: User = models.user().fromApiInput(user);
|
||||
await models.user().checkIfAllowed(ctx.joplin.owner, isNew ? AclAction.Create : AclAction.Update, userToSave);
|
||||
|
||||
if (isNew) {
|
||||
await userModel.save(userToSave);
|
||||
await models.user().save(userToSave);
|
||||
} else {
|
||||
await userModel.save(userToSave, { isNew: false });
|
||||
await models.user().save(userToSave, { isNew: false });
|
||||
}
|
||||
} else if (fields.user_cancel_subscription_button) {
|
||||
await cancelSubscriptionByUserId(ctx.joplin.models, userId);
|
||||
await cancelSubscriptionByUserId(models, userId);
|
||||
const sessionId = contextSessionId(ctx, false);
|
||||
if (sessionId) {
|
||||
await ctx.joplin.models.session().logout(sessionId);
|
||||
await models.session().logout(sessionId);
|
||||
return redirect(ctx, config().baseUrl);
|
||||
}
|
||||
} else {
|
||||
if (ctx.joplin.owner.is_admin) {
|
||||
if (fields.disable_button || fields.restore_button) {
|
||||
const user = await userModel.load(path.id);
|
||||
await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user);
|
||||
await userModel.enable(path.id, !!fields.restore_button);
|
||||
const user = await models.user().load(path.id);
|
||||
await models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user);
|
||||
|
||||
await models.userFlag().toggle(user.id, UserFlagType.ManuallyDisabled, !!fields.restore_button);
|
||||
} else if (fields.send_reset_password_email) {
|
||||
const user = await userModel.load(path.id);
|
||||
await userModel.save({ id: user.id, must_set_password: 1 });
|
||||
await userModel.sendAccountConfirmationEmail(user);
|
||||
const user = await models.user().load(path.id);
|
||||
await models.user().save({ id: user.id, must_set_password: 1 });
|
||||
await models.user().sendAccountConfirmationEmail(user);
|
||||
} else if (fields.cancel_subscription_button) {
|
||||
await cancelSubscriptionByUserId(ctx.joplin.models, userId);
|
||||
await cancelSubscriptionByUserId(models, userId);
|
||||
} else if (fields.update_subscription_basic_button) {
|
||||
await updateSubscriptionType(ctx.joplin.models, userId, AccountType.Basic);
|
||||
await updateSubscriptionType(models, userId, AccountType.Basic);
|
||||
} else if (fields.update_subscription_pro_button) {
|
||||
await updateSubscriptionType(ctx.joplin.models, userId, AccountType.Pro);
|
||||
await updateSubscriptionType(models, userId, AccountType.Pro);
|
||||
} else {
|
||||
throw new Error('Invalid form button');
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import { DatabaseConfig } from '../utils/types';
|
||||
const { execCommand } = require('@joplin/tools/tool-utils');
|
||||
|
||||
export interface CreateDbOptions {
|
||||
dropIfExists: boolean;
|
||||
dropIfExists?: boolean;
|
||||
autoMigrate?: boolean;
|
||||
}
|
||||
|
||||
export interface DropDbOptions {
|
||||
@ -15,6 +16,7 @@ export interface DropDbOptions {
|
||||
export async function createDb(config: DatabaseConfig, options: CreateDbOptions = null) {
|
||||
options = {
|
||||
dropIfExists: false,
|
||||
autoMigrate: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
@ -46,7 +48,7 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
|
||||
|
||||
try {
|
||||
const db = await connectDb(config);
|
||||
await migrateLatest(db);
|
||||
if (options.autoMigrate) await migrateLatest(db);
|
||||
await disconnectDb(db);
|
||||
} catch (error) {
|
||||
error.message += `: ${config.name}`;
|
||||
|
@ -22,36 +22,38 @@ const config = {
|
||||
'tableNameCasing': 'pascal' as any,
|
||||
'filename': './db',
|
||||
'extends': {
|
||||
'main.sessions': 'WithDates, WithUuid',
|
||||
'main.users': 'WithDates, WithUuid',
|
||||
'main.items': 'WithDates, WithUuid',
|
||||
'main.api_clients': 'WithDates, WithUuid',
|
||||
'main.changes': 'WithDates, WithUuid',
|
||||
'main.notifications': 'WithDates, WithUuid',
|
||||
'main.shares': 'WithDates, WithUuid',
|
||||
'main.share_users': 'WithDates, WithUuid',
|
||||
'main.user_items': 'WithDates',
|
||||
'main.emails': 'WithDates',
|
||||
'main.items': 'WithDates, WithUuid',
|
||||
'main.notifications': 'WithDates, WithUuid',
|
||||
'main.sessions': 'WithDates, WithUuid',
|
||||
'main.share_users': 'WithDates, WithUuid',
|
||||
'main.shares': 'WithDates, WithUuid',
|
||||
'main.tokens': 'WithDates',
|
||||
'main.user_flags': 'WithDates',
|
||||
'main.user_items': 'WithDates',
|
||||
'main.users': 'WithDates, WithUuid',
|
||||
},
|
||||
};
|
||||
|
||||
const propertyTypes: Record<string, string> = {
|
||||
'*.item_type': 'ItemType',
|
||||
'changes.type': 'ChangeType',
|
||||
'notifications.level': 'NotificationLevel',
|
||||
'shares.type': 'ShareType',
|
||||
'items.content': 'Buffer',
|
||||
'items.jop_updated_time': 'number',
|
||||
'share_users.status': 'ShareUserStatus',
|
||||
'emails.sender_id': 'EmailSender',
|
||||
'emails.sent_time': 'number',
|
||||
'subscriptions.last_payment_time': 'number',
|
||||
'items.content': 'Buffer',
|
||||
'items.jop_updated_time': 'number',
|
||||
'notifications.level': 'NotificationLevel',
|
||||
'share_users.status': 'ShareUserStatus',
|
||||
'shares.type': 'ShareType',
|
||||
'subscriptions.last_payment_failed_time': 'number',
|
||||
'subscriptions.last_payment_time': 'number',
|
||||
'user_flags.type': 'UserFlagType',
|
||||
'users.can_share_folder': 'number | null',
|
||||
'users.can_share_note': 'number | null',
|
||||
'users.max_total_item_size': 'number | null',
|
||||
'users.max_item_size': 'number | null',
|
||||
'users.max_total_item_size': 'number | null',
|
||||
'users.total_item_size': 'number',
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { User, Session, DbConnection, connectDb, disconnectDb, truncateTables, Item, Uuid } from '../../db';
|
||||
import { createDb } from '../../tools/dbTools';
|
||||
import { createDb, CreateDbOptions } from '../../tools/dbTools';
|
||||
import modelFactory from '../../models/factory';
|
||||
import { AppContext, Env } from '../types';
|
||||
import config, { initConfig } from '../../config';
|
||||
@ -60,7 +60,7 @@ function initGlobalLogger() {
|
||||
}
|
||||
|
||||
let createdDbPath_: string = null;
|
||||
export async function beforeAllDb(unitName: string) {
|
||||
export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOptions = null) {
|
||||
unitName = unitName.replace(/\//g, '_');
|
||||
|
||||
createdDbPath_ = `${packageRootDir}/db-test-${unitName}.sqlite`;
|
||||
@ -89,7 +89,7 @@ export async function beforeAllDb(unitName: string) {
|
||||
|
||||
initGlobalLogger();
|
||||
|
||||
await createDb(config().database, { dropIfExists: true });
|
||||
await createDb(config().database, { dropIfExists: true, ...createDbOptions });
|
||||
db_ = await connectDb(config().database);
|
||||
|
||||
const mustache = new MustacheService(config().viewDir, config().baseUrl);
|
||||
|
Loading…
Reference in New Issue
Block a user