mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Server: Added signup pages
This commit is contained in:
parent
0ef7e98479
commit
41ed66d323
@ -30,7 +30,7 @@ if [ "$RESET_ALL" == "1" ]; then
|
||||
echo "config sync.9.username $USER_EMAIL" >> "$CMD_FILE"
|
||||
echo "config sync.9.password 123456" >> "$CMD_FILE"
|
||||
|
||||
if [ "$1" == "1" ]; then
|
||||
if [ "$USER_NUM" == "1" ]; then
|
||||
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api-joplincloud.local:22300/api/debug
|
||||
|
||||
echo 'mkbook "shared"' >> "$CMD_FILE"
|
||||
|
@ -5,6 +5,7 @@ import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
import config from '../config';
|
||||
import { NotificationKey } from '../models/NotificationModel';
|
||||
|
||||
const logger = Logger.create('notificationHandler');
|
||||
|
||||
@ -17,21 +18,12 @@ async function handleChangeAdminPasswordNotification(ctx: AppContext) {
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
ctx.owner.id,
|
||||
'change_admin_password',
|
||||
NotificationKey.ChangeAdminPassword,
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', ctx.models.user().profileUrl())
|
||||
);
|
||||
} else {
|
||||
await notificationModel.markAsRead(ctx.owner.id, 'change_admin_password');
|
||||
}
|
||||
|
||||
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
ctx.owner.id,
|
||||
'using_sqlite_in_prod',
|
||||
NotificationLevel.Important,
|
||||
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
|
||||
);
|
||||
await notificationModel.markAsRead(ctx.owner.id, NotificationKey.ChangeAdminPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,9 +35,7 @@ async function handleSqliteInProdNotification(ctx: AppContext) {
|
||||
if (config().database.client === 'sqlite3' && ctx.env === 'prod') {
|
||||
await notificationModel.add(
|
||||
ctx.owner.id,
|
||||
'using_sqlite_in_prod',
|
||||
NotificationLevel.Important,
|
||||
'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.'
|
||||
NotificationKey.UsingSqliteInProd
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.alterTable('users', function(table: Knex.CreateTableBuilder) {
|
||||
table.integer('account_type').defaultTo(0).notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(_db: DbConnection): Promise<any> {
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllTests, beforeEachDb, models, expectThrow } from '../utils/testing/testUtils';
|
||||
import { Notification, NotificationLevel } from '../db';
|
||||
import { NotificationKey } from './NotificationModel';
|
||||
|
||||
describe('NotificationModel', function() {
|
||||
|
||||
@ -16,15 +17,15 @@ describe('NotificationModel', function() {
|
||||
});
|
||||
|
||||
test('should require a user to create the notification', async function() {
|
||||
await expectThrow(async () => models().notification().add('', 'test', NotificationLevel.Normal, 'test'));
|
||||
await expectThrow(async () => models().notification().add('', NotificationKey.ConfirmEmail, NotificationLevel.Normal, NotificationKey.ConfirmEmail));
|
||||
});
|
||||
|
||||
test('should create a notification', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification();
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
const n: Notification = await model.loadByKey(user.id, 'test');
|
||||
expect(n.key).toBe('test');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
const n: Notification = await model.loadByKey(user.id, NotificationKey.ConfirmEmail);
|
||||
expect(n.key).toBe(NotificationKey.ConfirmEmail);
|
||||
expect(n.message).toBe('testing');
|
||||
expect(n.level).toBe(NotificationLevel.Important);
|
||||
});
|
||||
@ -32,18 +33,18 @@ describe('NotificationModel', function() {
|
||||
test('should create only one notification per key', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification();
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
expect((await model.all()).length).toBe(1);
|
||||
});
|
||||
|
||||
test('should mark a notification as read', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification();
|
||||
await model.add(user.id, 'test', NotificationLevel.Important, 'testing');
|
||||
expect((await model.loadByKey(user.id, 'test')).read).toBe(0);
|
||||
await model.markAsRead(user.id, 'test');
|
||||
expect((await model.loadByKey(user.id, 'test')).read).toBe(1);
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Important, 'testing');
|
||||
expect((await model.loadByKey(user.id, NotificationKey.ConfirmEmail)).read).toBe(0);
|
||||
await model.markAsRead(user.id, NotificationKey.ConfirmEmail);
|
||||
expect((await model.loadByKey(user.id, NotificationKey.ConfirmEmail)).read).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -2,6 +2,38 @@ import { Notification, NotificationLevel, Uuid } from '../db';
|
||||
import { ErrorUnprocessableEntity } from '../utils/errors';
|
||||
import BaseModel, { ValidateOptions } from './BaseModel';
|
||||
|
||||
export enum NotificationKey {
|
||||
ConfirmEmail = 'confirmEmail',
|
||||
PasswordSet = 'passwordSet',
|
||||
EmailConfirmed = 'emailConfirmed',
|
||||
ChangeAdminPassword = 'change_admin_password',
|
||||
UsingSqliteInProd = 'using_sqlite_in_prod',
|
||||
}
|
||||
|
||||
interface NotificationType {
|
||||
level: NotificationLevel;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const notificationTypes: Record<string, NotificationType> = {
|
||||
[NotificationKey.ConfirmEmail]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'Welcome to Joplin Cloud! An email has been sent to you containing an activation link to complete your registration.',
|
||||
},
|
||||
[NotificationKey.EmailConfirmed]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'You email has been confirmed',
|
||||
},
|
||||
[NotificationKey.PasswordSet]: {
|
||||
level: NotificationLevel.Normal,
|
||||
message: 'Welcome to Joplin Cloud! Your password has been set successfully.',
|
||||
},
|
||||
[NotificationKey.UsingSqliteInProd]: {
|
||||
level: NotificationLevel.Important,
|
||||
message: 'The server is currently using SQLite3 as a database. It is not recommended in production as it is slow and can cause locking issues. Please see the README for information on how to change it.',
|
||||
},
|
||||
};
|
||||
|
||||
export default class NotificationModel extends BaseModel<Notification> {
|
||||
|
||||
protected get tableName(): string {
|
||||
@ -13,13 +45,32 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
return super.validate(notification, options);
|
||||
}
|
||||
|
||||
public async add(userId: Uuid, key: string, level: NotificationLevel, message: string): Promise<Notification> {
|
||||
public async add(userId: Uuid, key: NotificationKey, level: NotificationLevel = null, message: string = null): Promise<Notification> {
|
||||
const n: Notification = await this.loadByKey(userId, key);
|
||||
if (n) return n;
|
||||
|
||||
const type = notificationTypes[key];
|
||||
|
||||
if (level === null) {
|
||||
if (type?.level) {
|
||||
level = type.level;
|
||||
} else {
|
||||
throw new Error('Missing notification level');
|
||||
}
|
||||
}
|
||||
|
||||
if (message === null) {
|
||||
if (type?.message) {
|
||||
message = type.message;
|
||||
} else {
|
||||
throw new Error('Missing notification message');
|
||||
}
|
||||
}
|
||||
|
||||
return this.save({ key, message, level, owner_id: userId });
|
||||
}
|
||||
|
||||
public async markAsRead(userId: Uuid, key: string): Promise<void> {
|
||||
public async markAsRead(userId: Uuid, key: NotificationKey): Promise<void> {
|
||||
const n = await this.loadByKey(userId, key);
|
||||
if (!n) return;
|
||||
|
||||
@ -29,7 +80,7 @@ export default class NotificationModel extends BaseModel<Notification> {
|
||||
.andWhere('owner_id', '=', userId);
|
||||
}
|
||||
|
||||
public loadByKey(userId: Uuid, key: string): Promise<Notification> {
|
||||
public loadByKey(userId: Uuid, key: NotificationKey): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('key', '=', key)
|
||||
|
@ -181,15 +181,15 @@ export default class UserModel extends BaseModel<User> {
|
||||
|
||||
if (isNew) {
|
||||
const validationToken = await this.models().token().generate(savedUser.id);
|
||||
const validationUrl = encodeURI(this.confirmUrl(savedUser.id, validationToken));
|
||||
const confirmUrl = encodeURI(this.confirmUrl(savedUser.id, validationToken));
|
||||
|
||||
await this.models().email().push({
|
||||
sender_id: EmailSender.NoReply,
|
||||
recipient_id: savedUser.id,
|
||||
recipient_email: savedUser.email,
|
||||
recipient_name: savedUser.full_name || '',
|
||||
subject: 'Verify your email',
|
||||
body: `Click this: ${validationUrl}`,
|
||||
subject: 'Please setup your Joplin account',
|
||||
body: `Your new Joplin account has been created!\n\nPlease click on the following link to complete the creation of your account:\n\n${confirmUrl}`,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,8 @@ describe('api_users', function() {
|
||||
expect(savedUser.email).toBe('toto@example.com');
|
||||
expect(savedUser.can_share).toBe(0);
|
||||
expect(savedUser.max_item_size).toBe(1000);
|
||||
expect(savedUser.email_confirmed).toBe(0);
|
||||
expect(savedUser.must_set_password).toBe(1);
|
||||
});
|
||||
|
||||
test('should patch a user', async function() {
|
||||
|
@ -34,6 +34,7 @@ router.post('api/users', async (_path: SubPath, ctx: AppContext) => {
|
||||
// set it after clicking on the confirmation link.
|
||||
user.password = uuidgen();
|
||||
user.must_set_password = 1;
|
||||
user.email_confirmed = 0;
|
||||
const output = await ctx.models.user().save(user);
|
||||
return ctx.models.user().toApiOutput(output);
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NotificationLevel } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils';
|
||||
|
||||
describe('index_notification', function() {
|
||||
@ -21,9 +22,9 @@ describe('index_notification', function() {
|
||||
|
||||
const model = models().notification();
|
||||
|
||||
await model.add(user.id, 'my_notification', NotificationLevel.Normal, 'testing notification');
|
||||
await model.add(user.id, NotificationKey.ConfirmEmail, NotificationLevel.Normal, 'testing notification');
|
||||
|
||||
const notification = await model.loadByKey(user.id, 'my_notification');
|
||||
const notification = await model.loadByKey(user.id, NotificationKey.ConfirmEmail);
|
||||
|
||||
expect(notification.read).toBe(0);
|
||||
|
||||
@ -40,7 +41,7 @@ describe('index_notification', function() {
|
||||
|
||||
await routeHandler(context);
|
||||
|
||||
expect((await model.loadByKey(user.id, 'my_notification')).read).toBe(1);
|
||||
expect((await model.loadByKey(user.id, NotificationKey.ConfirmEmail)).read).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
42
packages/server/src/routes/index/signup.test.ts
Normal file
42
packages/server/src/routes/index/signup.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/testing/testUtils';
|
||||
|
||||
describe('index_signup', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('index_signup');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllTests();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should create a new account', async function() {
|
||||
const context = await execRequestC('', 'POST', 'signup', {
|
||||
full_name: 'Toto',
|
||||
email: 'toto@example.com',
|
||||
password: 'testing',
|
||||
password2: 'testing',
|
||||
});
|
||||
|
||||
// Check that the user has been created
|
||||
const user = await models().user().loadByEmail('toto@example.com');
|
||||
expect(user).toBeTruthy();
|
||||
expect(user.email_confirmed).toBe(0);
|
||||
|
||||
// Check that the user is logged in
|
||||
const session = await models().session().load(context.cookies.get('sessionId'));
|
||||
expect(session.user_id).toBe(user.id);
|
||||
|
||||
// Check that the notification has been created
|
||||
const notifications = await models().notification().allUnreadByUserId(user.id);
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications[0].key).toBe(NotificationKey.ConfirmEmail);
|
||||
});
|
||||
|
||||
});
|
57
packages/server/src/routes/index/signup.ts
Normal file
57
packages/server/src/routes/index/signup.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { SubPath, redirect } from '../../utils/routeUtils';
|
||||
import Router from '../../utils/Router';
|
||||
import { RouteType } from '../../utils/types';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields } from '../../utils/requestUtils';
|
||||
import config from '../../config';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import { checkPassword } from './users';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
|
||||
function makeView(error: Error = null): View {
|
||||
const view = defaultView('signup');
|
||||
view.content.error = error;
|
||||
view.content.postUrl = `${config().baseUrl}/signup`;
|
||||
view.navbar = false;
|
||||
return view;
|
||||
}
|
||||
|
||||
interface FormUser {
|
||||
full_name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
password2: string;
|
||||
}
|
||||
|
||||
const router: Router = new Router(RouteType.Web);
|
||||
|
||||
router.public = true;
|
||||
|
||||
router.get('signup', async (_path: SubPath, _ctx: AppContext) => {
|
||||
return makeView();
|
||||
});
|
||||
|
||||
router.post('signup', async (_path: SubPath, ctx: AppContext) => {
|
||||
try {
|
||||
const formUser = await bodyFields<FormUser>(ctx.req);
|
||||
const password = checkPassword(formUser, true);
|
||||
|
||||
const user = await ctx.models.user().save({
|
||||
email: formUser.email,
|
||||
full_name: formUser.full_name,
|
||||
password,
|
||||
});
|
||||
|
||||
const session = await ctx.models.session().createUserSession(user.id);
|
||||
ctx.cookies.set('sessionId', session.id);
|
||||
|
||||
await ctx.models.notification().add(user.id, NotificationKey.ConfirmEmail);
|
||||
|
||||
return redirect(ctx, `${config().baseUrl}/home`);
|
||||
} catch (error) {
|
||||
return makeView(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
@ -1,5 +1,6 @@
|
||||
import { User } from '../../db';
|
||||
import routeHandler from '../../middleware/routeHandler';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import { execRequest, execRequestC } from '../../utils/testing/apiUtils';
|
||||
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, createUserAndSession, models, parseHtml, checkContextError, expectHttpError } from '../../utils/testing/testUtils';
|
||||
@ -154,13 +155,29 @@ describe('index_users', function() {
|
||||
});
|
||||
|
||||
test('should allow user to set a password for new accounts', async function() {
|
||||
const { user: user1 } = await createUserAndSession(1);
|
||||
let user1 = await models().user().save({
|
||||
email: 'user1@localhost',
|
||||
must_set_password: 1,
|
||||
email_confirmed: 0,
|
||||
password: '123456',
|
||||
});
|
||||
|
||||
const { user: user2 } = await createUserAndSession(2);
|
||||
const email = (await models().email().all()).find(e => e.recipient_id === user1.id);
|
||||
const matches = email.body.match(/\/(users\/.*)(\?token=)(.{32})/);
|
||||
const path = matches[1];
|
||||
const token = matches[3];
|
||||
|
||||
// Check that the email at first is not confirmed
|
||||
// expect(user1.email_confirmed).toBe(0);
|
||||
// expect(user1.must_set_password).toBe(1);
|
||||
|
||||
await execRequest('', 'GET', path, null, { query: { token } });
|
||||
|
||||
// As soon as the confirmation page is opened, we know the email is valid
|
||||
user1 = await models().user().load(user1.id);
|
||||
expect(user1.email_confirmed).toBe(1);
|
||||
|
||||
// Check that the token is valid
|
||||
expect(await models().token().isValid(user1.id, token)).toBe(true);
|
||||
|
||||
@ -201,6 +218,9 @@ describe('index_users', function() {
|
||||
const loggedInUser = await models().user().login(user1.email, 'newpassword');
|
||||
expect(loggedInUser.id).toBe(user1.id);
|
||||
|
||||
// Check that the email has been verified
|
||||
expect(user1.email_confirmed).toBe(1);
|
||||
|
||||
// Check that the token has been cleared
|
||||
expect(await models().token().isValid(user1.id, token)).toBe(false);
|
||||
|
||||
@ -209,19 +229,37 @@ describe('index_users', function() {
|
||||
expect(notification.key).toBe('passwordSet');
|
||||
});
|
||||
|
||||
// test('should handle invalid email validation', async function() {
|
||||
// await createUserAndSession(1);
|
||||
// const email = (await models().email().all())[0];
|
||||
// const matches = email.body.match(/\/(users\/.*)(\?token=)(.{32})/);
|
||||
// const path = matches[1];
|
||||
// const token = matches[3];
|
||||
test('should allow user to verify their email', async function() {
|
||||
let user1 = await models().user().save({
|
||||
email: 'user1@localhost',
|
||||
must_set_password: 0,
|
||||
email_confirmed: 0,
|
||||
password: '123456',
|
||||
});
|
||||
|
||||
// // Valid path but invalid token
|
||||
// await expectHttpError(async () => execRequest(null, 'GET', path, null, { query: { token: 'invalid' } }), ErrorNotFound.httpCode);
|
||||
const email = (await models().email().all()).find(e => e.recipient_id === user1.id);
|
||||
const matches = email.body.match(/\/(users\/.*)(\?token=)(.{32})/);
|
||||
const path = matches[1];
|
||||
const token = matches[3];
|
||||
|
||||
// // Valid token but invalid path
|
||||
// await expectHttpError(async () => execRequest(null, 'GET', 'users/abcd1234/confirm', null, { query: { token } }), ErrorNotFound.httpCode);
|
||||
// });
|
||||
const context = await execRequestC('', 'GET', path, null, { query: { token } });
|
||||
|
||||
user1 = await models().user().load(user1.id);
|
||||
|
||||
// Check that the user has been logged in
|
||||
const sessionId = context.cookies.get('sessionId');
|
||||
expect(sessionId).toBeFalsy();
|
||||
|
||||
// Check that the email has been verified
|
||||
expect(user1.email_confirmed).toBe(1);
|
||||
|
||||
// Check that the token has been cleared
|
||||
expect(await models().token().isValid(user1.id, token)).toBe(false);
|
||||
|
||||
// Check that a notification has been created
|
||||
const notification = (await models().notification().all())[0];
|
||||
expect(notification.key).toBe(NotificationKey.EmailConfirmed);
|
||||
});
|
||||
|
||||
test('should apply ACL', async function() {
|
||||
const { user: admin, session: adminSession } = await createUserAndSession(1, true);
|
||||
|
@ -4,14 +4,20 @@ import { RouteType } from '../../utils/types';
|
||||
import { AppContext, HttpMethod } from '../../utils/types';
|
||||
import { bodyFields, formParse } from '../../utils/requestUtils';
|
||||
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
|
||||
import { NotificationLevel, User } from '../../db';
|
||||
import { User } from '../../db';
|
||||
import config from '../../config';
|
||||
import { View } from '../../services/MustacheService';
|
||||
import defaultView from '../../utils/defaultView';
|
||||
import { AclAction } from '../../models/BaseModel';
|
||||
import { NotificationKey } from '../../models/NotificationModel';
|
||||
const prettyBytes = require('pretty-bytes');
|
||||
|
||||
function checkPassword(fields: SetPasswordFormData, required: boolean): string {
|
||||
interface CheckPasswordInput {
|
||||
password: string;
|
||||
password2: string;
|
||||
}
|
||||
|
||||
export function checkPassword(fields: CheckPasswordInput, required: boolean): string {
|
||||
if (fields.password) {
|
||||
if (fields.password !== fields.password2) throw new ErrorUnprocessableEntity('Passwords do not match');
|
||||
return fields.password;
|
||||
@ -113,18 +119,29 @@ router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Er
|
||||
|
||||
const user = await ctx.models.user().load(userId);
|
||||
|
||||
const view: View = {
|
||||
...defaultView('users/confirm'),
|
||||
content: {
|
||||
user,
|
||||
error,
|
||||
token,
|
||||
postUrl: ctx.models.user().confirmUrl(userId, token),
|
||||
},
|
||||
navbar: false,
|
||||
};
|
||||
if (user.must_set_password) {
|
||||
const view: View = {
|
||||
...defaultView('users/confirm'),
|
||||
content: {
|
||||
user,
|
||||
error,
|
||||
token,
|
||||
postUrl: ctx.models.user().confirmUrl(userId, token),
|
||||
},
|
||||
navbar: false,
|
||||
};
|
||||
|
||||
return view;
|
||||
return view;
|
||||
} else {
|
||||
await ctx.models.token().deleteByValue(userId, token);
|
||||
await ctx.models.notification().add(userId, NotificationKey.EmailConfirmed);
|
||||
|
||||
if (ctx.owner) {
|
||||
return redirect(ctx, `${config().baseUrl}/home`);
|
||||
} else {
|
||||
return redirect(ctx, `${config().baseUrl}/login`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interface SetPasswordFormData {
|
||||
@ -142,13 +159,13 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => {
|
||||
|
||||
const password = checkPassword(fields, true);
|
||||
|
||||
await ctx.models.user().save({ id: userId, password });
|
||||
await ctx.models.user().save({ id: userId, password, must_set_password: 0 });
|
||||
await ctx.models.token().deleteByValue(userId, fields.token);
|
||||
|
||||
const session = await ctx.models.session().createUserSession(userId);
|
||||
ctx.cookies.set('sessionId', session.id);
|
||||
|
||||
await ctx.models.notification().add(userId, 'passwordSet', NotificationLevel.Normal, 'Welcome to Joplin Cloud! Your password has been set successfully.');
|
||||
await ctx.models.notification().add(userId, NotificationKey.PasswordSet);
|
||||
|
||||
return redirect(ctx, `${config().baseUrl}/home`);
|
||||
} catch (error) {
|
||||
|
@ -15,6 +15,7 @@ import indexItems from './index/items';
|
||||
import indexLogin from './index/login';
|
||||
import indexLogout from './index/logout';
|
||||
import indexNotifications from './index/notifications';
|
||||
import indexSignup from './index/signup';
|
||||
import indexShares from './index/shares';
|
||||
import indexUsers from './index/users';
|
||||
|
||||
@ -36,6 +37,7 @@ const routes: Routers = {
|
||||
'login': indexLogin,
|
||||
'logout': indexLogout,
|
||||
'notifications': indexNotifications,
|
||||
'signup': indexSignup,
|
||||
'shares': indexShares,
|
||||
'users': indexUsers,
|
||||
|
||||
|
30
packages/server/src/views/index/signup.mustache
Normal file
30
packages/server/src/views/index/signup.mustache
Normal file
@ -0,0 +1,30 @@
|
||||
{{> errorBanner}}
|
||||
<form action="{{{postUrl}}}" method="POST">
|
||||
<div class="field">
|
||||
<label class="label">Full name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="full_name" value="{{user.full_name}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Email</label>
|
||||
<div class="control">
|
||||
<input class="input" type="email" name="email" value="{{user.email}}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Password</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" autocomplete="new-password"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Repeat password</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password2" autocomplete="new-password"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<input type="submit" name="post_button" class="button is-primary" value="Sign up" />
|
||||
</div>
|
||||
</form>
|
Loading…
Reference in New Issue
Block a user