1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Server: Check password complexity

This commit is contained in:
Laurent Cozic 2021-07-11 15:04:01 +01:00
parent f6f68e9413
commit 240cb35756
17 changed files with 171 additions and 43 deletions

View File

@ -37,6 +37,9 @@ module.exports = {
// Server admin UI global variables // Server admin UI global variables
'onDocumentReady': 'readonly', 'onDocumentReady': 'readonly',
'setupPasswordStrengthHandler': 'readonly',
'$': 'readonly',
'zxcvbn': 'readonly',
'tinymce': 'readonly', 'tinymce': 'readonly',
}, },

View File

@ -1466,6 +1466,12 @@
"integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==", "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==",
"dev": true "dev": true
}, },
"@types/zxcvbn": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.1.tgz",
"integrity": "sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==",
"dev": true
},
"abab": { "abab": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@ -5375,6 +5381,11 @@
} }
} }
}, },
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -8535,6 +8546,11 @@
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz",
"integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ=="
},
"zxcvbn": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
"integrity": "sha1-KOwXzwl0PtyrBW3dixsGJizHPDA="
} }
} }
} }

View File

@ -29,6 +29,7 @@
"formidable": "^1.2.2", "formidable": "^1.2.2",
"fs-extra": "^8.1.0", "fs-extra": "^8.1.0",
"html-entities": "^1.3.1", "html-entities": "^1.3.1",
"jquery": "^3.6.0",
"knex": "0.95.4", "knex": "0.95.4",
"koa": "^2.8.1", "koa": "^2.8.1",
"markdown-it": "^12.0.4", "markdown-it": "^12.0.4",
@ -44,7 +45,8 @@
"raw-body": "^2.4.1", "raw-body": "^2.4.1",
"sqlite3": "^4.1.0", "sqlite3": "^4.1.0",
"stripe": "^8.150.0", "stripe": "^8.150.0",
"yargs": "^14.0.0" "yargs": "^14.0.0",
"zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@joplin/tools": "^1.0.9", "@joplin/tools": "^1.0.9",
@ -57,6 +59,7 @@
"@types/mustache": "^0.8.32", "@types/mustache": "^0.8.32",
"@types/nodemailer": "^6.4.1", "@types/nodemailer": "^6.4.1",
"@types/yargs": "^13.0.2", "@types/yargs": "^13.0.2",
"@types/zxcvbn": "^4.4.1",
"jest": "^26.6.3", "jest": "^26.6.3",
"jsdom": "^16.4.0", "jsdom": "^16.4.0",
"node-mocks-http": "^1.10.0", "node-mocks-http": "^1.10.0",

View File

@ -6,3 +6,39 @@ function onDocumentReady(fn) {
document.addEventListener('DOMContentLoaded', fn); document.addEventListener('DOMContentLoaded', fn);
} }
} }
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
function setupPasswordStrengthHandler() {
$('#password_strength').hide();
const scoreToLabel = {
0: 'Very weak',
1: 'Weak',
2: 'Medium',
3: 'Strong',
4: 'Very strong',
};
function scoreToClass(score) {
return score < 3 ? 'has-text-danger-dark' : 'has-text-success-dark';
}
function checkPasswordEventHandler() {
const password = $(this).val();
if (!password) {
$('#password_strength').hide();
} else {
$('#password_strength').show();
const result = zxcvbn(password);
let msg = [`<span class="${scoreToClass(result.score)}">Strength: ${scoreToLabel[result.score]}.</span>`];
if (result.feedback.warning) msg.push(result.feedback.warning);
if (result.feedback.suggestions) msg = msg.concat(result.feedback.suggestions);
$('#password_strength').html(msg.join(' '));
}
}
$('#password').keydown(checkPasswordEventHandler);
$('#password').keyup(checkPasswordEventHandler);
$('#password').change(checkPasswordEventHandler);
}

View File

