diff --git a/packages/server/schema.sqlite b/packages/server/schema.sqlite index aefe885035..a7c412b2a2 100644 Binary files a/packages/server/schema.sqlite and b/packages/server/schema.sqlite differ diff --git a/packages/server/src/migrations/20230804154410_can_receive_folder.ts b/packages/server/src/migrations/20230804154410_can_receive_folder.ts new file mode 100644 index 0000000000..2ec3a1ff75 --- /dev/null +++ b/packages/server/src/migrations/20230804154410_can_receive_folder.ts @@ -0,0 +1,14 @@ +import { Knex } from 'knex'; +import { DbConnection } from '../db'; + +export const up = async (db: DbConnection) => { + await db.schema.alterTable('users', (table: Knex.CreateTableBuilder) => { + table.specificType('can_receive_folder', 'smallint').defaultTo(null).nullable(); + }); +}; + +export const down = async (db: DbConnection) => { + await db.schema.alterTable('users', (table: Knex.CreateTableBuilder) => { + table.dropColumn('can_receive_folder'); + }); +}; diff --git a/packages/server/src/models/ShareUserModel.ts b/packages/server/src/models/ShareUserModel.ts index f0c0fadfd7..0bb5d1c61d 100644 --- a/packages/server/src/models/ShareUserModel.ts +++ b/packages/server/src/models/ShareUserModel.ts @@ -1,7 +1,7 @@ import { Item, Share, ShareType, ShareUser, ShareUserStatus, User, Uuid } from '../services/database/types'; import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from '../utils/errors'; import BaseModel, { AclAction, DeleteOptions } from './BaseModel'; -import { getCanShareFolder } from './utils/user'; +import { getCanReceiveFolder } from './utils/user'; export default class ShareUserModel extends BaseModel { @@ -11,9 +11,9 @@ export default class ShareUserModel extends BaseModel { public async checkIfAllowed(user: User, action: AclAction, resource: ShareUser = null): Promise { if (action === AclAction.Create) { - const recipient = await this.models().user().load(resource.user_id, { fields: ['account_type', 'can_share_folder', 'enabled'] }); + const recipient = await this.models().user().load(resource.user_id, { fields: ['account_type', 'can_receive_folder', 'enabled'] }); if (!recipient.enabled) throw new ErrorForbidden('the recipient account is disabled'); - if (!getCanShareFolder(recipient)) throw new ErrorForbidden('The sharing feature is not enabled for the recipient account'); + if (!getCanReceiveFolder(recipient)) throw new ErrorForbidden('The sharing feature is not enabled for the recipient account'); const share = await this.models().share().load(resource.share_id); if (share.owner_id !== user.id) throw new ErrorForbidden('no access to the share object'); diff --git a/packages/server/src/models/UserModel.ts b/packages/server/src/models/UserModel.ts index 7d039b3402..22619309db 100644 --- a/packages/server/src/models/UserModel.ts +++ b/packages/server/src/models/UserModel.ts @@ -47,6 +47,7 @@ export enum AccountType { export interface Account { account_type: number; can_share_folder: number; + can_receive_folder: number; max_item_size: number; max_total_item_size: number; } @@ -61,18 +62,21 @@ export function accountByType(accountType: AccountType): Account { { account_type: AccountType.Default, can_share_folder: 1, + can_receive_folder: 1, max_item_size: 0, max_total_item_size: 0, }, { account_type: AccountType.Basic, can_share_folder: 0, + can_receive_folder: 1, max_item_size: 10 * MB, max_total_item_size: 1 * GB, }, { account_type: AccountType.Pro, can_share_folder: 1, + can_receive_folder: 1, max_item_size: 200 * MB, max_total_item_size: 10 * GB, }, @@ -136,6 +140,7 @@ export default class UserModel extends BaseModel { if ('max_item_size' in object) user.max_item_size = object.max_item_size; if ('max_total_item_size' in object) user.max_total_item_size = object.max_total_item_size; if ('can_share_folder' in object) user.can_share_folder = object.can_share_folder; + if ('can_receive_folder' in object) user.can_receive_folder = object.can_receive_folder; if ('can_upload' in object) user.can_upload = object.can_upload; if ('account_type' in object) user.account_type = object.account_type; if ('must_set_password' in object) user.must_set_password = object.must_set_password; diff --git a/packages/server/src/models/utils/user.ts b/packages/server/src/models/utils/user.ts index 98e9e5ba43..3d31f74ad1 100644 --- a/packages/server/src/models/utils/user.ts +++ b/packages/server/src/models/utils/user.ts @@ -7,6 +7,12 @@ export function getCanShareFolder(user: User): number { return user.can_share_folder !== null ? user.can_share_folder : account.can_share_folder; } +export function getCanReceiveFolder(user: User): number { + if (!('account_type' in user) || !('can_receive_folder' in user)) throw new Error('Missing account_type or can_receive_folder property'); + const account = accountByType(user.account_type); + return user.can_receive_folder !== null ? user.can_receive_folder : account.can_receive_folder; +} + export function getMaxItemSize(user: User): number { if (!('account_type' in user) || !('max_item_size' in user)) throw new Error('Missing account_type or max_item_size property'); const account = accountByType(user.account_type); diff --git a/packages/server/src/routes/admin/users.ts b/packages/server/src/routes/admin/users.ts index 2860f38cb3..20ae299185 100644 --- a/packages/server/src/routes/admin/users.ts +++ b/packages/server/src/routes/admin/users.ts @@ -64,6 +64,7 @@ function makeUser(isNew: boolean, fields: any): User { if ('max_item_size' in fields) user.max_item_size = intOrDefaultToValue(fields, 'max_item_size'); if ('max_total_item_size' in fields) user.max_total_item_size = intOrDefaultToValue(fields, 'max_total_item_size'); if ('can_share_folder' in fields) user.can_share_folder = boolOrDefaultToValue(fields, 'can_share_folder'); + if ('can_receive_folder' in fields) user.can_receive_folder = boolOrDefaultToValue(fields, 'can_receive_folder'); if ('can_upload' in fields) user.can_upload = intOrDefaultToValue(fields, 'can_upload'); if ('account_type' in fields) user.account_type = Number(fields.account_type); diff --git a/packages/server/src/routes/api/shares.folder.test.ts b/packages/server/src/routes/api/shares.folder.test.ts index 2cf6b84b88..f9d4aaebce 100644 --- a/packages/server/src/routes/api/shares.folder.test.ts +++ b/packages/server/src/routes/api/shares.folder.test.ts @@ -896,7 +896,7 @@ describe('shares.folder', () => { test('should check permissions - cannot share if share feature not enabled for recipient', async () => { const { session: session1 } = await createUserAndSession(1); const { user: user2, session: session2 } = await createUserAndSession(2); - await models().user().save({ id: user2.id, can_share_folder: 0 }); + await models().user().save({ id: user2.id, can_receive_folder: 0 }); await expectHttpError(async () => shareFolderWithUser(session1.id, session2.id, '000000000000000000000000000000F1', [ diff --git a/packages/server/src/routes/index/home.ts b/packages/server/src/routes/index/home.ts index a0dd15218c..edba94f713 100644 --- a/packages/server/src/routes/index/home.ts +++ b/packages/server/src/routes/index/home.ts @@ -7,7 +7,7 @@ import { ErrorMethodNotAllowed } from '../../utils/errors'; import defaultView from '../../utils/defaultView'; import { AccountType, accountTypeToString } from '../../models/UserModel'; import { formatMaxItemSize, formatMaxTotalSize, formatTotalSize, formatTotalSizePercent, yesOrNo } from '../../utils/strings'; -import { getCanShareFolder, totalSizeClass } from '../../models/utils/user'; +import { getCanReceiveFolder, getCanShareFolder, totalSizeClass } from '../../models/utils/user'; import config from '../../config'; import { escapeHtml } from '../../utils/htmlUtils'; import { betaStartSubUrl, betaUserTrialPeriodDays, isBetaUser } from '../../utils/stripe'; @@ -33,41 +33,46 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => { view.content = { userProps: [ { - label: 'Account Type', + label: 'Account type', value: accountTypeToString(user.account_type), show: true, }, { - label: 'Is Admin', + label: 'Is admin', value: yesOrNo(user.is_admin), show: !!user.is_admin, }, { - label: 'Max Item Size', + label: 'Max item size', value: formatMaxItemSize(user), show: true, }, { - label: 'Total Size', + label: 'Total size', classes: [totalSizeClass(user)], value: `${formatTotalSize(user)} (${formatTotalSizePercent(user)})`, show: true, }, { - label: 'Max Total Size', + label: 'Max total size', value: formatMaxTotalSize(user), show: true, }, { - label: 'Can Publish Note', + label: 'Can publish notes', value: yesOrNo(true), show: true, }, { - label: 'Can Share Notebook', + label: 'Can share notebooks', value: yesOrNo(getCanShareFolder(user)), show: true, }, + { + label: 'Can receive notebooks', + value: yesOrNo(getCanReceiveFolder(user)), + show: true, + }, ], showUpgradeProButton: subscription && user.account_type === AccountType.Basic, showBetaMessage: await isBetaUser(ctx.joplin.models, user.id), diff --git a/packages/server/src/services/database/types.ts b/packages/server/src/services/database/types.ts index 2583d0f0ff..e71b3cfbf3 100644 --- a/packages/server/src/services/database/types.ts +++ b/packages/server/src/services/database/types.ts @@ -246,6 +246,7 @@ export interface User extends WithDates, WithUuid { total_item_size?: number; enabled?: number; disabled_time?: number; + can_receive_folder?: number; } export interface UserFlag extends WithDates { @@ -454,6 +455,7 @@ export const databaseSchema: DatabaseTables = { total_item_size: { type: 'string' }, enabled: { type: 'number' }, disabled_time: { type: 'string' }, + can_receive_folder: { type: 'number' }, }, user_flags: { id: { type: 'number' }, diff --git a/packages/server/src/tools/generateTypes.ts b/packages/server/src/tools/generateTypes.ts index 28dbe95fcc..f7fbd51643 100644 --- a/packages/server/src/tools/generateTypes.ts +++ b/packages/server/src/tools/generateTypes.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -import sqlts from '@rmp135/sql-ts'; +import sqlts, { Config } from '@rmp135/sql-ts'; require('source-map-support').install(); @@ -8,7 +8,7 @@ const dbFilePath = `${__dirname}/../../src/services/database/types.ts`; const fileReplaceWithinMarker = '// AUTO-GENERATED-TYPES'; -const config = { +const config: Config = { 'client': 'sqlite3', 'connection': { 'filename': './db-buildTypes.sqlite', @@ -23,6 +23,7 @@ const config = { 'singularTableNames': true, 'tableNameCasing': 'pascal' as any, 'filename': './db', + 'columnSortOrder': 'source', 'extends': { 'main.api_clients': 'WithDates, WithUuid', 'main.backup_items': 'WithCreatedDate',