api: Registration, password recovery, verification flows
This commit is contained in:
parent
d1d7921808
commit
45bbde1037
|
@ -1,4 +1,4 @@
|
|||
const validateRequest = (schema) => (req, res, next) => {
|
||||
const validateRequest = (schema, property = 'body') => (req, res, next) => {
|
||||
console.log('validateRequest');
|
||||
|
||||
const options = {
|
||||
|
@ -6,12 +6,12 @@ const validateRequest = (schema) => (req, res, next) => {
|
|||
allowUnknown: true, // ignore unknown props
|
||||
stripUnknown: true, // remove unknown props
|
||||
};
|
||||
const { error, value } = schema.validate(req.body, options);
|
||||
const { error, value } = schema.validate(req[property], options);
|
||||
if (error) {
|
||||
console.log('error: ', error);
|
||||
next(`Validation error: ${error.details.map((x) => x.message).join(', ')}`);
|
||||
} else {
|
||||
req.body = value;
|
||||
req[property] = value;
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -100,11 +100,11 @@ function randomTokenString() {
|
|||
async function sendVerificationEmail(account, origin) {
|
||||
let message;
|
||||
if (origin) {
|
||||
const verifyUrl = `${origin}/account/verify-email?token=${account.verificationToken}`;
|
||||
const verifyUrl = `${origin}/verify-email?token=${account.verificationToken}`;
|
||||
message = `<p>Please click the below link to verify your email address:</p>
|
||||
<p><a href="${verifyUrl}">${verifyUrl}</a></p>`;
|
||||
} else {
|
||||
message = `<p>Please use the below token to verify your email address with the <code>/account/verify-email</code> api route:</p>
|
||||
message = `<p>Please use the below token to verify your email address with the <code>/verify-email</code> api route:</p>
|
||||
<p><code>${account.verificationToken}</code></p>`;
|
||||
}
|
||||
|
||||
|
@ -120,9 +120,9 @@ async function sendVerificationEmail(account, origin) {
|
|||
async function sendAlreadyRegisteredEmail(email, origin) {
|
||||
let message;
|
||||
if (origin) {
|
||||
message = `<p>If you don't know your password please visit the <a href="${origin}/account/forgot-password">forgot password</a> page.</p>`;
|
||||
message = `<p>If you don't know your password please visit the <a href="${origin}/forgot-password">forgot password</a> page.</p>`;
|
||||
} else {
|
||||
message = `<p>If you don't know your password you can reset it via the <code>/account/forgot-password</code> api route.</p>`;
|
||||
message = `<p>If you don't know your password you can reset it via the <code>/forgot-password</code> api route.</p>`;
|
||||
}
|
||||
|
||||
await sendEmail({
|
||||
|
@ -137,11 +137,11 @@ async function sendAlreadyRegisteredEmail(email, origin) {
|
|||
async function sendPasswordResetEmail(account, origin) {
|
||||
let message;
|
||||
if (origin) {
|
||||
const resetUrl = `${origin}/account/reset-password?token=${account.resetToken.token}`;
|
||||
const resetUrl = `${origin}/reset-password?token=${account.resetToken.token}`;
|
||||
message = `<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
|
||||
<p><a href="${resetUrl}">${resetUrl}</a></p>`;
|
||||
} else {
|
||||
message = `<p>Please use the below token to reset your password with the <code>/account/reset-password</code> api route:</p>
|
||||
message = `<p>Please use the below token to reset your password with the <code>/reset-password</code> api route:</p>
|
||||
<p><code>${account.resetToken.token}</code></p>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,13 @@ async function loginWithPassword(email, password, done) {
|
|||
try {
|
||||
const user = await User.findOne({ email: email });
|
||||
if (!user || !user.validPassword(password)) {
|
||||
return done(null, false, { errors: { 'email or password': 'is invalid' } });
|
||||
return done(new Error('invalid credentials'), false);
|
||||
}
|
||||
|
||||
// Regardless of whether login is required, if you're logged in as an
|
||||
// unverified user, produce an error.
|
||||
if (user.needsEmailValidation) {
|
||||
return done(new Error('email not verified'), false);
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
|
@ -117,7 +123,7 @@ passport.use(
|
|||
const refreshToken = await RefreshToken.findOne({ token }).populate('user');
|
||||
if (refreshToken && refreshToken.user) {
|
||||
// TODO: scope
|
||||
return done(null, user, { scope: 'auth.refresh' });
|
||||
return done(null, refreshToken.user, { scope: 'auth.refresh' });
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
|
@ -181,7 +187,6 @@ function createMiddleware(strategies, required = true, session = false) {
|
|||
}
|
||||
|
||||
req.user = user;
|
||||
req.authInfo = info;
|
||||
req.scope = (info && info.scope) || '*';
|
||||
|
||||
return next();
|
||||
|
|
|
@ -16,8 +16,8 @@ const app = express();
|
|||
app.use(cors());
|
||||
|
||||
// Express configuration
|
||||
app.set('views', './views')
|
||||
app.set('view engine', 'pug')
|
||||
app.set('views', './views');
|
||||
app.set('view engine', 'pug');
|
||||
|
||||
// Normal express config defaults
|
||||
app.use(require('morgan')('dev'));
|
||||
|
@ -27,7 +27,7 @@ app.use(bodyParser.urlencoded({ limit: '50mb', extended: false }));
|
|||
app.use(require('method-override')());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use(session({ secret: 'obsobs', cookie: { maxAge: 60000 }, resave: false, saveUninitialized: false }));
|
||||
app.use(session({ secret: 'obsobs', cookie: { maxAge: 10 * 60 * 1000 }, resave: false, saveUninitialized: false }));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ const { URL } = require('url');
|
|||
const { createChallenge } = require('pkce');
|
||||
|
||||
const { AuthorizationCode, AccessToken, RefreshToken, Client } = require('../models');
|
||||
const auth = require('../config/passport');
|
||||
const wrapRoute = require('../_helpers/wrapRoute');
|
||||
|
||||
// Check whether the "bigScope" fully includes the "smallScope".
|
||||
|
@ -45,9 +46,30 @@ function isValidScope(scope) {
|
|||
return scope === '*' || scopeIncludes(scope, ALL_SCOPE_NAMES.join(' '));
|
||||
}
|
||||
|
||||
router.use((req, res, next) => {
|
||||
res.locals.user = req.user;
|
||||
next();
|
||||
});
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
passport.authenticate('usernameAndPasswordSession'),
|
||||
passport.authenticate('usernameAndPasswordSession', { session: true }),
|
||||
(err, req, res, next) => {
|
||||
if (!err) {
|
||||
next();
|
||||
}
|
||||
|
||||
if (err.message === 'invalid credentials') {
|
||||
return res.render('login', { badCredentials: true });
|
||||
}
|
||||
|
||||
let description = 'Unknown error while processing your login.';
|
||||
if (err.message === 'email not verified') {
|
||||
description = 'Your account is not yet verified, please check your email or start the password recovery.';
|
||||
}
|
||||
|
||||
return res.render('message', { type: 'error', title: 'Login failed', description });
|
||||
},
|
||||
wrapRoute((req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.redirect('/login');
|
||||
|
@ -69,10 +91,21 @@ router.get(
|
|||
return res.render('message', { type: 'success', title: 'You are already logged in.' });
|
||||
}
|
||||
|
||||
res.render('login');
|
||||
return res.render('login');
|
||||
}),
|
||||
);
|
||||
|
||||
router
|
||||
.route('/logout')
|
||||
.post(
|
||||
auth.usernameAndPasswordSession,
|
||||
wrapRoute(async (req, res) => {
|
||||
req.logout();
|
||||
return res.redirect('/login');
|
||||
}),
|
||||
)
|
||||
.get((req, res) => res.render('logout'));
|
||||
|
||||
const isIp = (ip) =>
|
||||
typeof ip === 'string' &&
|
||||
/^([0-9]{1,3}\.)[0-9]{1,3}$/.test(ip) &&
|
||||
|
@ -115,7 +148,6 @@ router.get(
|
|||
passport.authenticate('session'),
|
||||
wrapRoute(async (req, res) => {
|
||||
if (!req.user) {
|
||||
console.log(req);
|
||||
req.session.next = req.url;
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
@ -208,7 +240,6 @@ router.get(
|
|||
|
||||
res.render('authorize', { clientTitle: client.title, scope, redirectUri });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(400).json({ error: 'invalid_request', error_description: 'unknown error' });
|
||||
}
|
||||
}),
|
||||
|
@ -385,3 +416,104 @@ router.get(
|
|||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
const accountService = require('../accounts/account.service');
|
||||
const validateRequest = require('../_middleware/validate-request');
|
||||
const Joi = require('joi');
|
||||
|
||||
router
|
||||
.route('/register')
|
||||
.post(
|
||||
validateRequest(
|
||||
Joi.object({
|
||||
username: Joi.string().required(),
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
|
||||
}),
|
||||
),
|
||||
wrapRoute(async (req, res) => {
|
||||
await accountService.register(req.body, req.get('origin'));
|
||||
|
||||
return res.render('message', {
|
||||
type: 'success',
|
||||
title: 'Registration successful',
|
||||
description: 'Please check your email for verification instructions.',
|
||||
});
|
||||
}),
|
||||
)
|
||||
.get((req, res) => res.render('register'));
|
||||
|
||||
router.get(
|
||||
'/verify-email',
|
||||
validateRequest(
|
||||
Joi.object({
|
||||
token: Joi.string().required(),
|
||||
}),
|
||||
'query',
|
||||
),
|
||||
wrapRoute(async (req, res) => {
|
||||
await accountService.verifyEmail(req.query);
|
||||
return res.render('message', {
|
||||
type: 'success',
|
||||
title: 'Verification successful',
|
||||
description: 'You can now log in.',
|
||||
showLoginButton: true,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
router
|
||||
.route('/forgot-password')
|
||||
.post(
|
||||
validateRequest(
|
||||
Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
}),
|
||||
),
|
||||
wrapRoute(async (req, res) => {
|
||||
await accountService.forgotPassword(req.body, req.get('origin'));
|
||||
res.render('message', {
|
||||
type: 'success',
|
||||
title: 'Recovery mail sent',
|
||||
description: 'Please check your inbox for password recovery instructions.',
|
||||
});
|
||||
}),
|
||||
)
|
||||
.get((req, res) => res.render('forgot-password'));
|
||||
|
||||
router
|
||||
.route('/reset-password')
|
||||
.post(
|
||||
validateRequest(
|
||||
Joi.object({
|
||||
token: Joi.string().required(),
|
||||
password: Joi.string().min(6).required(),
|
||||
confirmPassword: Joi.string().valid(Joi.ref('password')).required(),
|
||||
}),
|
||||
),
|
||||
wrapRoute(async (req, res) => {
|
||||
await accountService.resetPassword(req.body);
|
||||
return res.render('message', {
|
||||
type: 'success',
|
||||
title: 'Password reset successful',
|
||||
description: 'You can now log in.',
|
||||
showLoginButton: true,
|
||||
});
|
||||
}),
|
||||
)
|
||||
.get(
|
||||
validateRequest(
|
||||
Joi.object({
|
||||
token: Joi.string().required(),
|
||||
}),
|
||||
'query',
|
||||
),
|
||||
wrapRoute(async (req, res) => {
|
||||
const { token } = req.query;
|
||||
await accountService.validateResetToken({ token });
|
||||
res.render('reset-password', { token });
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
12
api/views/forgot-password.pug
Normal file
12
api/views/forgot-password.pug
Normal file
|
@ -0,0 +1,12 @@
|
|||
extends layout.pug
|
||||
|
||||
block title
|
||||
| Reset Password
|
||||
|
||||
block content
|
||||
form(method="post")
|
||||
fieldset
|
||||
label(for="email") E-Mail Address
|
||||
input(id="email", name="email")
|
||||
|
||||
button(type="submit") Send recovery mail
|
|
@ -97,8 +97,45 @@ html
|
|||
background: #83d283;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
nav ul li {
|
||||
flex: 1 1 0;
|
||||
text-align: center;
|
||||
}
|
||||
nav ul li a {
|
||||
color: #999;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav ul li a:hover {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
nav header {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
body
|
||||
main
|
||||
nav
|
||||
header OpenBikeSensor Account Pages
|
||||
ul
|
||||
li: a(href="/") Back to Portal
|
||||
if !user
|
||||
li: a(href="/login") Login
|
||||
li: a(href="/register") Register
|
||||
if user
|
||||
li: a(href="/logout") Logout
|
||||
header: h1
|
||||
block title
|
||||
block content
|
||||
|
|
|
@ -13,4 +13,8 @@ block content
|
|||
label(for="password") Password
|
||||
input(name="password", type="password")
|
||||
|
||||
button(type="submit") Login
|
||||
if badCredentials
|
||||
p.message.error Invalid login credentials, please try again.
|
||||
|
||||
button(type="submit", style="margin-right: 2rem") Login
|
||||
a(href="/forgot-password") I forgot my password
|
||||
|
|
8
api/views/logout.pug
Normal file
8
api/views/logout.pug
Normal file
|
@ -0,0 +1,8 @@
|
|||
extends layout.pug
|
||||
|
||||
block title
|
||||
| Logout
|
||||
|
||||
block content
|
||||
form(method="post", action="/logout")
|
||||
button(type="submit", class="red") Log out now
|
|
@ -6,3 +6,6 @@ block title
|
|||
block content
|
||||
if description
|
||||
div(class="message " + type)= description
|
||||
|
||||
if showLoginButton
|
||||
p: a(href="/login") Go to login
|
||||
|
|
25
api/views/register.pug
Normal file
25
api/views/register.pug
Normal file
|
@ -0,0 +1,25 @@
|
|||
extends layout.pug
|
||||
|
||||
block title
|
||||
| Register
|
||||
|
||||
block content
|
||||
form(method="post")
|
||||
fieldset
|
||||
label(for="username") Username
|
||||
input(id="username", name="username")
|
||||
p.form-hint At least 3 characters, alphanumerical, must be unique
|
||||
|
||||
fieldset
|
||||
label(for="email") E-Mail Address
|
||||
input(id="email", name="email", type="email")
|
||||
|
||||
fieldset
|
||||
label(for="password") Password
|
||||
input(id="password", name="password", type="password")
|
||||
|
||||
fieldset
|
||||
label(for="confirmPassword") Confirm Password
|
||||
input(id="confirmPassword", name="confirmPassword", type="password")
|
||||
|
||||
button(type="submit") Register
|
18
api/views/reset-password.pug
Normal file
18
api/views/reset-password.pug
Normal file
|
@ -0,0 +1,18 @@
|
|||
extends layout.pug
|
||||
|
||||
block title
|
||||
| Reset Password
|
||||
|
||||
block content
|
||||
form(method="post")
|
||||
input(type="hidden", name="token", value=token)
|
||||
|
||||
fieldset
|
||||
label(for="password") New Password
|
||||
input(id="password", name="password", type="password")
|
||||
|
||||
fieldset
|
||||
label(for="confirmPassword") Confirm Password
|
||||
input(id="confirmPassword", name="confirmPassword", type="password")
|
||||
|
||||
button(type="submit") Set new password
|
Loading…
Reference in a new issue