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;
TERMS_ENABLED?: string;
ACCOUNT_TYPES_ENABLED?: 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,
signupEnabled: env.SIGNUP_ENABLED === '1',
termsEnabled: env.TERMS_ENABLED === '1',
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
...overrides,
};
}

View File

@ -8,7 +8,7 @@ import { formatBytes, MB } from '../utils/bytes';
export enum AccountType {
Default = 0,
Free = 1,
Basic = 1,
Pro = 2,
}
@ -18,6 +18,11 @@ interface AccountTypeProperties {
max_item_size: number;
}
interface AccountTypeSelectOptions {
value: number;
label: string;
}
export function accountTypeProperties(accountType: AccountType): AccountTypeProperties {
const types: AccountTypeProperties[] = [
{
@ -26,7 +31,7 @@ export function accountTypeProperties(accountType: AccountType): AccountTypeProp
max_item_size: 0,
},
{
account_type: AccountType.Free,
account_type: AccountType.Basic,
can_share: 0,
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> {
@ -68,6 +92,7 @@ export default class UserModel extends BaseModel<User> {
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 ('can_share' in object) user.can_share = object.can_share;
if ('account_type' in object) user.account_type = object.account_type;
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 ('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 ('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) {
@ -197,6 +223,17 @@ export default class UserModel extends BaseModel<User> {
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
// 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
const user = await models().user().loadByEmail('toto@example.com');
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.can_share).toBe(0);
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 user = await ctx.models.user().save({
...accountTypeProperties(AccountType.Free),
...accountTypeProperties(AccountType.Basic),
email: formUser.email,
full_name: formUser.full_name,
password,

View File

@ -11,6 +11,7 @@ import defaultView from '../../utils/defaultView';
import { AclAction } from '../../models/BaseModel';
import { NotificationKey } from '../../models/NotificationModel';
import { formatBytes } from '../../utils/bytes';
import { accountTypeOptions, accountTypeProperties } from '../../models/UserModel';
interface CheckPasswordInput {
password: string;
@ -29,7 +30,7 @@ export function checkPassword(fields: CheckPasswordInput, required: boolean): st
}
function makeUser(isNew: boolean, fields: any): User {
const user: User = {};
let user: User = {};
if ('email' in fields) user.email = fields.email;
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 ('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);
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.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;
});

View File

@ -80,6 +80,7 @@ export interface Config {
userContentBaseUrl: string;
signupEnabled: boolean;
termsEnabled: boolean;
accountTypesEnabled: boolean;
showErrorStackTraces: boolean;
database: DatabaseConfig;
mailer: MailerConfig;

View File

@ -15,6 +15,19 @@
</div>
</div>
{{#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">
<label class="label">Max item size</label>
<div class="control">