diff --git a/api/src/_middleware/validate-request.js b/api/src/_middleware/validate-request.js
index be64dc5..7222023 100644
--- a/api/src/_middleware/validate-request.js
+++ b/api/src/_middleware/validate-request.js
@@ -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();
}
};
diff --git a/api/src/accounts/account.service.js b/api/src/accounts/account.service.js
index 5d88b28..d8b4843 100644
--- a/api/src/accounts/account.service.js
+++ b/api/src/accounts/account.service.js
@@ -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 = `
Please click the below link to verify your email address:
${verifyUrl}
`;
} else {
- message = `Please use the below token to verify your email address with the /account/verify-email
api route:
+ message = `Please use the below token to verify your email address with the /verify-email
api route:
${account.verificationToken}
`;
}
@@ -120,9 +120,9 @@ async function sendVerificationEmail(account, origin) {
async function sendAlreadyRegisteredEmail(email, origin) {
let message;
if (origin) {
- message = `If you don't know your password please visit the forgot password page.
`;
+ message = `If you don't know your password please visit the forgot password page.
`;
} else {
- message = `If you don't know your password you can reset it via the /account/forgot-password
api route.
`;
+ message = `If you don't know your password you can reset it via the /forgot-password
api route.
`;
}
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 = `Please click the below link to reset your password, the link will be valid for 1 day:
${resetUrl}
`;
} else {
- message = `Please use the below token to reset your password with the /account/reset-password
api route:
+ message = `Please use the below token to reset your password with the /reset-password
api route:
${account.resetToken.token}
`;
}
diff --git a/api/src/config/oauth2orize.js b/api/src/config/oauth2orize.js
deleted file mode 100644
index e69de29..0000000
diff --git a/api/src/config/passport.js b/api/src/config/passport.js
index f3daf41..644a645 100644
--- a/api/src/config/passport.js
+++ b/api/src/config/passport.js
@@ -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();
diff --git a/api/src/index.js b/api/src/index.js
index 01831f9..4dcd326 100644
--- a/api/src/index.js
+++ b/api/src/index.js
@@ -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());
diff --git a/api/src/routes/auth.js b/api/src/routes/auth.js
index ca498f8..8c6c6c7 100644
--- a/api/src/routes/auth.js
+++ b/api/src/routes/auth.js
@@ -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;
diff --git a/api/views/forgot-password.pug b/api/views/forgot-password.pug
new file mode 100644
index 0000000..dba5152
--- /dev/null
+++ b/api/views/forgot-password.pug
@@ -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
diff --git a/api/views/layout.pug b/api/views/layout.pug
index 8a4a77b..b862e67 100644
--- a/api/views/layout.pug
+++ b/api/views/layout.pug
@@ -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
diff --git a/api/views/login.pug b/api/views/login.pug
index f697af5..0c646d0 100644
--- a/api/views/login.pug
+++ b/api/views/login.pug
@@ -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
diff --git a/api/views/logout.pug b/api/views/logout.pug
new file mode 100644
index 0000000..1d899e1
--- /dev/null
+++ b/api/views/logout.pug
@@ -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
diff --git a/api/views/message.pug b/api/views/message.pug
index 582b72f..637e2dc 100644
--- a/api/views/message.pug
+++ b/api/views/message.pug
@@ -6,3 +6,6 @@ block title
block content
if description
div(class="message " + type)= description
+
+ if showLoginButton
+ p: a(href="/login") Go to login
diff --git a/api/views/register.pug b/api/views/register.pug
new file mode 100644
index 0000000..5735f85
--- /dev/null
+++ b/api/views/register.pug
@@ -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
diff --git a/api/views/reset-password.pug b/api/views/reset-password.pug
new file mode 100644
index 0000000..4ca1695
--- /dev/null
+++ b/api/views/reset-password.pug
@@ -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