1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-17 00:33:59 +02:00

Compare commits

..

7 Commits

Author SHA1 Message Date
Laurent Cozic
8411e1270e fix 2021-08-22 11:25:51 +01:00
Laurent Cozic
e9d4a777fd refactor 2021-08-22 11:23:07 +01:00
Laurent Cozic
85984f1f39 comment 2021-08-21 18:17:17 +01:00
Laurent Cozic
3c0524c6e9 update 2021-08-21 17:50:40 +01:00
Laurent Cozic
769d47a768 flags 2021-08-21 16:59:50 +01:00
Laurent Cozic
87fe0e4dcf update 2021-08-21 15:14:54 +01:00
Laurent Cozic
2513e0aaab update 2021-08-21 15:06:41 +01:00
34 changed files with 449 additions and 117 deletions

View File

@@ -32,7 +32,7 @@
],
"owner": "Laurent Cozic"
},
"version": "2.4.0",
"version": "2.3.2",
"bin": {
"joplin": "./main.js"
},
@@ -40,8 +40,8 @@
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"aws-sdk": "^2.588.0",
"chalk": "^4.1.0",
"compare-version": "^0.1.2",
@@ -65,7 +65,7 @@
"yargs-parser": "^7.0.0"
},
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@types/fs-extra": "^9.0.6",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper [DEV]",
"version": "2.4.0",
"version": "2.3.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": "script-src 'self'; object-src 'self'",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.1",
"version": "2.3.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.1",
"version": "2.3.5",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.1",
"version": "2.3.5",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -93,7 +93,7 @@
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@testing-library/react-hooks": "^3.4.2",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.6",
@@ -122,8 +122,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"async-mutex": "^0.1.3",
"codemirror": "^5.56.0",
"color": "^3.1.2",

View File

@@ -142,7 +142,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097648
versionName "2.4.0"
versionName "2.3.4"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -492,7 +492,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -519,7 +519,7 @@
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -666,7 +666,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -697,7 +697,7 @@
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.4.0;
MARKETING_VERSION = 12.3.1;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -488,7 +488,7 @@ SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: 6da2c8ff1ebe6b6cf4510fcca58c24c4d02b16fc
FBReactNativeSpec: d2f54de51f69366bd1f5c1fb9270698dce678f8d
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 1,
"id": "<%= pluginId %>",
"app_min_version": "2.4",
"app_min_version": "2.3",
"version": "1.0.0",
"name": "<%= pluginName %>",
"description": "<%= pluginDescription %>",

View File

@@ -1,6 +1,6 @@
{
"name": "generator-joplin",
"version": "2.4.0",
"version": "2.3.0",
"description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": {

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"license": "ISC",
"dependencies": {
"async-mutex": "^0.1.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "2.4.0",
"version": "2.3.1",
"description": "Joplin Core library",
"author": "Laurent Cozic",
"homepage": "",
@@ -27,7 +27,7 @@
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.33",
"@joplin/fork-sax": "^1.2.37",
"@joplin/renderer": "~2.4",
"@joplin/renderer": "^2.3.1",
"@joplin/turndown": "^4.0.55",
"@joplin/turndown-plugin-gfm": "^1.0.37",
"async-mutex": "^0.1.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "2.4.0",
"version": "2.3.1",
"description": "",
"main": "index.js",
"bin": {
@@ -18,8 +18,8 @@
"author": "",
"license": "MIT",
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/tools": "~2.4",
"@joplin/lib": "^2.3.1",
"@joplin/tools": "^2.3.1",
"fs-extra": "^9.0.1",
"gh-release-assets": "^2.0.0",
"node-fetch": "^2.6.1",

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"license": "MIT",
"dependencies": {
"font-awesome-filetypes": "^2.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/renderer",
"version": "2.4.0",
"version": "2.3.1",
"description": "The Joplin note renderer, used the mobile and desktop application",
"repository": "https://github.com/laurent22/joplin/tree/dev/packages/renderer",
"main": "index.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.4.0",
"version": "2.3.7",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -19,8 +19,8 @@
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@joplin/lib": "~2.4",
"@joplin/renderer": "~2.4",
"@joplin/lib": "~2.3",
"@joplin/renderer": "~2.3",
"@koa/cors": "^3.1.0",
"bcryptjs": "^2.4.3",
"bulma": "^0.9.1",
@@ -51,7 +51,7 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@joplin/tools": "~2.4",
"@joplin/tools": "~2.3",
"@rmp135/sql-ts": "^1.7.0",
"@types/fs-extra": "^8.0.0",
"@types/jest": "^26.0.15",

Binary file not shown.

View 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);
// }
// });
// });

View File

@@ -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

View 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');
}

View File

@@ -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,

View 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);
});
});

View 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);
}
}

View File

@@ -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() {

View File

@@ -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(),

View File

@@ -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 {

View File

@@ -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', [

View File

@@ -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);
});

View File

@@ -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);
});
});

View File

@@ -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 () => {

View File

@@ -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');
}

View File

@@ -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}`;

View File

@@ -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',
};

View File

@@ -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);

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/tools",
"version": "2.4.0",
"version": "2.3.1",
"description": "Various tools for Joplin",
"main": "index.js",
"author": "Laurent Cozic",
@@ -18,7 +18,7 @@
},
"license": "MIT",
"dependencies": {
"@joplin/lib": "~2.4",
"@joplin/lib": "^2.3.1",
"execa": "^4.1.0",
"fs-extra": "^4.0.3",
"gettext-parser": "^1.3.0",