SAML; Dashboard
This commit is contained in:
@@ -54,12 +54,34 @@ class AuthController extends Controller {
|
||||
destination = req.session.auth.flow
|
||||
}
|
||||
|
||||
// TODO remember-device feature
|
||||
if ( user.mfa_enabled && !req.session.mfa_remember ) {
|
||||
req.session.auth.in_dmz = true
|
||||
destination = '/auth/mfa/challenge'
|
||||
}
|
||||
|
||||
if ( req.session?.auth?.message )
|
||||
delete req.session.auth.message
|
||||
|
||||
// If we're doing a trust flow, check the grant
|
||||
if ( req.body.grant_code && req.trust.has_flow() ) {
|
||||
if ( req.trust.check_grant(req.body.grant_code) ) {
|
||||
req.trust.grant(req.trust.flow_scope())
|
||||
|
||||
// Trust re-verification is granted,
|
||||
// but the user might still need to verify MFA
|
||||
const next = req.trust.end()
|
||||
if ( req.session.auth.in_dmz ) {
|
||||
req.session.auth.flow = next
|
||||
} else {
|
||||
destination = next
|
||||
}
|
||||
} else {
|
||||
return res.status(401)
|
||||
.message(`Unable to grant trust. Grant token is invalid.`)
|
||||
.api()
|
||||
}
|
||||
}
|
||||
|
||||
return res.api({
|
||||
success: true,
|
||||
session_created: !!req.body.create_session,
|
||||
@@ -124,15 +146,43 @@ class AuthController extends Controller {
|
||||
.api()
|
||||
|
||||
req.user.mfa_enabled = true
|
||||
req.user.mfa_enable_date = new Date
|
||||
req.user.save()
|
||||
|
||||
// TODO invalidate existing tokens and other logins
|
||||
// invalidate existing tokens and other logins
|
||||
const flitter = await this.auth.get_provider('flitter')
|
||||
await flitter.logout(req)
|
||||
await req.user.kickout()
|
||||
|
||||
return res.api({success: true, mfa_enabled: req.user.mfa_enabled})
|
||||
}
|
||||
|
||||
async disable_mfa(req, res, next) {
|
||||
if ( !req.user.mfa_enabled )
|
||||
return res.status(400)
|
||||
.message('The user does not have MFA enabled.')
|
||||
.api()
|
||||
|
||||
req.user.mfa_enabled = false
|
||||
delete req.user.mfa_enable_date
|
||||
delete req.user.mfa_token
|
||||
req.user.app_passwords = []
|
||||
await req.user.save()
|
||||
|
||||
// invalidate existing login tokens and logins
|
||||
const flitter = await this.auth.get_provider('flitter')
|
||||
await flitter.logout(req)
|
||||
await req.user.kickout()
|
||||
|
||||
return res.api({success: true, mfa_enabled: req.user.mfa_enabled})
|
||||
}
|
||||
|
||||
async get_mfa_enable_date(req, res, next) {
|
||||
if ( !req.user.mfa_enabled )
|
||||
return res.api({ mfa_enabled: false })
|
||||
|
||||
return res.api({ mfa_enabled: true, mfa_enable_date: req.user.mfa_enable_date })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = AuthController
|
||||
|
||||
45
app/controllers/api/v1/Message.controller.js
Normal file
45
app/controllers/api/v1/Message.controller.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class MessageController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
async get_banners(req, res, next) {
|
||||
const Message = this.models.get('Message')
|
||||
const messages = await Message.for_user(req.user)
|
||||
|
||||
return res.api(messages.map(x => {
|
||||
return {
|
||||
message: x.message,
|
||||
expires: x.expires,
|
||||
dismissed: x.dismissed,
|
||||
type: x.display_type,
|
||||
id: x.id,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async read_banner(req, res, next) {
|
||||
const banner_id = req.params.banner_id
|
||||
if ( !banner_id )
|
||||
return res.status(400)
|
||||
.message('Missing required parameter: banner_id')
|
||||
.api()
|
||||
|
||||
const Message = this.models.get('Message')
|
||||
const message = await Message.findById(banner_id)
|
||||
if ( !message )
|
||||
return res.status(404)
|
||||
.message('Banner message not found with that ID.')
|
||||
.api()
|
||||
|
||||
if ( message.user_id !== req.user.id )
|
||||
return res.status(401).api()
|
||||
|
||||
await message.dismiss()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = MessageController
|
||||
98
app/controllers/api/v1/Password.controller.js
Normal file
98
app/controllers/api/v1/Password.controller.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const zxcvbn = require('zxcvbn')
|
||||
|
||||
class PasswordController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'auth']
|
||||
}
|
||||
|
||||
async get_resets(req, res, next) {
|
||||
return res.api(req.user.password_resets.map(x => {
|
||||
return {
|
||||
reset_on: x.reset_on,
|
||||
reason: x.reason,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async get_app_passwords(req, res, next) {
|
||||
return res.api(req.user.app_passwords.map(x => {
|
||||
return {
|
||||
created: x.created,
|
||||
expires: x.expires,
|
||||
active: x.active,
|
||||
name: x.name ?? '(unnamed)',
|
||||
uuid: x.uuid,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async create_app_password(req, res, next) {
|
||||
if ( !req.body.name )
|
||||
return res.status(400)
|
||||
.message('Missing required field: name')
|
||||
.api()
|
||||
|
||||
const { password, record } = await req.user.app_password(req.body.name)
|
||||
await req.user.save()
|
||||
|
||||
return res.api({
|
||||
password,
|
||||
name: req.body.name,
|
||||
uuid: record.uuid,
|
||||
})
|
||||
}
|
||||
|
||||
async delete_app_password(req, res, next) {
|
||||
if ( !req.params.uuid )
|
||||
return res.status(400)
|
||||
.message('Missing required parameter: uuid')
|
||||
.api()
|
||||
|
||||
const match = req.user.app_passwords.filter(x => x.uuid === req.params.uuid)[0]
|
||||
if ( !match )
|
||||
return res.status(400)
|
||||
.message('App password not found with that UUID.')
|
||||
.api()
|
||||
|
||||
req.user.app_passwords = req.user.app_passwords.filter(x => x.uuid !== req.params.uuid)
|
||||
await req.user.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
async reset_password(req, res, next) {
|
||||
if ( !req.body.password )
|
||||
return res.status(400)
|
||||
.message('Missing required field: password')
|
||||
.api()
|
||||
|
||||
// Verify password complexity
|
||||
const min_score = 3
|
||||
const result = zxcvbn(req.body.password)
|
||||
if ( result.score < min_score )
|
||||
return res.status(400)
|
||||
.message(`Password does not meet the minimum complexity score of ${min_score}.`)
|
||||
.api()
|
||||
|
||||
// Make sure it's not a re-do
|
||||
for ( const old_pw of req.user.password_resets ) {
|
||||
if ( await old_pw.check(req.body.password) ) {
|
||||
return res.status(400)
|
||||
.message(`This password is a duplicate of one of your previous passwords.`)
|
||||
.api()
|
||||
}
|
||||
}
|
||||
|
||||
// Create the password reset
|
||||
const reset = await req.user.reset_password(req.body.password)
|
||||
await req.user.save()
|
||||
|
||||
// invalidate existing tokens and other logins
|
||||
const flitter = await this.auth.get_provider('flitter')
|
||||
await flitter.logout(req)
|
||||
await req.user.kickout()
|
||||
return res.api()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = PasswordController
|
||||
77
app/controllers/api/v1/Profile.controller.js
Normal file
77
app/controllers/api/v1/Profile.controller.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const { Controller } = require('libflitter')
|
||||
const Validator = require('email-validator')
|
||||
|
||||
class ProfileController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models']
|
||||
}
|
||||
|
||||
async fetch(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
|
||||
let user
|
||||
if ( req.params.user_id === 'me' ) user = req.user
|
||||
else { // if not me, verify that user can view profile
|
||||
if ( !req.user.can(`profile:view:${req.params.user_id}`) )
|
||||
return res.status(401).api()
|
||||
|
||||
user = await User.findById(req.params.user_id)
|
||||
}
|
||||
|
||||
return res.api({
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
email: user.email,
|
||||
uid: user.uid,
|
||||
tagline: user.tagline,
|
||||
user_id: user.id,
|
||||
})
|
||||
}
|
||||
|
||||
async update(req, res, next) {
|
||||
const User = this.models.get('auth:User')
|
||||
|
||||
let user
|
||||
if ( req.params.user_id === 'me' ) user = req.user
|
||||
else { // If not me, verify that user can modify profile
|
||||
if ( !req.user.can(`profile:update:${req.params.user_id}`) )
|
||||
return res.status(401).api()
|
||||
|
||||
user = await User.findById(req.params.user_id)
|
||||
}
|
||||
|
||||
if ( !user )
|
||||
return res.status(404)
|
||||
.message('No user found with the specified ID.')
|
||||
.api()
|
||||
|
||||
// Make sure the required fields are provided
|
||||
const required_fields = ['first_name', 'last_name', 'email']
|
||||
for ( const field of required_fields ) {
|
||||
if ( !req.body[field]?.trim() )
|
||||
return res.status(400)
|
||||
.message(`Required field "${field}" is missing or invalid.`)
|
||||
.api()
|
||||
}
|
||||
|
||||
// Validate the e-mail
|
||||
if ( !Validator.validate(req.body.email) )
|
||||
return res.status(400)
|
||||
.message(`"email" field must be a valid e-mail address.`)
|
||||
.api()
|
||||
|
||||
// Update the user's profile
|
||||
user.first_name = req.body.first_name
|
||||
user.last_name = req.body.last_name
|
||||
user.email = req.body.email
|
||||
if ( req.body.tagline ) user.tagline = req.body.tagline
|
||||
else delete user.tagline
|
||||
|
||||
// Save the record
|
||||
await user.save()
|
||||
return res.api()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = ProfileController
|
||||
Reference in New Issue
Block a user