Server: Allow creating a user with a specific account type from admin UI

release-2.0
Laurent Cozic 2021-06-16 15:02:26 +01:00
parent 3c181906c2
commit ecd1602658
7 changed files with 76 additions and 6 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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