@ -19,11 +19,16 @@ describe('notificationHandler', function() {
test('should check admin password', async function() { test('should check admin password', async function() {
const { session } = await createUserAndSession(1, true); const { session } = await createUserAndSession(1, true);
// The default admin password actually doesn't pass the complexity
// check, so we need to skip validation for testing here. Eventually, a
// better mechanism to set the initial default admin password should
// probably be implemented.
const admin = await models().user().save({ const admin = await models().user().save({
email: defaultAdminEmail, email: defaultAdminEmail,
password: defaultAdminPassword, password: defaultAdminPassword,
is_admin: 1, is_admin: 1,
}); }, { skipValidation: true });
{ {
const ctx = await koaAppContext({ sessionId: session.id }); const ctx = await koaAppContext({ sessionId: session.id });
@ -41,7 +46,7 @@ describe('notificationHandler', function() {
await models().user().save({ await models().user().save({
id: admin.id, id: admin.id,
password: 'changed!', password: 'changed!',
}); }, { skipValidation: true });
const ctx = await koaAppContext({ sessionId: session.id }); const ctx = await koaAppContext({ sessionId: session.id });
await notificationHandler(ctx, koaNext); await notificationHandler(ctx, koaNext);

View File

@ -7,6 +7,7 @@ import { _ } from '@joplin/lib/locale';
import { formatBytes, GB, MB } from '../utils/bytes'; import { formatBytes, GB, MB } from '../utils/bytes';
import { itemIsEncrypted } from '../utils/joplinUtils'; import { itemIsEncrypted } from '../utils/joplinUtils';
import { getMaxItemSize, getMaxTotalItemSize } from './utils/user'; import { getMaxItemSize, getMaxTotalItemSize } from './utils/user';
import * as zxcvbn from 'zxcvbn';
export enum AccountType { export enum AccountType {
Default = 0, Default = 0,
@ -186,9 +187,22 @@ export default class UserModel extends BaseModel<User> {
} }
} }
private validatePassword(password: string) {
const result = zxcvbn(password);
if (result.score < 3) {
let msg: string[] = [result.feedback.warning];
if (result.feedback.suggestions) {
msg = msg.concat(result.feedback.suggestions);
}
throw new ErrorUnprocessableEntity(msg.join(' '));
}
}
protected async validate(object: User, options: ValidateOptions = {}): Promise<User> { protected async validate(object: User, options: ValidateOptions = {}): Promise<User> {
const user: User = await super.validate(object, options); const user: User = await super.validate(object, options);
// Note that we don't validate the password here because it's already
// been hashed by then.
if (options.isNew) { if (options.isNew) {
if (!user.email) throw new ErrorUnprocessableEntity('email must be set'); if (!user.email) throw new ErrorUnprocessableEntity('email must be set');
if (!user.password && !user.must_set_password) throw new ErrorUnprocessableEntity('password must be set'); if (!user.password && !user.must_set_password) throw new ErrorUnprocessableEntity('password must be set');
@ -270,7 +284,10 @@ export default class UserModel extends BaseModel<User> {
public async save(object: User, options: SaveOptions = {}): Promise<User> { public async save(object: User, options: SaveOptions = {}): Promise<User> {
const user = this.formatValues(object); const user = this.formatValues(object);
if (user.password) user.password = auth.hashPassword(user.password); if (user.password) {
if (!options.skipValidation) this.validatePassword(user.password);
user.password = auth.hashPassword(user.password);
}
const isNew = await this.isNew(object, options); const isNew = await this.isNew(object, options);

View File

@ -20,10 +20,10 @@ async function postSession(email: string, password: string): Promise<AppContext>
return context; return context;
} }
describe('api_sessions', function() { describe('api/sessions', function() {
beforeAll(async () => { beforeAll(async () => {
await beforeAllDb('api_sessions'); await beforeAllDb('api/sessions');
}); });
afterAll(async () => { afterAll(async () => {
@ -35,9 +35,9 @@ describe('api_sessions', function() {
}); });
test('should login user', async function() { test('should login user', async function() {
const { user } = await createUserAndSession(1, false); const { user, password } = await createUserAndSession(1, false);
const context = await postSession(user.email, '123456'); const context = await postSession(user.email, password);
expect(context.response.status).toBe(200); expect(context.response.status).toBe(200);
expect(!!context.response.body.id).toBe(true); expect(!!context.response.body.id).toBe(true);

View File

@ -20,6 +20,10 @@ const pathToFileMap: PathToFileMap = {
'css/bulma.min.css': 'node_modules/bulma/css/bulma.min.css', 'css/bulma.min.css': 'node_modules/bulma/css/bulma.min.css',
'css/bulma-prefers-dark.min.css': 'node_modules/bulma-prefers-dark/css/bulma-prefers-dark.min.css', 'css/bulma-prefers-dark.min.css': 'node_modules/bulma-prefers-dark/css/bulma-prefers-dark.min.css',
'css/fontawesome/css/all.min.css': 'node_modules/@fortawesome/fontawesome-free/css/all.min.css', 'css/fontawesome/css/all.min.css': 'node_modules/@fortawesome/fontawesome-free/css/all.min.css',
'js/zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
'js/zxcvbn.js.map': 'node_modules/zxcvbn/dist/zxcvbn.js.map',
'js/jquery.min.js': 'node_modules/jquery/dist/jquery.min.js',
'js/jquery.min.map': 'node_modules/jquery/dist/jquery.min.map',
// Hard-coded for now but it could be made dynamic later on // Hard-coded for now but it could be made dynamic later on
// 'apps/joplin/css/note.css': 'src/apps/joplin/css/note.css', // 'apps/joplin/css/note.css': 'src/apps/joplin/css/note.css',

View File

@ -5,6 +5,7 @@ import { getCanShareFolder, getMaxItemSize } from '../../models/utils/user';
import { MB } from '../../utils/bytes'; import { MB } from '../../utils/bytes';
import { execRequestC } from '../../utils/testing/apiUtils'; import { execRequestC } from '../../utils/testing/apiUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils'; import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils';
import uuidgen from '../../utils/uuidgen';
import { FormUser } from './signup'; import { FormUser } from './signup';
describe('index_signup', function() { describe('index_signup', function() {
@ -22,11 +23,12 @@ describe('index_signup', function() {
}); });
test('should create a new account', async function() { test('should create a new account', async function() {
const password = uuidgen();
const formUser: FormUser = { const formUser: FormUser = {
full_name: 'Toto', full_name: 'Toto',
email: 'toto@example.com', email: 'toto@example.com',
password: 'testing', password: password,
password2: 'testing', password2: password,
}; };
// First confirm that it doesn't work if sign up is disabled // First confirm that it doesn't work if sign up is disabled

View File

@ -4,8 +4,11 @@ import { NotificationKey } from '../../models/NotificationModel';
import { ErrorForbidden } from '../../utils/errors'; import { ErrorForbidden } from '../../utils/errors';
import { execRequest, execRequestC } from '../../utils/testing/apiUtils'; import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils'; import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils';
import uuidgen from '../../utils/uuidgen';
export async function postUser(sessionId: string, email: string, password: string = null, props: any = null): Promise<User> {
password = password === null ? uuidgen() : password;
export async function postUser(sessionId: string, email: string, password: string, props: any = null): Promise<User> {
const context = await koaAppContext({ const context = await koaAppContext({
sessionId: sessionId, sessionId: sessionId,
request: { request: {
@ -75,7 +78,8 @@ describe('index/users', function() {
test('should create a new user', async function() { test('should create a new user', async function() {
const { session } = await createUserAndSession(1, true); const { session } = await createUserAndSession(1, true);
await postUser(session.id, 'test@example.com', '123456', { const password = uuidgen();
await postUser(session.id, 'test@example.com', password, {
max_item_size: '', max_item_size: '',
}); });
const newUser = await models().user().loadByEmail('test@example.com'); const newUser = await models().user().loadByEmail('test@example.com');
@ -91,13 +95,13 @@ describe('index/users', function() {
const userFromModel: User = await userModel.load(newUser.id); const userFromModel: User = await userModel.load(newUser.id);
expect(!!userFromModel.password).toBe(true); expect(!!userFromModel.password).toBe(true);
expect(userFromModel.password === '123456').toBe(false); // Password has been hashed expect(userFromModel.password === password).toBe(false); // Password has been hashed
}); });
test('should create a user with null properties if they are not explicitly set', async function() { test('should create a user with null properties if they are not explicitly set', async function() {
const { session } = await createUserAndSession(1, true); const { session } = await createUserAndSession(1, true);
await postUser(session.id, 'test@example.com', '123456'); await postUser(session.id, 'test@example.com');
const newUser = await models().user().loadByEmail('test@example.com'); const newUser = await models().user().loadByEmail('test@example.com');
expect(newUser.max_item_size).toBe(null); expect(newUser.max_item_size).toBe(null);
@ -121,8 +125,9 @@ describe('index/users', function() {
test('new user should be able to login', async function() { test('new user should be able to login', async function() {
const { session } = await createUserAndSession(1, true); const { session } = await createUserAndSession(1, true);
await postUser(session.id, 'test@example.com', '123456'); const password = uuidgen();
const loggedInUser = await models().user().login('test@example.com', '123456'); await postUser(session.id, 'test@example.com', password);
const loggedInUser = await models().user().login('test@example.com', password);
expect(!!loggedInUser).toBe(true); expect(!!loggedInUser).toBe(true);
expect(loggedInUser.email).toBe('test@example.com'); expect(loggedInUser.email).toBe('test@example.com');
}); });
@ -132,8 +137,9 @@ describe('index/users', function() {
const { session } = await createUserAndSession(1, true); const { session } = await createUserAndSession(1, true);
await postUser(session.id, email, '123456'); const password = uuidgen();
const loggedInUser = await models().user().login(email, '123456'); await postUser(session.id, email, password);
const loggedInUser = await models().user().login(email, password);
expect(!!loggedInUser).toBe(true); expect(!!loggedInUser).toBe(true);
expect(loggedInUser.email).toBe('ilikeuppercaseandspaces@example.com'); expect(loggedInUser.email).toBe('ilikeuppercaseandspaces@example.com');
}); });
@ -143,13 +149,14 @@ describe('index/users', function() {
const userModel = models().user(); const userModel = models().user();
await postUser(session.id, 'test@example.com', '123456'); const password = uuidgen();
await postUser(session.id, 'test@example.com', password);
const beforeUserCount = (await userModel.all()).length; const beforeUserCount = (await userModel.all()).length;
expect(beforeUserCount).toBe(2); expect(beforeUserCount).toBe(2);
try { try {
await postUser(session.id, 'test@example.com', '123456'); await postUser(session.id, 'test@example.com', password);
} catch { } catch {
// Ignore // Ignore
} }
@ -173,8 +180,9 @@ describe('index/users', function() {
const userModel = models().user(); const userModel = models().user();
await patchUser(session.id, { id: user.id, password: 'abcdefgh', password2: 'abcdefgh' }); const password = uuidgen();
const modUser = await userModel.login('user1@localhost', 'abcdefgh'); await patchUser(session.id, { id: user.id, password: password, password2: password });
const modUser = await userModel.login('user1@localhost', password);
expect(!!modUser).toBe(true); expect(!!modUser).toBe(true);
expect(modUser.id).toBe(user.id); expect(modUser.id).toBe(user.id);
}); });
@ -203,7 +211,7 @@ describe('index/users', function() {
email: 'user1@localhost', email: 'user1@localhost',
must_set_password: 1, must_set_password: 1,
email_confirmed: 0, email_confirmed: 0,
password: '123456', password: uuidgen(),
}); });
const { user: user2 } = await createUserAndSession(2); const { user: user2 } = await createUserAndSession(2);
@ -227,9 +235,10 @@ describe('index/users', function() {
// Check that we can't set the password without the token // Check that we can't set the password without the token
{ {
const newPassword = uuidgen();
const context = await execRequestC('', 'POST', path, { const context = await execRequestC('', 'POST', path, {
password: 'newpassword', password: newPassword,
password2: 'newpassword', password2: newPassword,
}); });
const sessionId = context.cookies.get('sessionId'); const sessionId = context.cookies.get('sessionId');
expect(sessionId).toBeFalsy(); expect(sessionId).toBeFalsy();
@ -237,19 +246,22 @@ describe('index/users', function() {
// Check that we can't set the password with someone else's token // Check that we can't set the password with someone else's token
{ {
const newPassword = uuidgen();
const token2 = (await models().token().allByUserId(user2.id))[0].value; const token2 = (await models().token().allByUserId(user2.id))[0].value;
const context = await execRequestC('', 'POST', path, { const context = await execRequestC('', 'POST', path, {
password: 'newpassword', password: newPassword,
password2: 'newpassword', password2: newPassword,
token: token2, token: token2,
}); });
const sessionId = context.cookies.get('sessionId'); const sessionId = context.cookies.get('sessionId');
expect(sessionId).toBeFalsy(); expect(sessionId).toBeFalsy();
} }
const newPassword = uuidgen();
const context = await execRequestC('', 'POST', path, { const context = await execRequestC('', 'POST', path, {
password: 'newpassword', password: newPassword,
password2: 'newpassword', password2: newPassword,
token: token, token: token,
}); });
@ -259,7 +271,7 @@ describe('index/users', function() {
expect(session.user_id).toBe(user1.id); expect(session.user_id).toBe(user1.id);
// Check that the password has been set // Check that the password has been set
const loggedInUser = await models().user().login(user1.email, 'newpassword'); const loggedInUser = await models().user().login(user1.email, newPassword);
expect(loggedInUser.id).toBe(user1.id); expect(loggedInUser.id).toBe(user1.id);
// Check that the email has been verified // Check that the email has been verified
@ -278,7 +290,7 @@ describe('index/users', function() {
email: 'user1@localhost', email: 'user1@localhost',
must_set_password: 0, must_set_password: 0,
email_confirmed: 0, email_confirmed: 0,
password: '123456', password: uuidgen(),
}); });
const email = (await models().email().all()).find(e => e.recipient_id === user1.id); const email = (await models().email().all()).find(e => e.recipient_id === user1.id);
@ -316,7 +328,7 @@ describe('index/users', function() {
await expectHttpError(async () => execRequest(session1.id, 'GET', `users/${admin.id}`), ErrorForbidden.httpCode); await expectHttpError(async () => execRequest(session1.id, 'GET', `users/${admin.id}`), ErrorForbidden.httpCode);
// non-admin user cannot create a new user // non-admin user cannot create a new user
await expectHttpError(async () => postUser(session1.id, 'cantdothat@example.com', '123456'), ErrorForbidden.httpCode); await expectHttpError(async () => postUser(session1.id, 'cantdothat@example.com'), ErrorForbidden.httpCode);
// non-admin user cannot update another user // non-admin user cannot update another user
await expectHttpError(async () => patchUser(session1.id, { id: admin.id, email: 'cantdothateither@example.com' }), ErrorForbidden.httpCode); await expectHttpError(async () => patchUser(session1.id, { id: admin.id, email: 'cantdothateither@example.com' }), ErrorForbidden.httpCode);

View File

@ -143,8 +143,9 @@ 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;
view.content.showResetPasswordButton = !isNew && owner.is_admin; view.content.showResetPasswordButton = !isNew && owner.is_admin;
view.content.canSetEmail = isNew || owner.is_admin;
view.content.canShareFolderOptions = yesNoDefaultOptions(user, 'can_share_folder'); view.content.canShareFolderOptions = yesNoDefaultOptions(user, 'can_share_folder');
view.jsFiles.push('zxcvbn');
if (config().accountTypesEnabled) { if (config().accountTypesEnabled) {
view.content.showAccountTypes = true; view.content.showAccountTypes = true;
@ -178,6 +179,8 @@ router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Er
navbar: false, navbar: false,
}; };
view.jsFiles.push('zxcvbn');
return view; return view;
} else { } else {
await ctx.joplin.models.token().deleteByValue(userId, token); await ctx.joplin.models.token().deleteByValue(userId, token);

View File

@ -44,9 +44,14 @@ export async function createDb(config: DatabaseConfig, options: CreateDbOptions
} }
} }
try {
const db = await connectDb(config); const db = await connectDb(config);
await migrateDb(db); await migrateDb(db);
await disconnectDb(db); await disconnectDb(db);
} catch (error) {
error.message += `: ${config.name}`;
throw error;
}
} }
export async function dropDb(config: DatabaseConfig, options: DropDbOptions = null) { export async function dropDb(config: DatabaseConfig, options: DropDbOptions = null) {

View File

@ -8,5 +8,7 @@ export default function(name: string, title: string): View {
content: {}, content: {},
navbar: true, navbar: true,
title: title, title: title,
jsFiles: [],
cssFiles: [],
}; };
} }

View File

@ -19,6 +19,7 @@ import { FolderEntity, NoteEntity, ResourceEntity } from '@joplin/lib/services/d
import { ModelType } from '@joplin/lib/BaseModel'; import { ModelType } from '@joplin/lib/BaseModel';
import { initializeJoplinUtils } from '../joplinUtils'; import { initializeJoplinUtils } from '../joplinUtils';
import MustacheService from '../../services/MustacheService'; import MustacheService from '../../services/MustacheService';
import uuidgen from '../uuidgen';
// Takes into account the fact that this file will be inside the /dist directory // Takes into account the fact that this file will be inside the /dist directory
// when it runs. // when it runs.
@ -58,6 +59,8 @@ function initGlobalLogger() {
let createdDbPath_: string = null; let createdDbPath_: string = null;
export async function beforeAllDb(unitName: string) { export async function beforeAllDb(unitName: string) {
unitName = unitName.replace(/\//g, '_');
createdDbPath_ = `${packageRootDir}/db-test-${unitName}.sqlite`; createdDbPath_ = `${packageRootDir}/db-test-${unitName}.sqlite`;
const tempDir = `${packageRootDir}/temp/test-${unitName}`; const tempDir = `${packageRootDir}/temp/test-${unitName}`;
@ -216,6 +219,7 @@ export const testAssetDir = `${packageRootDir}/assets/tests`;
interface UserAndSession { interface UserAndSession {
user: User; user: User;
session: Session; session: Session;
password: string;
} }
export function db() { export function db() {
@ -241,9 +245,11 @@ interface CreateUserAndSessionOptions {
} }
export const createUserAndSession = async function(index: number = 1, isAdmin: boolean = false, options: CreateUserAndSessionOptions = null): Promise<UserAndSession> { export const createUserAndSession = async function(index: number = 1, isAdmin: boolean = false, options: CreateUserAndSessionOptions = null): Promise<UserAndSession> {
const password = uuidgen();
options = { options = {
email: `user${index}@localhost`, email: `user${index}@localhost`,
password: '123456', password,
...options, ...options,
}; };
@ -252,7 +258,8 @@ export const createUserAndSession = async function(index: number = 1, isAdmin: b
return { return {
user: await models().user().load(user.id), user: await models().user().load(user.id),
session: session, session,
password,
}; };
}; };

View File

@ -11,9 +11,11 @@
<div class="field"> <div class="field">
<label class="label">Email</label> <label class="label">Email</label>
<div class="control"> <div class="control">
<input class="input" type="email" name="email" value="{{user.email}}" disabled/> <input class="input" type="email" name="email" value="{{user.email}}" {{^canSetEmail}}disabled{{/canSetEmail}}/>
</div> </div>
{{^canSetEmail}}
<p class="help">For security reasons the email cannot currently be changed. To request a change please contact {{global.supportEmail}}</p> <p class="help">For security reasons the email cannot currently be changed. To request a change please contact {{global.supportEmail}}</p>
{{/canSetEmail}}
</div> </div>
{{#global.owner.is_admin}} {{#global.owner.is_admin}}
@ -60,8 +62,9 @@
<div class="field"> <div class="field">
<label class="label">Password</label> <label class="label">Password</label>
<div class="control"> <div class="control">
<input class="input" type="password" name="password" autocomplete="new-password"/> <input id="password" class="input" type="password" name="password" autocomplete="new-password"/>
</div> </div>
<p id="password_strength" class="help"></p>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Repeat password</label> <label class="label">Repeat password</label>
@ -84,12 +87,14 @@
</form> </form>
<script> <script>
onDocumentReady(function() { $(() => {
document.getElementById("user_form").addEventListener('submit', function(event) { document.getElementById("user_form").addEventListener('submit', function(event) {
if (event.submitter.getAttribute('name') === 'delete_button') { if (event.submitter.getAttribute('name') === 'delete_button') {
const ok = confirm('Delete this user?'); const ok = confirm('Delete this user?');
if (!ok) event.preventDefault(); if (!ok) event.preventDefault();
} }
}); });
setupPasswordStrengthHandler();
}); });
</script> </script>

View File

@ -15,7 +15,8 @@
<div class="field"> <div class="field">
<label class="label">Password</label> <label class="label">Password</label>
<div class="control"> <div class="control">
<input class="input" type="password" name="password"/> <input id="password" class="input" type="password" name="password"/>
<p id="password_strength" class="help"></p>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
@ -31,3 +32,9 @@
</form> </form>
</div> </div>
</section> </section>
<script>
$(() => {
setupPasswordStrengthHandler();
});
</script>

View File

@ -9,6 +9,7 @@
{{/global.prefersDarkEnabled}} {{/global.prefersDarkEnabled}}
<link rel="stylesheet" href="{{{global.baseUrl}}}/css/main.css" crossorigin="anonymous"> <link rel="stylesheet" href="{{{global.baseUrl}}}/css/main.css" crossorigin="anonymous">
<link rel="stylesheet" href="{{{global.baseUrl}}}/css/fontawesome/css/all.min.css" crossorigin="anonymous"> <link rel="stylesheet" href="{{{global.baseUrl}}}/css/fontawesome/css/all.min.css" crossorigin="anonymous">
<script src="{{{global.baseUrl}}}/js/jquery.min.js"></script>
<script src="{{{global.baseUrl}}}/js/main.js"></script> <script src="{{{global.baseUrl}}}/js/main.js"></script>
{{#cssFiles}} {{#cssFiles}}
<link rel="stylesheet" href="{{{.}}}" crossorigin="anonymous"> <link rel="stylesheet" href="{{{.}}}" crossorigin="anonymous">