mirror of https://github.com/laurent22/joplin.git
Server: Allow creating a user with a specific account type from admin UI
parent
3c181906c2
commit
ecd1602658
|
@ -38,6 +38,7 @@ export interface EnvVariables {
|
||||||
|
|
||||||
SIGNUP_ENABLED?: string;
|
SIGNUP_ENABLED?: string;
|
||||||
TERMS_ENABLED?: string;
|
TERMS_ENABLED?: string;
|
||||||
|
ACCOUNT_TYPES_ENABLED?: string;
|
||||||
|
|
||||||
ERROR_STACK_TRACES?: string;
|
ERROR_STACK_TRACES?: string;
|
||||||
}
|
}
|
||||||
|
@ -152,6 +153,7 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
|
||||||
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
|
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
|
||||||
signupEnabled: env.SIGNUP_ENABLED === '1',
|
signupEnabled: env.SIGNUP_ENABLED === '1',
|
||||||
termsEnabled: env.TERMS_ENABLED === '1',
|
termsEnabled: env.TERMS_ENABLED === '1',
|
||||||
|
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { formatBytes, MB } from '../utils/bytes';
|
||||||
|
|
||||||
export enum AccountType {
|
export enum AccountType {
|
||||||
Default = 0,
|
Default = 0,
|
||||||
Free = 1,
|
Basic = 1,
|
||||||
Pro = 2,
|
Pro = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,11 @@ interface AccountTypeProperties {
|
||||||
max_item_size: number;
|
max_item_size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AccountTypeSelectOptions {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function accountTypeProperties(accountType: AccountType): AccountTypeProperties {
|
export function accountTypeProperties(accountType: AccountType): AccountTypeProperties {
|
||||||
const types: AccountTypeProperties[] = [
|
const types: AccountTypeProperties[] = [
|
||||||
{
|
{
|
||||||
|
@ -26,7 +31,7 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
|
||||||
max_item_size: 0,
|
max_item_size: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
account_type: AccountType.Free,
|
account_type: AccountType.Basic,
|
||||||
can_share: 0,
|
can_share: 0,
|
||||||
max_item_size: 10 * MB,
|
max_item_size: 10 * MB,
|
||||||
},
|
},
|
||||||
|
@ -37,7 +42,26 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return types.find(a => a.account_type === accountType);
|
const type = types.find(a => a.account_type === accountType);
|
||||||
|
if (!type) throw new Error(`Invalid account type: ${accountType}`);
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function accountTypeOptions(): AccountTypeSelectOptions[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: AccountType.Default,
|
||||||
|
label: 'Default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: AccountType.Basic,
|
||||||
|
label: 'Basic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: AccountType.Pro,
|
||||||
|
label: 'Pro',
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UserModel extends BaseModel<User> {
|
export default class UserModel extends BaseModel<User> {
|
||||||
|
@ -68,6 +92,7 @@ export default class UserModel extends BaseModel<User> {
|
||||||
if ('full_name' in object) user.full_name = object.full_name;
|
if ('full_name' in object) user.full_name = object.full_name;
|
||||||
if ('max_item_size' in object) user.max_item_size = object.max_item_size;
|
if ('max_item_size' in object) user.max_item_size = object.max_item_size;
|
||||||
if ('can_share' in object) user.can_share = object.can_share;
|
if ('can_share' in object) user.can_share = object.can_share;
|
||||||
|
if ('account_type' in object) user.account_type = object.account_type;
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
@ -96,6 +121,7 @@ export default class UserModel extends BaseModel<User> {
|
||||||
if (user.is_admin && user.id === resource.id && 'is_admin' in resource && !resource.is_admin) throw new ErrorForbidden('admin user cannot make themselves a non-admin');
|
if (user.is_admin && user.id === resource.id && 'is_admin' in resource && !resource.is_admin) throw new ErrorForbidden('admin user cannot make themselves a non-admin');
|
||||||
if ('max_item_size' in resource && !user.is_admin && resource.max_item_size !== previousResource.max_item_size) throw new ErrorForbidden('non-admin user cannot change max_item_size');
|
if ('max_item_size' in resource && !user.is_admin && resource.max_item_size !== previousResource.max_item_size) throw new ErrorForbidden('non-admin user cannot change max_item_size');
|
||||||
if ('can_share' in resource && !user.is_admin && resource.can_share !== previousResource.can_share) throw new ErrorForbidden('non-admin user cannot change can_share');
|
if ('can_share' in resource && !user.is_admin && resource.can_share !== previousResource.can_share) throw new ErrorForbidden('non-admin user cannot change can_share');
|
||||||
|
if ('account_type' in resource && !user.is_admin && resource.account_type !== previousResource.account_type) throw new ErrorForbidden('non-admin user cannot change account_type');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === AclAction.Delete) {
|
if (action === AclAction.Delete) {
|
||||||
|
@ -197,6 +223,17 @@ export default class UserModel extends BaseModel<User> {
|
||||||
await this.save({ id: user.id, email_confirmed: 1 });
|
await this.save({ id: user.id, email_confirmed: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// public async saveWithAccountType(accountType:AccountType, user: User, options: SaveOptions = {}): Promise<User> {
|
||||||
|
// if (accountType !== AccountType.Default) {
|
||||||
|
// user = {
|
||||||
|
// ...user,
|
||||||
|
// ...accountTypeProperties(accountType),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return this.save(user, options);
|
||||||
|
// }
|
||||||
|
|
||||||
// Note that when the "password" property is provided, it is going to be
|
// Note that when the "password" property is provided, it is going to be
|
||||||
// hashed automatically. It means that it is not safe to do:
|
// hashed automatically. It means that it is not safe to do:
|
||||||
//
|
//
|
||||||
|
|
|
@ -41,7 +41,7 @@ describe('index_signup', function() {
|
||||||
// Check that the user has been created
|
// Check that the user has been created
|
||||||
const user = await models().user().loadByEmail('toto@example.com');
|
const user = await models().user().loadByEmail('toto@example.com');
|
||||||
expect(user).toBeTruthy();
|
expect(user).toBeTruthy();
|
||||||
expect(user.account_type).toBe(AccountType.Free);
|
expect(user.account_type).toBe(AccountType.Basic);
|
||||||
expect(user.email_confirmed).toBe(0);
|
expect(user.email_confirmed).toBe(0);
|
||||||
expect(user.can_share).toBe(0);
|
expect(user.can_share).toBe(0);
|
||||||
expect(user.max_item_size).toBe(10 * MB);
|
expect(user.max_item_size).toBe(10 * MB);
|
||||||
|
|
|
@ -44,7 +44,7 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => {
|
||||||
const password = checkPassword(formUser, true);
|
const password = checkPassword(formUser, true);
|
||||||
|
|
||||||
const user = await ctx.models.user().save({
|
const user = await ctx.models.user().save({
|
||||||
...accountTypeProperties(AccountType.Free),
|
...accountTypeProperties(AccountType.Basic),
|
||||||
email: formUser.email,
|
email: formUser.email,
|
||||||
full_name: formUser.full_name,
|
full_name: formUser.full_name,
|
||||||
password,
|
password,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import defaultView from '../../utils/defaultView';
|
||||||
import { AclAction } from '../../models/BaseModel';
|
import { AclAction } from '../../models/BaseModel';
|
||||||
import { NotificationKey } from '../../models/NotificationModel';
|
import { NotificationKey } from '../../models/NotificationModel';
|
||||||
import { formatBytes } from '../../utils/bytes';
|
import { formatBytes } from '../../utils/bytes';
|
||||||
|
import { accountTypeOptions, accountTypeProperties } from '../../models/UserModel';
|
||||||
|
|
||||||
interface CheckPasswordInput {
|
interface CheckPasswordInput {
|
||||||
password: string;
|
password: string;
|
||||||
|
@ -29,7 +30,7 @@ export function checkPassword(fields: CheckPasswordInput, required: boolean): st
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUser(isNew: boolean, fields: any): User {
|
function makeUser(isNew: boolean, fields: any): User {
|
||||||
const user: User = {};
|
let user: User = {};
|
||||||
|
|
||||||
if ('email' in fields) user.email = fields.email;
|
if ('email' in fields) user.email = fields.email;
|
||||||
if ('full_name' in fields) user.full_name = fields.full_name;
|
if ('full_name' in fields) user.full_name = fields.full_name;
|
||||||
|
@ -37,6 +38,14 @@ function makeUser(isNew: boolean, fields: any): User {
|
||||||
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size || 0;
|
if ('max_item_size' in fields) user.max_item_size = fields.max_item_size || 0;
|
||||||
if ('can_share' in fields) user.can_share = fields.can_share ? 1 : 0;
|
if ('can_share' in fields) user.can_share = fields.can_share ? 1 : 0;
|
||||||
|
|
||||||
|
if ('account_type' in fields) {
|
||||||
|
user.account_type = Number(fields.account_type);
|
||||||
|
user = {
|
||||||
|
...user,
|
||||||
|
...accountTypeProperties(user.account_type),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const password = checkPassword(fields, false);
|
const password = checkPassword(fields, false);
|
||||||
if (password) user.password = password;
|
if (password) user.password = password;
|
||||||
|
|
||||||
|
@ -108,6 +117,14 @@ router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null
|
||||||
view.content.postUrl = postUrl;
|
view.content.postUrl = postUrl;
|
||||||
view.content.showDeleteButton = !isNew && !!owner.is_admin && owner.id !== user.id;
|
view.content.showDeleteButton = !isNew && !!owner.is_admin && owner.id !== user.id;
|
||||||
|
|
||||||
|
if (config().accountTypesEnabled) {
|
||||||
|
view.content.showAccountTypes = true;
|
||||||
|
view.content.accountTypes = accountTypeOptions().map((o: any) => {
|
||||||
|
o.selected = user.account_type === o.value;
|
||||||
|
return o;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,7 @@ export interface Config {
|
||||||
userContentBaseUrl: string;
|
userContentBaseUrl: string;
|
||||||
signupEnabled: boolean;
|
signupEnabled: boolean;
|
||||||
termsEnabled: boolean;
|
termsEnabled: boolean;
|
||||||
|
accountTypesEnabled: boolean;
|
||||||
showErrorStackTraces: boolean;
|
showErrorStackTraces: boolean;
|
||||||
database: DatabaseConfig;
|
database: DatabaseConfig;
|
||||||
mailer: MailerConfig;
|
mailer: MailerConfig;
|
||||||
|
|
|
@ -15,6 +15,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{#global.owner.is_admin}}
|
{{#global.owner.is_admin}}
|
||||||
|
{{#showAccountTypes}}
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Account type</label>
|
||||||
|
<div class="select">
|
||||||
|
<select name="account_type">
|
||||||
|
{{#accountTypes}}
|
||||||
|
<option value="{{value}}" {{#selected}}selected{{/selected}}>{{label}}</option>
|
||||||
|
{{/accountTypes}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/showAccountTypes}}
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Max item size</label>
|
<label class="label">Max item size</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
|
|
Loading…
Reference in New Issue