'use strict'; const router = require('express').Router(); 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{ await permission.byGroup(req.user, ['app_sso_admin']) return res.json({ results: await User[req.query.detail ? "listDetail" : "list"](), }); }catch(error){ next(error); } }); router.post('/', async function(req, res, next){ try{ await permission.byGroup(req.user, ['app_sso_admin']) req.body.created_by = req.user.uid req.body.manager = [req.user.dn]; let 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); // Service accounts (a Unix account an app/service runs as, not a // person) are marked by membership in app_sso_service_account rather // than a schema change -- see models/user_ldap.js User.listDetail. if (req.body.isServiceAccount === true || req.body.isServiceAccount === 'true' || req.body.isServiceAccount === 'on') { try { const group = await Group.get('app_sso_service_account'); await group.addMember(user); // User.add() already cached `user` (via its own internal // User.get()) before this group membership existed, so the // cached isServiceAccount would be stuck wrong for 5 minutes // (the cache TTL) without this -- re-fetch after clearing. User.clearCache(); user = await User.get(user.uid); } catch (error) { console.error(`user.add: failed to mark ${user.uid} as a service account:`, error.message); } } return res.json({results: user, message: `User ${user.uid} created.`}); }catch(error){ next(error); } }); router.delete('/:uid', 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']) } return res.json({uid: req.params.uid, results: await user.remove()}) }catch(error){ next(error); } }); router.get('/me', async function(req, res, next){ try{ const user = JSON.parse(JSON.stringify(await User.get({uid: req.user.uid}))); // The shared client framework gates the UI on a single effective-rights // flag (the OIDC-client apps send the same key). Here "admin" means // membership in app_sso_admin; group-level gating still reads memberOf. const groups = (user.memberOf || []).map(function(dn){ return String(dn).split(',')[0].replace(/^cn=/i, ''); }); user.isAdmin = groups.includes('app_sso_admin'); return res.json(user); }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, message: 'Password changed.'}); }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.get('/:uid/group-members', async function(req, res, next){ try{ await permission.byGroup(req.user, ['app_sso_admin']); return res.json({results: await User.getPersonalGroupMembers(req.params.uid)}); }catch(error){ next(error); } }); router.put('/:uid/group-member/:memberUid', async function(req, res, next){ try{ await permission.byGroup(req.user, ['app_sso_admin']); await User.addPersonalGroupMember(req.params.uid, req.params.memberUid); return res.json({ results: true, message: `Added ${req.params.memberUid} to ${req.params.uid}'s group` }); }catch(error){ next(error); } }); router.delete('/:uid/group-member/:memberUid', async function(req, res, next){ try{ await permission.byGroup(req.user, ['app_sso_admin']); await User.removePersonalGroupMember(req.params.uid, req.params.memberUid); return res.json({ results: true, message: `Removed ${req.params.memberUid} from ${req.params.uid}'s group` }); }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; }else{ user = await User.get(req.params.uid); const isManager = (user.manager || []).includes(req.user.dn); if(!isManager) await permission.byGroup(req.user, ['app_sso_admin']) } // The manager picker is a tag widget backed by a single newline-separated // hidden input (see public/js/app.js app.ui.userSelect), same convention // as oauth_client.js's allowed_groups. if (typeof req.body.manager === 'string') { req.body.manager = req.body.manager.split('\n').map(s => s.trim()).filter(Boolean); } return res.json({ results: await user.update(req.body), message: `Updated ${req.params.uid} user` }); }catch(error){ next(error); } }); router.post('/invite', async function(req, res, next){ try{ 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({ token: token.token, link: `${req.protocol}://${req.hostname}/login/invite/${token.token}`, mail_sent: !!mail, }); }catch(error){ next(error); } }); router.get('/invite', async function(req, res, next){ try{ 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.list(); 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); } }); 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); } }); router.post('/key', async function(req, res, next){ try{ let added = await User.addSSHkey({ uid: req.user.uid, key: req.body.key }); return res.status(added === true ? 200 : 400).json({ message: added }); }catch(error){ next(error); } }); 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({ results: await User.get(req.params.uid), }); }catch(error){ next(error); } }); module.exports = router;