Browse Source

email verififcation and OTP/2FA

develop
Rob Colbert 2 years ago
parent
commit
1463bd4d6f
  1. 61
      app/controllers/user.js
  2. 8
      app/services/otp-auth.js
  3. 6
      app/services/user.js
  4. 8
      app/views/user/otp-disabled.pug
  5. 8
      app/views/user/otp-setup-complete.pug
  6. 78
      app/views/user/settings.pug
  7. 2
      client/less/site/image.less
  8. 10
      config/limiter.js

61
app/controllers/user.js

@ -31,6 +31,12 @@ class UserController extends SiteController {
dtp.app.use('/user', router);
const authRequired = sessionService.authCheckMiddleware({ requireLogin: true });
const otpSetup = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: true,
otpRedirectURL: async (req) => { return `/user/${req.user._id}`; },
});
const otpMiddleware = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: false,
@ -58,31 +64,52 @@ class UserController extends SiteController {
router.param('userId', this.populateUser.bind(this));
router.post('/:userId/profile-photo',
router.post(
'/:userId/profile-photo',
limiterService.create(limiterService.config.user.postProfilePhoto),
checkProfileOwner,
upload.single('imageFile'),
this.postProfilePhoto.bind(this),
);
router.post('/:userId/settings',
router.post(
'/:userId/settings',
limiterService.create(limiterService.config.user.postUpdateSettings),
checkProfileOwner,
upload.none(),
this.postUpdateSettings.bind(this),
);
router.post('/',
router.post(
'/',
limiterService.create(limiterService.config.user.postCreate),
this.postCreateUser.bind(this),
);
router.get('/:userId/settings',
router.get(
'/:userId/otp-setup',
limiterService.create(limiterService.config.user.getOtpSetup),
otpSetup,
this.getOtpSetup.bind(this),
);
router.get(
'/:userId/otp-disable',
limiterService.create(limiterService.config.user.getOtpDisable),
authRequired,
this.getOtpDisable.bind(this),
);
router.get(
'/:userId/settings',
limiterService.create(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
checkProfileOwner,
this.getUserSettingsView.bind(this),
);
router.get('/:userId',
router.get(
'/:userId',
limiterService.create(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
@ -90,7 +117,8 @@ class UserController extends SiteController {
this.getUserView.bind(this),
);
router.delete('/:userId/profile-photo',
router.delete(
'/:userId/profile-photo',
limiterService.create(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
@ -214,8 +242,29 @@ class UserController extends SiteController {
}
}
async getOtpSetup (req, res) {
res.render('user/otp-setup-complete');
}
async getOtpDisable (req, res) {
const { otpAuth: otpAuthService } = this.dtp.services;
try {
await otpAuthService.destroyOtpSession(req, 'Account');
await otpAuthService.removeForUser(req.user, 'Account');
res.render('user/otp-disabled');
} catch (error) {
this.log.error('failed to disable OTP service for Account', { error });
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async getUserSettingsView (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services;
try {
res.locals.hasOtpAccount = await otpAuthService.isUserProtected(req.user, 'Account');
res.locals.startTab = req.query.st || 'watch';
res.render('user/settings');
} catch (error) {

8
app/services/otp-auth.js

@ -213,6 +213,14 @@ class OtpAuthService extends SiteService {
return true;
}
async isUserProtected (user, serviceName) {
const account = await OtpAccount.findOne({ user: user._id, service: serviceName });
if (!account) {
return false;
}
return true;
}
async removeForUser (user) {
return await OtpAccount.deleteMany({ user: user });
}

6
app/services/user.js

@ -277,7 +277,7 @@ class UserService {
{ username_lc: accountUsername },
]
})
.select('+passwordSalt +password +flags')
.select('+passwordSalt +password +flags +optIn +permissions')
.lean();
if (!user) {
throw new SiteError(404, 'Member credentials are invalid');
@ -371,7 +371,7 @@ class UserService {
async getUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +picture')
.select('+email +flags +permissions +optIn +picture')
.populate(this.populateUser)
.lean();
if (!user) {
@ -388,7 +388,7 @@ class UserService {
const users = await User
.find(search)
.sort({ username_lc: 1 })
.select('+email +flags +permissions')
.select('+email +flags +permissions +optIn')
.skip(pagination.skip)
.limit(pagination.cpp)
.lean()

8
app/views/user/otp-disabled.pug

@ -0,0 +1,8 @@
extends ../layouts/main
block content
section.uk-section.uk-section-default
.uk-container.uk-text-center
h1 Two-Factor Authentication
p You have successfully disabled Two-Factor Authentication.
a(href=`/user/${user._id}/settings?tab=account`).uk-button.dtp-button-default Return to Settings

8
app/views/user/otp-setup-complete.pug

@ -0,0 +1,8 @@
extends ../layouts/main
block content
section.uk-section.uk-section-default
.uk-container.uk-text-center
h1 Two-Factor Authentication
p You have successfully enabled Two-Factor Authentication on your account.
a(href=`/user/${user._id}/settings?tab=account`).uk-button.dtp-button-default Return to Settings

78
app/views/user/settings.pug

@ -7,10 +7,8 @@ block content
include ../components/file-upload-image
section.uk-section.uk-section-default
section.uk-section.uk-section-default.uk-section-small
.uk-container
h1 Settings
div(uk-grid)
div(class="uk-width-1-1 uk-width-1-3@m")
-
@ -19,17 +17,34 @@ block content
currentProfile = user.picture.large;
}
.uk-margin
+renderFileUploadImage(
`/user/${user._id}/profile-photo`,
'profile-picture-upload',
'profile-picture-file',
'site-profile-picture',
`/img/default-member.png`,
currentProfile,
{ aspectRatio: 1 },
)
.uk-card.uk-card-default.uk-card-small
.uk-card-header.uk-text-center
h1.uk-card-title Profile Photo
.uk-card-body
+renderFileUploadImage(
`/user/${user._id}/profile-photo`,
'profile-picture-upload',
'profile-picture-file',
'site-profile-picture',
`/img/default-member.png`,
currentProfile,
{ aspectRatio: 1 },
)
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-header.uk-text-center
h1.uk-card-title Two-Factor Authentication
.uk-card-body
p Enabling Two-Factor Authentication (2FA) with a One-Time Password authenticator app can help protect your account and settings from unauthorized access.
.uk-card-footer.uk-text-center
if hasOtpAccount
a(href=`/user/${user._id}/otp-disable`).uk-button.dtp-button-danger Disable 2FA
else
a(href=`/user/${user._id}/otp-setup`).uk-button.dtp-button-default Enable 2FA
div(class="uk-width-1-1 uk-width-expand@m")
h1 Settings
form(method="POST", action=`/user/${user._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form
.uk-margin
label(for="username").uk-form-label Username
@ -38,5 +53,44 @@ block content
label(for="display-name").uk-form-label Display Name
input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= user.displayName).uk-input
.uk-margin
div(uk-grid).uk-grid-small
.uk-width-1-2
.uk-margin
label(for="password").uk-form-label New Password
input(id="password", name="password", type="password", placeholder="Enter new password", autocomplete= "new-password").uk-input
.uk-width-1-2
.uk-margin
label(for="passwordv").uk-form-label Verify New Password
input(id="passwordv", name="passwordv", type="password", placeholder="Enter new password again", autocomplete= "new-password").uk-input
fieldset
legend Email Preferences
.uk-margin
label(for="email").uk-form-label
span Email Address
if user.flags.isEmailVerified
span (verified)
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-expand@s")
.uk-margin-small
input(id="email", name="email", type="email", placeholder="Enter email address", value= user.email).uk-input
if user.flags.isEmailVerified
.uk-text-small.uk-text-muted Changing your email address will un-verify you and send a new verification email. Check your spam folder!
else
.uk-text-small.uk-text-muted Changing your email address will send a new verification email. Check your spam folder!
div(class="uk-width-1-1 uk-width-auto@s")
button(type="button", onclick="return dtp.app.resendWelcomeEmail(event);").uk-button.dtp-button-default Resend Welcome Email
.uk-margin
div(uk-grid).uk-grid-small
label
input(id="optin-system", name="optIn.system", type="checkbox", checked= user.optIn ? user.optIn.system : false).uk-checkbox
| System messages
label
input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= user.optIn ? user.optIn.marketing : false).uk-checkbox
| Sales / Marketing
.uk-margin
button(type="submit").uk-button.dtp-button-primary Update account settings

2
client/less/site/image.less

@ -25,7 +25,7 @@ img.site-profile-picture {
height: auto;
margin-left: auto;
margin-right: auto;
border-radius: 50%;
border-radius: 5%;
background-color: #8a8a8a;
}

10
config/limiter.js

@ -146,6 +146,16 @@ module.exports = {
expire: ONE_MINUTE,
message: 'You are updating account settings too quickly',
},
getOtpSetup: {
total: 10,
expire: ONE_MINUTE,
message: 'You are configuring two-factor authentication too quickly',
},
getOtpDisable: {
total: 10,
expire: ONE_MINUTE,
message: 'You are disabling two-factor authentication too quickly',
},
getSettings: {
total: 8,
expire: ONE_MINUTE,

Loading…
Cancel
Save