Updated frontend
This commit is contained in:
+162
-8
@@ -1,12 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const router = require('express').Router();
|
||||
const {User} = require('../models/user');
|
||||
const {Auth, AuthToken} = require('../models/auth');
|
||||
const {PasswordResetToken} = require('../models/token');
|
||||
const {PasswordResetToken, ImpersonationToken, OtpToken} = require('../models/token');
|
||||
const {hashPasswordSSHA512} = require('../models/user_ldap');
|
||||
const {UserVerification} = require('../models/verification');
|
||||
const {SMS} = require('../models/sms');
|
||||
const {Mail} = require('../models/email');
|
||||
const middleware = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rate_limit');
|
||||
const permission = require('../utils/permission');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
|
||||
async function findUserByLogin(login) {
|
||||
try {
|
||||
return await User.get(login);
|
||||
} catch(e) {
|
||||
if (e.status === 404 || e.name === 'UserNotFound') {
|
||||
return await User.get({searchKey: 'mail', searchValue: login});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
router.post('/login', async function(req, res, next){
|
||||
router.get('/username-suggestions', async function(req, res, next) {
|
||||
try {
|
||||
const { givenName, sn, dob } = req.query;
|
||||
if (!givenName || !sn) return res.json({ suggestions: [] });
|
||||
return res.json({ suggestions: await User.usernameSuggestions(givenName, sn, dob) });
|
||||
} catch(e) { next(e); }
|
||||
});
|
||||
|
||||
router.post('/login', rateLimit.login, async function(req, res, next){
|
||||
try{
|
||||
let auth = await Auth.login(req.body);
|
||||
return res.json({
|
||||
@@ -31,7 +59,7 @@ router.all('/logout', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/resetpassword', async function(req, res, next){
|
||||
router.post('/resetpassword', rateLimit.passwordReset, async function(req, res, next){
|
||||
try{
|
||||
let sent = await User.passwordReset(`${req.protocol}://${req.hostname}`, req.body.mail);
|
||||
|
||||
@@ -45,29 +73,35 @@ router.post('/resetpassword', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/resetpassword/:token', async function(req, res, next){
|
||||
router.post('/resetpassword/:token', rateLimit.passwordReset, async function(req, res, next){
|
||||
try{
|
||||
let token = await PasswordResetToken.get(req.params.token);
|
||||
|
||||
if(token.is_valid && 86400000+Number(token.created_on) > (new Date).getTime()){
|
||||
let user = await User.get(token.created_by);
|
||||
await user.setPassword(req.body);
|
||||
token.update({is_valid: false});
|
||||
await token.update({is_valid: false});
|
||||
return res.json({
|
||||
message: 'Password has been changed.'
|
||||
});
|
||||
}
|
||||
|
||||
let error = new Error('TokenExpired');
|
||||
error.name = 'TokenExpired';
|
||||
error.message = 'Password reset token is invalid or has expired.';
|
||||
error.status = 401;
|
||||
throw error;
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/invite/:token/:mailToken', async function(req, res, next) {
|
||||
router.post('/invite/:token/:mailToken', rateLimit.invite, async function(req, res, next) {
|
||||
try{
|
||||
req.body.token = req.params.token;
|
||||
req.body.mailToken = req.params.mailToken;
|
||||
let user = await User.addByInvite(req.body);
|
||||
let token = await AuthToken.add(user);
|
||||
let token = await AuthToken.create(user);
|
||||
|
||||
return res.json({
|
||||
user: user.uid,
|
||||
@@ -80,7 +114,7 @@ router.post('/invite/:token/:mailToken', async function(req, res, next) {
|
||||
|
||||
});
|
||||
|
||||
router.post('/invite/:token', async function(req, res, next){
|
||||
router.post('/invite/:token', rateLimit.invite, async function(req, res, next){
|
||||
try{
|
||||
let data = {
|
||||
token: req.params.token,
|
||||
@@ -95,6 +129,126 @@ router.post('/invite/:token', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/otp/request', rateLimit.otpRequest, async function(req, res, next) {
|
||||
try {
|
||||
const {login, method} = req.body;
|
||||
if (!login || !method) {
|
||||
return res.status(400).json({message: 'login and method are required'});
|
||||
}
|
||||
|
||||
const user = await findUserByLogin(login);
|
||||
|
||||
if (method === 'sms' && !user.mobile) {
|
||||
return res.status(400).json({message: 'No phone number on file for this account'});
|
||||
}
|
||||
|
||||
const otp = await OtpToken.issue(user.uid, method);
|
||||
|
||||
if (method === 'email') {
|
||||
await Mail.sendTemplate(user.mail, 'otp_code', {givenName: user.givenName, code: otp.code});
|
||||
} else if (method === 'sms') {
|
||||
try {
|
||||
await SMS.send(user.mobile, `Your ${conf.name} login code: ${otp.code}`);
|
||||
} catch (smsErr) {
|
||||
const err = new Error('SMS delivery failed. Please try email or contact your administrator.');
|
||||
err.status = 502;
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
return res.status(400).json({message: 'method must be email or sms'});
|
||||
}
|
||||
|
||||
return res.json({message: 'Code sent', method, expires_at: otp.expires_at});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/otp/verify', rateLimit.otpVerify, async function(req, res, next) {
|
||||
try {
|
||||
const {login, code} = req.body;
|
||||
if (!login || !code) {
|
||||
return res.status(400).json({message: 'login and code are required'});
|
||||
}
|
||||
|
||||
const user = await findUserByLogin(login);
|
||||
const otp = await OtpToken.verify(user.uid, String(code));
|
||||
|
||||
if (!otp) {
|
||||
const error = new Error('Invalid or expired code');
|
||||
error.status = 401;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const verif = await UserVerification.getOrCreate(user.uid);
|
||||
if (otp.method === 'email') await verif.markEmailVerified();
|
||||
if (otp.method === 'sms') await verif.markPhoneVerified();
|
||||
|
||||
const authToken = await AuthToken.create(user);
|
||||
return res.json({login: true, token: authToken.token});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/impersonate/:uid', middleware.auth, async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
|
||||
const target = await User.get(req.params.uid);
|
||||
|
||||
// Clean up any existing impersonation for this target
|
||||
const existing = await ImpersonationToken.listDetail({ target_uid: target.uid });
|
||||
for (const old of existing) {
|
||||
if (old.is_valid && !old.isExpired) {
|
||||
try { await target.removeTempPassword(old.temp_hash); } catch(_) {}
|
||||
}
|
||||
await old.update({ is_valid: false });
|
||||
}
|
||||
|
||||
const tempPassword = crypto.randomBytes(16).toString('base64url');
|
||||
const tempHash = hashPasswordSSHA512(tempPassword);
|
||||
|
||||
await target.addTempPassword(tempHash);
|
||||
|
||||
const token = await ImpersonationToken.add({
|
||||
admin_uid: req.user.uid,
|
||||
target_uid: target.uid,
|
||||
temp_hash: tempHash,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
uid: target.uid,
|
||||
temp_password: tempPassword,
|
||||
expires_at: token.expires_at,
|
||||
});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/impersonate/:uid', middleware.auth, async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
|
||||
const target = await User.get(req.params.uid);
|
||||
const existing = await ImpersonationToken.listDetail({ target_uid: target.uid });
|
||||
|
||||
let revoked = 0;
|
||||
for (const token of existing) {
|
||||
if (token.is_valid) {
|
||||
try { await target.removeTempPassword(token.temp_hash); } catch(_) {}
|
||||
await token.update({ is_valid: false });
|
||||
revoked++;
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({ message: `Impersonation ended for ${target.uid}`, revoked });
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
/*
|
||||
|
||||
@@ -32,6 +32,7 @@ router.post('/', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get('/:name', async function(req, res, next){
|
||||
try{
|
||||
return res.json({
|
||||
@@ -50,7 +51,7 @@ router.put('/owner/:group/:uid', async function(req, res, next){
|
||||
var group = await Group.get(req.params.group);
|
||||
var user = await User.get(req.params.uid);
|
||||
return res.json({
|
||||
results: group.addOwner(user),
|
||||
results: await group.addOwner(user),
|
||||
message: `Added owner ${req.params.uid} to ${req.params.group} group.`
|
||||
});
|
||||
}catch(error){
|
||||
@@ -66,7 +67,7 @@ router.delete('/owner/:group/:uid', async function(req, res, next){
|
||||
var group = await Group.get(req.params.group);
|
||||
var user = await User.get(req.params.uid);
|
||||
return res.json({
|
||||
results: group.removeOwner(user),
|
||||
results: await group.removeOwner(user),
|
||||
message: `Removed Owner ${req.params.uid} from ${req.params.group} group.`
|
||||
});
|
||||
}catch(error){
|
||||
@@ -82,7 +83,7 @@ router.put('/:group/:uid', async function(req, res, next){
|
||||
var group = await Group.get(req.params.group);
|
||||
var user = await User.get(req.params.uid);
|
||||
return res.json({
|
||||
results: group.addMember(user),
|
||||
results: await group.addMember(user),
|
||||
message: `Added user ${req.params.uid} to ${req.params.group} group.`
|
||||
});
|
||||
}catch(error){
|
||||
@@ -98,7 +99,7 @@ router.delete('/:group/:uid', async function(req, res, next){
|
||||
var group = await Group.get(req.params.group);
|
||||
var user = await User.get(req.params.uid);
|
||||
return res.json({
|
||||
results: group.removeMember(user),
|
||||
results: await group.removeMember(user),
|
||||
message: `Removed user ${req.params.uid} from ${req.params.group} group.`
|
||||
});
|
||||
}catch(error){
|
||||
|
||||
+76
-12
@@ -1,34 +1,93 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const moment = require('moment');
|
||||
const {marked} = require('marked');
|
||||
const {InviteToken, PasswordResetToken} = require('./../models/token');
|
||||
const conf = require('../conf/conf.js');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
|
||||
const tosHtml = marked(fs.readFileSync(path.join(__dirname, '../../tos.md'), 'utf8'));
|
||||
const { version: buildVersion } = require('../package.json');
|
||||
let buildHash = 'unknown';
|
||||
try { buildHash = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); } catch(_) {}
|
||||
|
||||
const values ={
|
||||
title: conf.environment !== 'production' ? `dev` : '',
|
||||
titleIcon: conf.environment !== 'production' ? `<i class="fa-brands fa-dev"></i>` : '',
|
||||
name: conf.name,
|
||||
buildVersion,
|
||||
buildHash,
|
||||
buildYear: new Date().getFullYear(),
|
||||
}
|
||||
|
||||
// List of front end node modules to be served
|
||||
const frontEndModules = ['bootstrap', 'mustache', 'jquery', '@fortawesome',
|
||||
'moment', '@popper', 'jq-repeat',
|
||||
];
|
||||
|
||||
// Server front end modules
|
||||
// https://stackoverflow.com/a/55700773/3140931
|
||||
frontEndModules.forEach(dep => {
|
||||
router.use(`/static-modules/${dep}`, express.static(path.join(__dirname, `../node_modules/${dep}`)))
|
||||
});
|
||||
|
||||
// Have express server static content( images, CSS, browser JS) from the public
|
||||
// local folder.
|
||||
router.use('/static', express.static(path.join(__dirname, '../public')))
|
||||
|
||||
|
||||
router.get('/tos', function(req, res) {
|
||||
res.render('tos', {...values, tosHtml});
|
||||
});
|
||||
|
||||
router.get('/admin', function(req, res) {
|
||||
res.render('admin', {...values});
|
||||
});
|
||||
|
||||
router.get('/notifications', function(req, res) {
|
||||
res.render('notifications', {...values});
|
||||
});
|
||||
|
||||
router.get('/invites', function(req, res) {
|
||||
res.render('invites', {...values});
|
||||
});
|
||||
|
||||
router.get('/onboarding', function(req, res) {
|
||||
res.render('onboarding', {...values, tosHtml});
|
||||
});
|
||||
|
||||
router.get('/', async function(req, res, next) {
|
||||
res.render('home', { title: 'Express', name: conf.name });
|
||||
res.render('profile', {...values});
|
||||
});
|
||||
|
||||
router.get('/login', function(req, res, next) {
|
||||
res.render('login', {redirect: req.query.redirect, name: conf.name });
|
||||
router.get('/users', async function(req, res, next) {
|
||||
res.render('users', {...values});
|
||||
});
|
||||
|
||||
router.get('/users', function(req, res, next) {
|
||||
res.render('users', { title: 'Express', name: conf.name });
|
||||
router.get('/login', async function(req, res, next) {
|
||||
res.render('login', {...values, redirect: req.query.redirect});
|
||||
});
|
||||
|
||||
router.get('/oauth-clients', function(req, res, next) {
|
||||
res.render('oauth_clients', {...values});
|
||||
});
|
||||
|
||||
|
||||
|
||||
router.get('/users/:uid', function(req, res, next) {
|
||||
res.render('home', { title: 'Express', name: conf.name });
|
||||
res.render('profile', {...values});
|
||||
});
|
||||
|
||||
router.get('/groups', function(req, res, next) {
|
||||
res.render('groups', { title: 'Express', name: conf.name });
|
||||
res.render('groups', {...values});
|
||||
});
|
||||
|
||||
router.get('/token', function(req, res, next) {
|
||||
res.render('token', { title: 'Express', name: conf.name });
|
||||
res.render('token', {...values});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +95,7 @@ router.get('/login/resetpassword/:token', async function(req, res, next){
|
||||
let token = await PasswordResetToken.get(req.params.token);
|
||||
|
||||
if(token.is_valid && 86400000+Number(token.created_on) > (new Date).getTime()){
|
||||
res.render('reset_password', {token:token, name: conf.name });
|
||||
res.render('reset_password', {token:token, ...values });
|
||||
}else{
|
||||
next({message: 'token not found', status: 404});
|
||||
}
|
||||
@@ -48,7 +107,7 @@ router.get('/login/invite/:token/:mailToken', async function(req, res, next){
|
||||
let token = await InviteToken.get(req.params.token);
|
||||
if(token.is_valid && token.mail !== '__NONE__' && token.mail_token === req.params.mailToken){
|
||||
token.created_on = moment(token.created_on, 'x').fromNow();
|
||||
res.render('invite', { title: 'Express', invite: token, name: conf.name });
|
||||
res.render('invite', {invite: token, ...values});
|
||||
}else{
|
||||
next({message: 'token not found', status: 404});
|
||||
}
|
||||
@@ -63,7 +122,7 @@ router.get('/login/invite/:token', async function(req, res, next){
|
||||
token.created_on = moment(token.created_on, 'x').fromNow();
|
||||
|
||||
if(token.is_valid){
|
||||
res.render('invite_email', { title: 'Express', invite: token, name: conf.name });
|
||||
res.render('invite_email', {invite: token, ...values});
|
||||
}else{
|
||||
next({message: 'token not found', status: 404});
|
||||
}
|
||||
@@ -72,4 +131,9 @@ router.get('/login/invite/:token', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get('/login/*splat', async function(req, res, next) {
|
||||
res.render('login', {...values, redirect: req.query.redirect});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const {User} = require('../models/user_ldap');
|
||||
const {Group} = require('../models/group_ldap');
|
||||
const {Notification} = require('../models/notification');
|
||||
const {Mail} = require('../models/email');
|
||||
const permission = require('../utils/permission');
|
||||
|
||||
async function resolveRecipients(filter_type, filter_value, active_only) {
|
||||
if (filter_type === 'all' || filter_type === 'all_active') {
|
||||
const users = await User.listDetail();
|
||||
const active = filter_type === 'all_active' || active_only;
|
||||
return active ? users.filter(u => !u.pwdAccountLockedTime) : users;
|
||||
}
|
||||
|
||||
if (filter_type === 'group') {
|
||||
const groupNames = filter_value.split(',').map(s => s.trim()).filter(Boolean);
|
||||
const groups = await Promise.all(groupNames.map(name => Group.get(name).catch(() => null)));
|
||||
const dnSet = new Set();
|
||||
groups.filter(Boolean).forEach(group => {
|
||||
[].concat(group.member || []).forEach(dn => dnSet.add(dn));
|
||||
});
|
||||
const uids = [...dnSet].map(dn => {
|
||||
const m = dn.match(/^uid=([^,]+)/i);
|
||||
return m ? m[1] : null;
|
||||
}).filter(Boolean);
|
||||
const users = (await Promise.all(uids.map(uid => User.get(uid).catch(() => null)))).filter(Boolean);
|
||||
return active_only ? users.filter(u => !u.pwdAccountLockedTime) : users;
|
||||
}
|
||||
|
||||
if (filter_type === 'users') {
|
||||
const uids = JSON.parse(filter_value);
|
||||
const users = await Promise.all(uids.map(uid => User.get(uid).catch(() => null)));
|
||||
return users.filter(Boolean);
|
||||
}
|
||||
|
||||
throw Object.assign(new Error('Invalid filter_type'), { status: 400 });
|
||||
}
|
||||
|
||||
router.post('/', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
|
||||
const { subject, message, filter_type, filter_value = '', active_only = false } = req.body;
|
||||
if (!subject || !message || !filter_type) {
|
||||
return res.status(400).json({ name: 'ValidationError', message: 'subject, message, and filter_type are required' });
|
||||
}
|
||||
|
||||
const recipients = await resolveRecipients(filter_type, filter_value, active_only);
|
||||
|
||||
const record = await Notification.create({
|
||||
created_by: req.user.uid,
|
||||
subject,
|
||||
message,
|
||||
filter_type,
|
||||
filter_value: String(filter_value),
|
||||
active_only: Boolean(active_only),
|
||||
});
|
||||
|
||||
let sent = 0, failed = 0;
|
||||
for (const user of recipients) {
|
||||
if (!user.mail) { failed++; continue; }
|
||||
try {
|
||||
await Mail.sendTemplate(user.mail, 'notification', {
|
||||
givenName: user.givenName || user.uid,
|
||||
subject,
|
||||
message,
|
||||
});
|
||||
sent++;
|
||||
} catch(e) {
|
||||
console.error(`Notification send failed for ${user.uid}:`, e.message);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
await record.update({ status: 'sent', sent_count: sent, failed_count: failed, sent_at: Date.now() });
|
||||
|
||||
return res.json({ results: record });
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
const list = await Notification.listDetail();
|
||||
list.sort((a, b) => (b.created_on || 0) - (a.created_on || 0));
|
||||
return res.json({ results: list });
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:id', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
return res.json({ results: await Notification.get(req.params.id) });
|
||||
} catch(e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,368 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const { execSync } = require('child_process');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const express = require('express');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const { OAuthClient } = require('../models/oauth_client');
|
||||
const { OAuthCode, OAuthAccessToken, OAuthRefreshToken } = require('../models/oauth_code');
|
||||
const { User } = require('../models/user');
|
||||
|
||||
const oauthConf = conf.oauth || {};
|
||||
const issuer = oauthConf.issuer || `http://localhost:${conf.port || 3000}`;
|
||||
const jwtSecret = oauthConf.jwtSecret || 'change-me-in-secrets';
|
||||
|
||||
const { version: buildVersion } = require('../package.json');
|
||||
let buildHash = 'unknown';
|
||||
try { buildHash = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim(); } catch(_) {}
|
||||
|
||||
const pageLocals = {
|
||||
title: conf.environment !== 'production' ? 'dev' : '',
|
||||
titleIcon: conf.environment !== 'production' ? '<i class="fa-brands fa-dev"></i>' : '',
|
||||
name: conf.name,
|
||||
buildVersion,
|
||||
buildHash,
|
||||
buildYear: new Date().getFullYear(),
|
||||
};
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
function makeError(name, message, status) {
|
||||
const error = new Error(name);
|
||||
error.name = name;
|
||||
error.message = message;
|
||||
error.status = status;
|
||||
return error;
|
||||
}
|
||||
|
||||
function verifyPKCE(code_verifier, code_challenge, method) {
|
||||
if (method !== 'S256') return false;
|
||||
const hash = crypto.createHash('sha256').update(code_verifier).digest();
|
||||
const b64 = hash.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
return b64 === code_challenge;
|
||||
}
|
||||
|
||||
function parseClientAuth(req) {
|
||||
// Support Basic auth header and POST body
|
||||
const auth = req.headers['authorization'];
|
||||
if (auth && auth.startsWith('Basic ')) {
|
||||
const [client_id, client_secret] = Buffer.from(auth.slice(6), 'base64').toString().split(':');
|
||||
return { client_id, client_secret };
|
||||
}
|
||||
return {
|
||||
client_id: req.body.client_id,
|
||||
client_secret: req.body.client_secret,
|
||||
};
|
||||
}
|
||||
|
||||
function buildIdToken(user, client, scope, now) {
|
||||
const scopes = scope.split(' ');
|
||||
const claims = {
|
||||
iss: issuer,
|
||||
sub: user.uid,
|
||||
aud: client.client_id,
|
||||
iat: Math.floor(now / 1000),
|
||||
exp: Math.floor(now / 1000) + (client.token_lifetime.access_token || 3600),
|
||||
preferred_username: user.uid,
|
||||
};
|
||||
|
||||
if (scopes.includes('profile')) {
|
||||
claims.name = `${user.givenName} ${user.sn}`;
|
||||
claims.given_name = user.givenName;
|
||||
claims.family_name = user.sn;
|
||||
}
|
||||
if (scopes.includes('email')) {
|
||||
claims.email = user.mail;
|
||||
}
|
||||
|
||||
return jwt.sign(claims, jwtSecret, { algorithm: 'HS256' });
|
||||
}
|
||||
|
||||
function userClaims(user, scope) {
|
||||
const scopes = scope.split(' ');
|
||||
const claims = { sub: user.uid };
|
||||
|
||||
if (scopes.includes('profile')) {
|
||||
claims.name = `${user.givenName} ${user.sn}`;
|
||||
claims.given_name = user.givenName;
|
||||
claims.family_name = user.sn;
|
||||
claims.preferred_username = user.uid;
|
||||
}
|
||||
if (scopes.includes('email')) {
|
||||
claims.email = user.mail;
|
||||
}
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
// --- page router (mounted at /oauth) ---
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Consent page — validates OAuth params and renders the consent UI.
|
||||
// The actual code issuance happens via POST /api/oauth/authorize (authenticated).
|
||||
router.get('/authorize', async function(req, res, next) {
|
||||
try {
|
||||
const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = req.query;
|
||||
|
||||
if (response_type !== 'code') {
|
||||
return next(makeError('UnsupportedResponseType', 'Only response_type=code is supported.', 400));
|
||||
}
|
||||
if (!code_challenge) {
|
||||
return next(makeError('PKCERequired', 'code_challenge is required.', 400));
|
||||
}
|
||||
if ((code_challenge_method || 'S256') !== 'S256') {
|
||||
return next(makeError('UnsupportedMethod', 'Only code_challenge_method=S256 is supported.', 400));
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = await OAuthClient.get(client_id);
|
||||
} catch(_) {
|
||||
return next(makeError('InvalidClient', 'Unknown client_id.', 400));
|
||||
}
|
||||
|
||||
if (!client.is_valid) {
|
||||
return next(makeError('InvalidClient', 'Client is disabled.', 400));
|
||||
}
|
||||
if (!client.redirect_uris.includes(redirect_uri)) {
|
||||
return next(makeError('InvalidRedirectURI', 'redirect_uri is not registered for this client.', 400));
|
||||
}
|
||||
|
||||
const requestedScopes = (scope || '').split(' ').filter(Boolean);
|
||||
const allowedScopes = requestedScopes.filter(s => client.scopes.includes(s));
|
||||
|
||||
res.render('oauth_authorize', {
|
||||
...pageLocals,
|
||||
oauthClient: {
|
||||
client_id: client.client_id,
|
||||
name: client.name,
|
||||
description: client.description,
|
||||
},
|
||||
params: {
|
||||
response_type,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope: allowedScopes.join(' '),
|
||||
state: state || '',
|
||||
code_challenge,
|
||||
code_challenge_method: code_challenge_method || 'S256',
|
||||
},
|
||||
scopes: allowedScopes,
|
||||
});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Token endpoint — exchanges auth codes and refresh tokens.
|
||||
router.post('/token', express.urlencoded({ extended: false }), async function(req, res, next) {
|
||||
try {
|
||||
const { grant_type, code, redirect_uri, code_verifier, refresh_token } = req.body;
|
||||
const { client_id, client_secret } = parseClientAuth(req);
|
||||
|
||||
// Authenticate the client
|
||||
let client;
|
||||
try {
|
||||
client = await OAuthClient.get(client_id);
|
||||
} catch(_) {
|
||||
return res.status(401).json({ error: 'invalid_client', error_description: 'Unknown client.' });
|
||||
}
|
||||
if (!client.is_valid || !(await client.verifySecret(client_secret))) {
|
||||
return res.status(401).json({ error: 'invalid_client', error_description: 'Client authentication failed.' });
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (grant_type === 'authorization_code') {
|
||||
let authCode;
|
||||
try {
|
||||
authCode = await OAuthCode.get(code);
|
||||
} catch(_) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'Authorization code not found.' });
|
||||
}
|
||||
|
||||
if (!authCode.is_valid || authCode.isExpired) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'Authorization code has expired or been used.' });
|
||||
}
|
||||
if (authCode.client_id !== client_id) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'Code was not issued to this client.' });
|
||||
}
|
||||
if (authCode.redirect_uri !== redirect_uri) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri does not match.' });
|
||||
}
|
||||
if (authCode.code_challenge && !verifyPKCE(code_verifier, authCode.code_challenge, authCode.code_challenge_method)) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'PKCE verification failed.' });
|
||||
}
|
||||
|
||||
// Consume the code
|
||||
await authCode.update({ is_valid: false });
|
||||
|
||||
const user = await User.get(authCode.username);
|
||||
const accessExpires = now + (client.token_lifetime.access_token * 1000);
|
||||
const refreshExpires = now + (client.token_lifetime.refresh_token * 1000);
|
||||
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
OAuthAccessToken.add({ username: authCode.username, client_id, scope: authCode.scope, expires_at: accessExpires }),
|
||||
OAuthRefreshToken.add({ username: authCode.username, client_id, scope: authCode.scope, expires_at: refreshExpires }),
|
||||
]);
|
||||
|
||||
const response = {
|
||||
access_token: accessToken.token,
|
||||
token_type: 'Bearer',
|
||||
expires_in: client.token_lifetime.access_token,
|
||||
refresh_token: refreshToken.token,
|
||||
};
|
||||
|
||||
if (authCode.scope.split(' ').includes('openid')) {
|
||||
response.id_token = buildIdToken(user, client, authCode.scope, now);
|
||||
}
|
||||
|
||||
return res.json(response);
|
||||
|
||||
} else if (grant_type === 'refresh_token') {
|
||||
let oldRefreshToken;
|
||||
try {
|
||||
oldRefreshToken = await OAuthRefreshToken.get(refresh_token);
|
||||
} catch(_) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'Refresh token not found.' });
|
||||
}
|
||||
|
||||
if (!oldRefreshToken.is_valid || oldRefreshToken.isExpired) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'Refresh token has expired or been revoked.' });
|
||||
}
|
||||
if (oldRefreshToken.client_id !== client_id) {
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'Refresh token was not issued to this client.' });
|
||||
}
|
||||
|
||||
// Rotate — invalidate old, issue new
|
||||
await oldRefreshToken.update({ is_valid: false });
|
||||
|
||||
const user = await User.get(oldRefreshToken.username);
|
||||
const accessExpires = now + (client.token_lifetime.access_token * 1000);
|
||||
const refreshExpires = now + (client.token_lifetime.refresh_token * 1000);
|
||||
|
||||
const [accessToken, newRefreshToken] = await Promise.all([
|
||||
OAuthAccessToken.add({ username: oldRefreshToken.username, client_id, scope: oldRefreshToken.scope, expires_at: accessExpires }),
|
||||
OAuthRefreshToken.add({ username: oldRefreshToken.username, client_id, scope: oldRefreshToken.scope, expires_at: refreshExpires }),
|
||||
]);
|
||||
|
||||
const response = {
|
||||
access_token: accessToken.token,
|
||||
token_type: 'Bearer',
|
||||
expires_in: client.token_lifetime.access_token,
|
||||
refresh_token: newRefreshToken.token,
|
||||
};
|
||||
|
||||
if (oldRefreshToken.scope.split(' ').includes('openid')) {
|
||||
response.id_token = buildIdToken(user, client, oldRefreshToken.scope, now);
|
||||
}
|
||||
|
||||
return res.json(response);
|
||||
|
||||
} else {
|
||||
return res.status(400).json({ error: 'unsupported_grant_type', error_description: `grant_type '${grant_type}' is not supported.` });
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// UserInfo endpoint — accepts a Bearer access token and returns user claims.
|
||||
router.get('/userinfo', async function(req, res, next) {
|
||||
try {
|
||||
const auth = req.headers['authorization'];
|
||||
if (!auth || !auth.startsWith('Bearer ')) {
|
||||
res.set('WWW-Authenticate', 'Bearer');
|
||||
return res.status(401).json({ error: 'invalid_token', error_description: 'Bearer token required.' });
|
||||
}
|
||||
|
||||
const tokenStr = auth.slice(7);
|
||||
let accessToken;
|
||||
try {
|
||||
accessToken = await OAuthAccessToken.get(tokenStr);
|
||||
} catch(_) {
|
||||
res.set('WWW-Authenticate', 'Bearer error="invalid_token"');
|
||||
return res.status(401).json({ error: 'invalid_token', error_description: 'Access token not found.' });
|
||||
}
|
||||
|
||||
if (!accessToken.is_valid || accessToken.isExpired) {
|
||||
res.set('WWW-Authenticate', 'Bearer error="invalid_token"');
|
||||
return res.status(401).json({ error: 'invalid_token', error_description: 'Access token has expired.' });
|
||||
}
|
||||
|
||||
const user = await User.get(accessToken.username);
|
||||
return res.json(userClaims(user, accessToken.scope));
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- authenticated API router (mounted at /api/oauth with auth middleware) ---
|
||||
|
||||
const authRouter = express.Router();
|
||||
|
||||
// Issues an authorization code after the user approves the consent form.
|
||||
authRouter.post('/authorize', async function(req, res, next) {
|
||||
try {
|
||||
const { client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, response_type } = req.body;
|
||||
|
||||
if (response_type !== 'code') {
|
||||
return next(makeError('UnsupportedResponseType', 'Only response_type=code is supported.', 400));
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = await OAuthClient.get(client_id);
|
||||
} catch(_) {
|
||||
return next(makeError('InvalidClient', 'Unknown client_id.', 400));
|
||||
}
|
||||
|
||||
if (!client.is_valid) {
|
||||
return next(makeError('InvalidClient', 'Client is disabled.', 400));
|
||||
}
|
||||
if (!client.redirect_uris.includes(redirect_uri)) {
|
||||
return next(makeError('InvalidRedirectURI', 'redirect_uri is not registered for this client.', 400));
|
||||
}
|
||||
|
||||
const authCode = await OAuthCode.add({
|
||||
username: req.user.uid,
|
||||
client_id,
|
||||
redirect_uri,
|
||||
scope: scope || 'openid',
|
||||
code_challenge: code_challenge || '',
|
||||
code_challenge_method: code_challenge_method || 'S256',
|
||||
});
|
||||
|
||||
const redirectUrl = new URL(redirect_uri);
|
||||
redirectUrl.searchParams.set('code', authCode.token);
|
||||
if (state) redirectUrl.searchParams.set('state', state);
|
||||
|
||||
return res.json({ redirect_url: redirectUrl.toString() });
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// --- discovery document (used directly in app.js) ---
|
||||
|
||||
function discovery(req, res) {
|
||||
const base = issuer.replace(/\/$/, '');
|
||||
res.json({
|
||||
issuer: base,
|
||||
authorization_endpoint: `${base}/oauth/authorize`,
|
||||
token_endpoint: `${base}/oauth/token`,
|
||||
userinfo_endpoint: `${base}/oauth/userinfo`,
|
||||
scopes_supported: ['openid', 'profile', 'email'],
|
||||
response_types_supported: ['code'],
|
||||
grant_types_supported: ['authorization_code', 'refresh_token'],
|
||||
code_challenge_methods_supported: ['S256'],
|
||||
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic'],
|
||||
subject_types_supported: ['public'],
|
||||
id_token_signing_alg_values_supported: ['HS256'],
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { router, authRouter, discovery };
|
||||
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const { OAuthClient } = require('../models/oauth_client');
|
||||
const permission = require('../utils/permission');
|
||||
|
||||
const ADMIN_GROUP = 'app_sso_oauth_admin';
|
||||
|
||||
router.get('/', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, [ADMIN_GROUP]);
|
||||
return res.json({ results: await OAuthClient.listDetail() });
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, [ADMIN_GROUP]);
|
||||
|
||||
req.body.created_by = req.user.uid;
|
||||
|
||||
// Parse redirect_uris if sent as newline-separated string from the form
|
||||
if (typeof req.body.redirect_uris === 'string') {
|
||||
req.body.redirect_uris = req.body.redirect_uris.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
// Parse scopes if sent as space-separated string
|
||||
if (typeof req.body.scopes === 'string') {
|
||||
req.body.scopes = req.body.scopes.split(' ').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
// jQuery serializeObject sends nested fields as "token_lifetime[access_token]"
|
||||
if (req.body['token_lifetime[access_token]'] || req.body['token_lifetime[refresh_token]']) {
|
||||
req.body.token_lifetime = {
|
||||
access_token: Number(req.body['token_lifetime[access_token]']) || 3600,
|
||||
refresh_token: Number(req.body['token_lifetime[refresh_token]']) || 2592000,
|
||||
};
|
||||
delete req.body['token_lifetime[access_token]'];
|
||||
delete req.body['token_lifetime[refresh_token]'];
|
||||
}
|
||||
|
||||
const client = await OAuthClient.add(req.body);
|
||||
|
||||
return res.json({
|
||||
results: client,
|
||||
client_secret: client._raw_secret,
|
||||
message: `OAuth client '${client.name}' created. Save the client secret — it will not be shown again.`,
|
||||
});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:client_id', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, [ADMIN_GROUP]);
|
||||
return res.json({ results: await OAuthClient.get(req.params.client_id) });
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:client_id', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, [ADMIN_GROUP]);
|
||||
|
||||
const client = await OAuthClient.get(req.params.client_id);
|
||||
|
||||
if (typeof req.body.redirect_uris === 'string') {
|
||||
req.body.redirect_uris = req.body.redirect_uris.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
if (typeof req.body.scopes === 'string') {
|
||||
req.body.scopes = req.body.scopes.split(' ').map(s => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
results: await client.update(req.body),
|
||||
message: `OAuth client '${client.name}' updated.`,
|
||||
});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:client_id', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, [ADMIN_GROUP]);
|
||||
|
||||
const client = await OAuthClient.get(req.params.client_id);
|
||||
await client.remove();
|
||||
|
||||
return res.json({
|
||||
client_id: req.params.client_id,
|
||||
message: `OAuth client '${client.name}' deleted.`,
|
||||
});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:client_id/rotate', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, [ADMIN_GROUP]);
|
||||
|
||||
const client = await OAuthClient.get(req.params.client_id);
|
||||
const new_secret = await client.rotateSecret();
|
||||
|
||||
return res.json({
|
||||
client_secret: new_secret,
|
||||
message: `Client secret rotated for '${client.name}'. Save it — it will not be shown again.`,
|
||||
});
|
||||
} catch(error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -23,8 +23,6 @@ router.get('/', async function(req, res, next){
|
||||
|
||||
router.get('/:name', async function(req, res, next){
|
||||
try{
|
||||
console.log(tokens, req.params.name)
|
||||
|
||||
return res.json({
|
||||
results: await tokens[req.params.name][req.query.detail ? "listDetail" : "list"]()
|
||||
});
|
||||
|
||||
+208
-36
@@ -1,8 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const {User} = require('../models/user');
|
||||
const {User} = require('../models/user');
|
||||
const {Group} = require('../models/group_ldap');
|
||||
const permission = require('../utils/permission');
|
||||
const {UserVerification} = require('../models/verification');
|
||||
const {InviteToken} = require('../models/token');
|
||||
|
||||
router.get('/', async function(req, res, next){
|
||||
try{
|
||||
@@ -18,10 +21,15 @@ router.get('/', async function(req, res, next){
|
||||
router.post('/', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin'])
|
||||
|
||||
|
||||
req.body.created_by = req.user.uid
|
||||
|
||||
return res.json({results: await User.add(req.body)});
|
||||
const user = await User.add(req.body);
|
||||
const verif = await UserVerification.getOrCreate(user.uid);
|
||||
const updates = { password_must_change: true };
|
||||
if (req.body.tosAgree) updates.tos_accepted = true, updates.tos_accepted_at = Date.now();
|
||||
await verif.update(updates);
|
||||
return res.json({results: user});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
@@ -44,12 +52,84 @@ router.delete('/:uid', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/me', async function(req, res, next){
|
||||
try{
|
||||
return res.json(await User.get({uid: req.user.uid}));
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/accept-tos', async function(req, res, next){
|
||||
try{
|
||||
const verif = await UserVerification.getOrCreate(req.user.uid);
|
||||
await verif.markTosAccepted();
|
||||
User.clearCache();
|
||||
return res.json({ success: true });
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/password', async function(req, res, next){
|
||||
try{
|
||||
const result = await req.user.setPassword(req.body);
|
||||
const verif = await UserVerification.getOrCreate(req.user.uid);
|
||||
await verif.update({ password_must_change: false });
|
||||
User.clearCache();
|
||||
return res.json({results: result});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:uid/password', async function(req, res, next){
|
||||
try{
|
||||
let user;
|
||||
|
||||
if(req.params.uid.toLowerCase() === req.user.uid.toLowerCase()){
|
||||
user = req.user;
|
||||
}else{
|
||||
user = await User.get(req.params.uid);
|
||||
await permission.byGroup(req.user, ['app_sso_admin'])
|
||||
}
|
||||
|
||||
const result = await user.setPassword(req.body);
|
||||
if(req.params.uid.toLowerCase() !== req.user.uid.toLowerCase()){
|
||||
const verif = await UserVerification.getOrCreate(user.uid);
|
||||
await verif.update({ password_must_change: true });
|
||||
}
|
||||
return res.json({
|
||||
results: result,
|
||||
message: `User ${user.uid} password changed.`
|
||||
});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:uid/active', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
const user = await User.get(req.params.uid);
|
||||
const active = req.body.active !== false && req.body.active !== 'false';
|
||||
await user.setActive(active);
|
||||
return res.json({
|
||||
uid: req.params.uid,
|
||||
active,
|
||||
message: `User ${req.params.uid} ${active ? 'activated' : 'deactivated'}`
|
||||
});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:uid', async function(req, res, next){
|
||||
try{
|
||||
let user;
|
||||
|
||||
if(req.params.uid.toLowerCase() === req.user.uid.toLowerCase()){
|
||||
user = req.user;
|
||||
user = req.user;
|
||||
}else{
|
||||
user = await User.get(req.params.uid);
|
||||
await permission.byGroup(req.user, ['app_sso_admin'])
|
||||
@@ -65,48 +145,82 @@ router.put('/:uid', async function(req, res, next){
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/me', async function(req, res, next){
|
||||
router.post('/invite', async function(req, res, next){
|
||||
try{
|
||||
|
||||
return res.json(await User.get({uid: req.user.uid}));
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/password', async function(req, res, next){
|
||||
try{
|
||||
return res.json({results: await req.user.setPassword(req.body)})
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:uid/password', async function(req, res, next){
|
||||
try{
|
||||
let user;
|
||||
|
||||
if(req.params.uid.toLowerCase() === req.user.uid.toLowerCase()){
|
||||
user = req.user;
|
||||
}else{
|
||||
user = await User.get(req.params.uid);
|
||||
await permission.byGroup(req.user, ['app_sso_admin'])
|
||||
}
|
||||
|
||||
await permission.byGroup(req.user, ['app_sso_admin', 'app_sso_invite']);
|
||||
const { mail, groups = [] } = req.body;
|
||||
const token = await req.user.invite({
|
||||
mail,
|
||||
groups,
|
||||
url: `${req.protocol}://${req.hostname}`,
|
||||
});
|
||||
return res.json({
|
||||
results: await user.setPassword(req.body),
|
||||
message: `User ${user.uid} password changed.`
|
||||
token: token.token,
|
||||
link: `${req.protocol}://${req.hostname}/login/invite/${token.token}`,
|
||||
mail_sent: !!mail,
|
||||
});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/invite', async function(req, res, next){
|
||||
router.get('/invite', async function(req, res, next){
|
||||
try{
|
||||
let token = await req.user.invite();
|
||||
await permission.byGroup(req.user, ['app_sso_admin', 'app_sso_invite']);
|
||||
const isAdmin = await permission.byGroup(req.user, ['app_sso_admin']).then(() => true).catch(() => false);
|
||||
const all = await InviteToken.listDetail();
|
||||
const visible = isAdmin ? all : all.filter(t => t.created_by === req.user.uid);
|
||||
const results = visible.map(t => ({ token: t.token, ...t }));
|
||||
return res.json({ results });
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({token: token.token});
|
||||
router.put('/invite/:token', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin', 'app_sso_invite']);
|
||||
const token = await InviteToken.get(req.params.token);
|
||||
const isAdmin = await permission.byGroup(req.user, ['app_sso_admin']).then(() => true).catch(() => false);
|
||||
if (!isAdmin && token.created_by !== req.user.uid) {
|
||||
const err = new Error('Insufficient Permission'); err.status = 401; throw err;
|
||||
}
|
||||
if (!token.is_valid) {
|
||||
const err = new Error('Token is no longer valid'); err.status = 400; throw err;
|
||||
}
|
||||
const update = {};
|
||||
if (req.body.groups !== undefined) {
|
||||
update.groups = JSON.stringify([].concat(req.body.groups || []));
|
||||
}
|
||||
if (req.body.mail !== undefined && req.body.mail !== token.mail) {
|
||||
if (req.body.mail) {
|
||||
await User.verifyEmail({ token: token.token, mail: req.body.mail, url: `${req.protocol}://${req.hostname}` });
|
||||
const refreshed = await InviteToken.get(token.token);
|
||||
update.mail = refreshed.mail;
|
||||
update.mail_token = refreshed.mail_token;
|
||||
} else {
|
||||
update.mail = '__NONE__';
|
||||
update.mail_token = '__NONE__';
|
||||
}
|
||||
}
|
||||
if (Object.keys(update).length) await token.update(update);
|
||||
const updated = await InviteToken.get(req.params.token);
|
||||
return res.json({ results: { token: updated.token, ...updated } });
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/invite/:token', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin', 'app_sso_invite']);
|
||||
const token = await InviteToken.get(req.params.token);
|
||||
const isAdmin = await permission.byGroup(req.user, ['app_sso_admin']).then(() => true).catch(() => false);
|
||||
if (!isAdmin && token.created_by !== req.user.uid) {
|
||||
const err = new Error('Insufficient Permission'); err.status = 401; throw err;
|
||||
}
|
||||
await token.update({ is_valid: false });
|
||||
return res.json({ results: true });
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
@@ -129,6 +243,64 @@ router.post('/key', async function(req, res, next){
|
||||
|
||||
});
|
||||
|
||||
router.get('/:uid/verification', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
const verif = await UserVerification.getOrCreate(req.params.uid);
|
||||
return res.json({
|
||||
uid: req.params.uid,
|
||||
emailVerified: verif.email_verified,
|
||||
emailVerifiedAt: verif.email_verified_at || null,
|
||||
phoneVerified: verif.phone_verified,
|
||||
phoneVerifiedAt: verif.phone_verified_at || null,
|
||||
tosAccepted: verif.tos_accepted,
|
||||
tosAcceptedAt: verif.tos_accepted_at || null,
|
||||
});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/stats', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
const [users, groups] = await Promise.all([User.listDetail(), Group.list()]);
|
||||
const active = users.filter(u => !u.pwdAccountLockedTime);
|
||||
const inactive = users.filter(u => u.pwdAccountLockedTime);
|
||||
const recent = [...users]
|
||||
.sort((a, b) => (b.createTimestamp || '').localeCompare(a.createTimestamp || ''))
|
||||
.slice(0, 10)
|
||||
.map(u => ({ uid: u.uid, givenName: u.givenName, sn: u.sn, mail: u.mail, createTimestamp: u.createTimestamp }));
|
||||
return res.json({
|
||||
totalUsers: users.length,
|
||||
activeUsers: active.length,
|
||||
inactiveUsers: inactive.length,
|
||||
totalGroups: groups.length,
|
||||
recentSignups: recent,
|
||||
inactiveList: inactive.map(u => ({ uid: u.uid, givenName: u.givenName, sn: u.sn, mail: u.mail })),
|
||||
});
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/export', async function(req, res, next){
|
||||
try{
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
const users = await User.listDetail();
|
||||
const headers = ['uid', 'givenName', 'sn', 'mail', 'mobile', 'uidNumber', 'isActive', 'createTimestamp'];
|
||||
const rows = users.map(u => headers.map(h => {
|
||||
const v = u[h] || '';
|
||||
return `"${String(v).replace(/"/g, '""')}"`;
|
||||
}).join(','));
|
||||
res.setHeader('Content-Type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="users.csv"');
|
||||
return res.send([headers.join(','), ...rows].join('\n'));
|
||||
}catch(error){
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/:uid', async function(req, res, next){
|
||||
try{
|
||||
return res.json({
|
||||
|
||||
Reference in New Issue
Block a user