Add ability to require e-mail verification
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is failing

This commit is contained in:
Garrett Mills 2021-05-04 11:28:55 -05:00
parent f45e92af1e
commit 62c818dc8d
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
11 changed files with 189 additions and 5 deletions

View File

@ -1,3 +1,5 @@
import {message_service} from './Message.service.js'
class ProfileService {
async get_profile(user_id = 'me') {
@ -11,7 +13,10 @@ class ProfileService {
}
async update_profile({ user_id, first_name, last_name, email, login_shell = undefined, tagline = undefined }) {
await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline, login_shell })
const results = await axios.patch(`/api/v1/profile/${user_id}`, { first_name, last_name, email, tagline, login_shell })
if ( results && results.data && results.data.data && results.data.data.force_message_refresh ) {
await message_service._listener_tick()
}
}
async update_notify({ user_id = 'me', app_key, gateway_url }) {

View File

@ -124,6 +124,8 @@ class ProfileController extends Controller {
async update(req, res, next) {
const User = this.models.get('auth:User')
const Message = this.models.get('Message')
const Setting = this.models.get('Setting')
let user
if ( req.params.user_id === 'me' ) user = req.user
@ -155,6 +157,11 @@ class ProfileController extends Controller {
.api()
// Update the user's profile
if ( user.email !== req.body.email && (await Setting.get('auth.require_email_verify')) ) {
await req.trap.begin('verify_email', { session_only: false })
await Message.create(req.user, 'Your e-mail address has changed, and a verification e-mail has been sent. You must complete this process to continue.')
}
user.first_name = req.body.first_name
user.last_name = req.body.last_name
user.email = req.body.email
@ -163,7 +170,9 @@ class ProfileController extends Controller {
// Save the record
await user.save()
return res.api()
return res.api({
force_message_refresh: true,
})
}
async update_photo(req, res, next) {

View File

@ -7,7 +7,7 @@ const FormController = require('flitter-auth/controllers/Forms')
*/
class Forms extends FormController {
static get services() {
return [...super.services, 'Vue', 'models']
return [...super.services, 'Vue', 'models', 'jobs']
}
async registration_provider_get(req, res, next) {
@ -20,6 +20,50 @@ class Forms extends FormController {
})
}
async email_verify_keyaction(req, res, next) {
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
req.user.email_verified = true
await req.user.save()
await req.trap.end()
const url = req.session.email_verify_flow || '/dash/profile'
return res.redirect(url)
}
async show_verify_email(req, res, next) {
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
const verify_queue = this.jobs.queue('verifications')
await verify_queue.add('SendVerificationEmail', { user_id: req.user.id })
return res.page('public:message', {
...this.Vue.data({
message: req.T('auth.must_verify_email'),
actions: [
{
text: 'Send Verification E-Mail',
action: 'redirect',
next: '/auth/verify-email/sent',
},
],
})
})
}
async send_verify_email(req, res, next) {
if ( !req.trap.has_trap('verify_email') ) return res.redirect(req.session.email_verify_flow || '/dash/profile')
return res.page('public:message', {
...this.Vue.data({
message: req.T('auth.verify_email_sent'),
actions: [
{
text: 'Re-send Verification E-Mail',
action: 'redirect',
next: '/auth/verify-email/sent',
},
],
})
})
}
async finish_registration(req, res, next) {
if ( req.trap.has_trap() && req.trap.get_trap() === 'registrant_flow' ) await req.trap.end()
const dest = req.session.registrant_flow || '/dash/profile'

View File

@ -0,0 +1,62 @@
const { Job } = require('flitter-jobs')
class SendVerificationEmailJob extends Job {
static get services() {
return [...super.services, 'models', 'jobs', 'output', 'configs']
}
async execute(job) {
const {data} = job
const {user_id} = data
try {
const User = this.models.get('auth:User')
const user = await User.findById(user_id)
if (!user) {
this.error(`Unable to find user with ID: ${user_id}`)
throw new Error('Unable to find user with that ID.')
}
this.info(`Sending verification email for user: ${user.uid}`)
// Create an authenticated key-action
const key_action = await this.key_action(user)
this.info(`Created verification keyaction ${key_action.id} (key: ${key_action.key}, handler: ${key_action.handler})`)
await this.jobs.queue('mailer').add('EMail', {
to: user.email,
subject: 'Confirm Your E-mail | ' + this.configs.get('app.name'),
email_params: {
header_text: 'Confirm Your E-mail',
body_paragraphs: [
'The e-mail address for your ' + this.configs.get('app.name') + ' was set or changed. Click the link below to verify this change.',
'If you didn\'t request this e-mail, please contact your system administrator.',
],
button_text: 'Confirm E-mail',
button_link: key_action.url(),
}
})
this.info('Logged e-mail job.')
} catch (e) {
this.error(e)
throw e
}
}
async key_action(user) {
const KeyAction = this.models.get('auth:KeyAction')
const ka_data = {
handler: 'controller::auth:Forms.email_verify_keyaction',
used: false,
user_id: user._id,
auto_login: true,
no_auto_logout: false,
}
return (new KeyAction(ka_data)).save()
}
}
module.exports = exports = SendVerificationEmailJob

View File

@ -26,6 +26,7 @@ class User extends AuthUser {
last_name: String,
tagline: String,
email: String,
email_verified: {type: Boolean, default: false},
ldap_visible: {type: Boolean, default: true},
active: {type: Boolean, default: true},
mfa_token: MFAToken,

View File

@ -58,7 +58,29 @@ class TrapUtility {
allows(route) {
const config = this.config()
return route.startsWith('/assets') || config.allowed_routes.includes(route.toLowerCase().trim())
const allowed = route.startsWith('/assets') || config.allowed_routes.includes(route.toLowerCase().trim())
if ( allowed ) return true
for ( const allowed_route of config.allowed_routes ) {
console.log('comparing', allowed_route, 'to', route)
const allowed_parts = allowed_route.split('/')
const parts = route.split('/')
let matches = true
for ( let i = 0; i < allowed_parts.length; i += 1 ) {
if ( allowed_parts[i] !== parts[i] && allowed_parts[i] !== '*' ) {
matches = false
}
}
if ( matches ) {
console.log('allows true')
return true
}
}
console.log('allows false')
return false
}
}
@ -68,8 +90,19 @@ class TrapsMiddleware extends Middleware {
}
async test(req, res, next, args = {}) {
const Setting = this.models.get('Setting')
req.trap = new TrapUtility(req, res, this.configs.get('traps.types'))
if (
!req.trap.has_trap()
&& req.user
&& !req.user.email_verified
&& (await Setting.get('auth.require_email_verify'))
) {
req.session.email_verify_flow = req.originalUrl
await req.trap.begin('verify_email', { session_only: false })
}
if ( !req.trap.has_trap() ) return next()
else if ( req.trap.allows(req.path) ) return next()
else return req.trap.redirect()

View File

@ -72,6 +72,16 @@ const index = {
'controller::auth:Forms.finish_registration',
],
'/verify-email': [
'middleware::auth:UserOnly',
'controller::auth:Forms.show_verify_email',
],
'/verify-email/sent': [
'middleware::auth:UserOnly',
'controller::auth:Forms.send_verify_email',
],
'/login-message': [
'middleware::auth:UserOnly',
'controller::api:v1:System.show_login_message',

View File

@ -15,6 +15,7 @@ const jobs_config = {
'mailer',
'password_resets',
'notifications',
'verifications',
],
// Mapping of worker name => worker config
@ -23,7 +24,7 @@ const jobs_config = {
// The name of the worker is "main"
main: {
// This worker will process these queues
queues: ['mailer', 'password_resets', 'notifications'],
queues: ['mailer', 'password_resets', 'notifications', 'verifications'],
},
// You can have many workers, and multiple workers can

View File

@ -1,6 +1,7 @@
const setting_config = {
settings: {
'auth.allow_registration': true,
'auth.require_email_verify': false,
'auth.default_roles': [ 'base_user' ],
'home.allow_landing': true,
'home.redirect_authenticated': true,

View File

@ -41,6 +41,22 @@ const traps_config = {
'/api/v1/vault/get-trust-payload',
],
},
verify_email: {
redirect_to: '/auth/verify-email',
allowed_routes: [
'/auth/verify-email',
'/auth/verify-email/sent',
'/auth/logout',
'/auth/login',
'/api/v1/locale/batch',
'/api/v1/auth/validate/username',
'/api/v1/auth/attempt',
'/api/v1/vault/get-trust-payload',
'/auth/action/*',
'/api/v1/message/banners',
'/api/v1/message/banners/read/*',
],
},
},
}

View File

@ -26,4 +26,6 @@ module.exports = exports = {
oauth_prompt: 'CLIENT_NAME is requesting access to your APP_NAME account. Once you grant it, you may not be prompted for permission again.',
will_redirect: 'You will be redirected to:',
reauth_to_continue: 'Please re-authenticate to continue.',
must_verify_email: 'You must verify your e-mail address to continue. Click below to send the verification e-mail.',
verify_email_sent: 'Check your e-mail for the link to verify your account.',
}