Add ability to require e-mail verification
This commit is contained in:
parent
f45e92af1e
commit
62c818dc8d
@ -1,3 +1,5 @@
|
|||||||
|
import {message_service} from './Message.service.js'
|
||||||
|
|
||||||
class ProfileService {
|
class ProfileService {
|
||||||
|
|
||||||
async get_profile(user_id = 'me') {
|
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 }) {
|
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 }) {
|
async update_notify({ user_id = 'me', app_key, gateway_url }) {
|
||||||
|
@ -124,6 +124,8 @@ class ProfileController extends Controller {
|
|||||||
|
|
||||||
async update(req, res, next) {
|
async update(req, res, next) {
|
||||||
const User = this.models.get('auth:User')
|
const User = this.models.get('auth:User')
|
||||||
|
const Message = this.models.get('Message')
|
||||||
|
const Setting = this.models.get('Setting')
|
||||||
|
|
||||||
let user
|
let user
|
||||||
if ( req.params.user_id === 'me' ) user = req.user
|
if ( req.params.user_id === 'me' ) user = req.user
|
||||||
@ -155,6 +157,11 @@ class ProfileController extends Controller {
|
|||||||
.api()
|
.api()
|
||||||
|
|
||||||
// Update the user's profile
|
// 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.first_name = req.body.first_name
|
||||||
user.last_name = req.body.last_name
|
user.last_name = req.body.last_name
|
||||||
user.email = req.body.email
|
user.email = req.body.email
|
||||||
@ -163,7 +170,9 @@ class ProfileController extends Controller {
|
|||||||
|
|
||||||
// Save the record
|
// Save the record
|
||||||
await user.save()
|
await user.save()
|
||||||
return res.api()
|
return res.api({
|
||||||
|
force_message_refresh: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async update_photo(req, res, next) {
|
async update_photo(req, res, next) {
|
||||||
|
@ -7,7 +7,7 @@ const FormController = require('flitter-auth/controllers/Forms')
|
|||||||
*/
|
*/
|
||||||
class Forms extends FormController {
|
class Forms extends FormController {
|
||||||
static get services() {
|
static get services() {
|
||||||
return [...super.services, 'Vue', 'models']
|
return [...super.services, 'Vue', 'models', 'jobs']
|
||||||
}
|
}
|
||||||
|
|
||||||
async registration_provider_get(req, res, next) {
|
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) {
|
async finish_registration(req, res, next) {
|
||||||
if ( req.trap.has_trap() && req.trap.get_trap() === 'registrant_flow' ) await req.trap.end()
|
if ( req.trap.has_trap() && req.trap.get_trap() === 'registrant_flow' ) await req.trap.end()
|
||||||
const dest = req.session.registrant_flow || '/dash/profile'
|
const dest = req.session.registrant_flow || '/dash/profile'
|
||||||
|
62
app/jobs/SendVerificationEmail.job.js
Normal file
62
app/jobs/SendVerificationEmail.job.js
Normal 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
|
@ -26,6 +26,7 @@ class User extends AuthUser {
|
|||||||
last_name: String,
|
last_name: String,
|
||||||
tagline: String,
|
tagline: String,
|
||||||
email: String,
|
email: String,
|
||||||
|
email_verified: {type: Boolean, default: false},
|
||||||
ldap_visible: {type: Boolean, default: true},
|
ldap_visible: {type: Boolean, default: true},
|
||||||
active: {type: Boolean, default: true},
|
active: {type: Boolean, default: true},
|
||||||
mfa_token: MFAToken,
|
mfa_token: MFAToken,
|
||||||
|
@ -58,7 +58,29 @@ class TrapUtility {
|
|||||||
|
|
||||||
allows(route) {
|
allows(route) {
|
||||||
const config = this.config()
|
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 = {}) {
|
async test(req, res, next, args = {}) {
|
||||||
|
const Setting = this.models.get('Setting')
|
||||||
req.trap = new TrapUtility(req, res, this.configs.get('traps.types'))
|
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()
|
if ( !req.trap.has_trap() ) return next()
|
||||||
else if ( req.trap.allows(req.path) ) return next()
|
else if ( req.trap.allows(req.path) ) return next()
|
||||||
else return req.trap.redirect()
|
else return req.trap.redirect()
|
||||||
|
@ -72,6 +72,16 @@ const index = {
|
|||||||
'controller::auth:Forms.finish_registration',
|
'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': [
|
'/login-message': [
|
||||||
'middleware::auth:UserOnly',
|
'middleware::auth:UserOnly',
|
||||||
'controller::api:v1:System.show_login_message',
|
'controller::api:v1:System.show_login_message',
|
||||||
|
@ -15,6 +15,7 @@ const jobs_config = {
|
|||||||
'mailer',
|
'mailer',
|
||||||
'password_resets',
|
'password_resets',
|
||||||
'notifications',
|
'notifications',
|
||||||
|
'verifications',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Mapping of worker name => worker config
|
// Mapping of worker name => worker config
|
||||||
@ -23,7 +24,7 @@ const jobs_config = {
|
|||||||
// The name of the worker is "main"
|
// The name of the worker is "main"
|
||||||
main: {
|
main: {
|
||||||
// This worker will process these queues
|
// 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
|
// You can have many workers, and multiple workers can
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const setting_config = {
|
const setting_config = {
|
||||||
settings: {
|
settings: {
|
||||||
'auth.allow_registration': true,
|
'auth.allow_registration': true,
|
||||||
|
'auth.require_email_verify': false,
|
||||||
'auth.default_roles': [ 'base_user' ],
|
'auth.default_roles': [ 'base_user' ],
|
||||||
'home.allow_landing': true,
|
'home.allow_landing': true,
|
||||||
'home.redirect_authenticated': true,
|
'home.redirect_authenticated': true,
|
||||||
|
@ -41,6 +41,22 @@ const traps_config = {
|
|||||||
'/api/v1/vault/get-trust-payload',
|
'/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/*',
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.',
|
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:',
|
will_redirect: 'You will be redirected to:',
|
||||||
reauth_to_continue: 'Please re-authenticate to continue.',
|
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.',
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user