UNPKG

@qelos/auth

Version:

Express Passport authentication service

502 lines (434 loc) 15.3 kB
import { Response } from 'express' import { Types } from 'mongoose' import Workspace from '../models/workspace'; import { AuthRequest } from '../../types'; import { emitPlatformEvent } from '@qelos/api-kit'; import logger from '../services/logger'; import { getSignedToken, getUniqueId, setCookie, verifyToken } from '../services/tokens'; import { getCookieTokenName, getCookieTokenValue, updateToken } from '../services/users'; import User, { UserModel } from '../models/user'; import { cookieTokenExpiration } from '../../config'; import { getRequestHost } from '../services/req-host'; import { getWorkspaceConfiguration } from '../services/workspace-configuration'; import { getEncryptedData, setEncryptedData } from '../services/encrypted-data'; const ObjectId = Types.ObjectId; function getWorkspaceIdIfExists(_id: string, tenant: string) { return Workspace.findOne({ _id, tenant }).select('_id').lean().exec(); } export async function getWorkspace(req: AuthRequest, res: Response) { if (!req.isWorkspacePrivileged) { res.status(200).json(req.workspace).end() return; } try { const { members, invites } = await Workspace.findOne({ _id: req.workspace._id }, 'members invites').lean().exec(); res.status(200).json({ ...req.workspace.toJSON(), members, invites, }).end() } catch (err) { res.status(500).json({ message: 'Failed to load workspace' }).end() } } export async function getWorkspaces(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const userId = req.userPayload.sub; try { const workspaces = await Workspace.find({ tenant, 'members.user': new ObjectId(userId), }) .select('name logo tenant members.$').lean().exec(); res.status(200).json(workspaces.map(ws => { return { ...ws, isPrivilegedUser: ws.members[0].roles.includes('admin') }; })).end() } catch (err) { res.status(500).json({ message: 'Failed to load workspaces' }).end() } } export async function getEveryWorkspaces(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const dbQuery: any = { tenant, }; if (req.query._id) { if (req.query._id instanceof Array) { dbQuery._id = { $in: req.query._id }; } else { dbQuery._id = { $in: req.query._id.toString().split(',') } } } if (req.query.q) { const reg = new RegExp(req.query.q.toString(), 'i'); dbQuery.$or = [ { name: reg }, { 'invites.email': reg }, { 'invites.name': reg }, ] } try { const workspaces = await Workspace.find(dbQuery) .select(req.query.select?.toString().trim().replace(/,/, ' ') || 'name logo tenant labels').lean().exec(); res.status(200).json(workspaces).end() } catch (err) { res.status(500).json({ message: 'Failed to load workspaces' }).end() } } export async function getWorkspaceEncryptedData(req, res) { const tenant = req.headers.tenant as string; if (!tenant) { return res.status(401).end(); } try { const workspace = await getWorkspaceIdIfExists(req.params.workspaceId, tenant); if (!workspace) { throw new Error('workspace not found'); } const encryptedId = req.headers['x-encrypted-id']; const id = workspace._id + (encryptedId ? ('-' + encryptedId) : ''); const { value } = await getEncryptedData(tenant, id, 'workspace'); res.status(200).set('Content-Type', 'application/json').end(value); } catch (e) { res.status(200).json(null).end() } } export async function setWorkspaceEncryptedData(req, res) { const tenant = req.headers.tenant as string; if (!tenant) { return res.status(401).end(); } try { const workspace = await getWorkspaceIdIfExists(req.params.workspaceId, tenant); if (!workspace) { throw new Error('workspace not found'); } const encryptedId = req.headers['x-encrypted-id']; const id = workspace._id + (encryptedId ? ('-' + encryptedId) : ''); await setEncryptedData(tenant, id, JSON.stringify(req.body), 'workspace'); res.status(200).set('Content-Type', 'application/json').end('{}'); } catch (e) { res.status(400).json({ message: 'failed to set encrypted data for user' }).end(); } } export async function createWorkspace(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const userId = req.userPayload.sub; const { name, logo, invites = [], labels = [] } = req.body; const wsConfig = await getWorkspaceConfiguration(tenant); if ( wsConfig.creationPrivilegedRoles?.length && !wsConfig.creationPrivilegedRoles.some(role => role === '*' || req.userPayload.roles.includes(role)) ) { res.status(403).json({ message: 'you are not permitted to create a workspace' }).end(); return; } if (!(labels instanceof Array)) { res.status(400).json({ message: 'please provide all mandatory properties (missing "labels").' }).end(); return; } if (!wsConfig.allowNonLabeledWorkspaces) { if (labels.length === 0) { res.status(400).json({ message: 'please provide labels with valid values.' }).end(); return; } const selectedLabelsDefinition = wsConfig.labels.find(definition => { return definition.value?.length === labels.length && definition.value.map(label => labels.includes(label)).length === labels.length; }) if (!selectedLabelsDefinition) { res.status(400).json({ message: 'provided labels does not match available options.' }).end(); return; } } try { const workspace = new Workspace({ tenant, name, logo, invites, labels }); workspace.members = [{ user: userId, roles: ['admin', 'user'] }]; if (req.userPayload.isPrivileged) { if (req.body.members?.length) { workspace.members = req.body.members; } if (req.body.labels instanceof Array) { workspace.labels = req.body.labels; } } await workspace.save(); res.status(200).json(workspace).end() emitPlatformEvent({ tenant: tenant, user: userId, source: 'auth', kind: 'workspaces', eventName: 'workspace-created', description: 'workspace created by user endpoint', metadata: workspace, }); emitPlatformEvent({ tenant: tenant, user: userId, source: 'auth', kind: 'invites', eventName: 'invite-created', description: 'invites created', metadata: { workspaceId: workspace._id, invites, }, }); } catch (err) { res.status(500).json({ message: 'failed to create workspace' }).end() } } export async function updateWorkspace(req: AuthRequest, res: Response) { const { name, logo, invites, members, labels } = req.body; const workspace = req.workspace; try { if (name) { workspace.name = name; } if (logo) { workspace.logo = logo; } if (invites) { workspace.invites = invites; } if (req.userPayload.isPrivileged) { if (members) { if (!Array.isArray(members)) { return res .status(400) .json({ message: "Invalid data. Must be an array." }) .end(); } if (members.length === 0) { return res .status(400) .json({ message: "Members cannot be empty." }) .end(); } workspace.members = members; } if (labels) { if (!(labels instanceof Array)) { return res .status(400) .json({ message: "Invalid data. Must be an array." }) .end(); } workspace.labels = labels; } } await workspace.save(); res.status(200).json(workspace).end() } catch (err) { logger.log('workspace update error', err); res.status(500).json({ message: 'failed to update workspace' }).end() } } export async function deleteWorkspace(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const userId = req.userPayload.sub; if (req.activeWorkspace?._id && req.workspace._id.toString() === req.activeWorkspace._id) { res.status(400).json({ message: 'You cannot remove your active workspace.' }).end(); return; } try { await req.workspace.deleteOne(); res.status(200).json(req.workspace).end(); emitPlatformEvent({ tenant: tenant, user: userId, source: 'auth', kind: 'workspaces', eventName: 'workspace-deleted', description: 'workspace deleted by user endpoint', metadata: req.workspace }) } catch (err) { res.status(500).json(err.message).end() } } export async function activateWorkspace(req: AuthRequest, res: Response) { const token = getCookieTokenValue(req); const tenant = req.headers.tenant; try { const payload = await verifyToken(token, tenant) as any; const user = await User .findOne({ _id: req.userPayload.sub, tenant }) .select('tenant email fullName firstName lastName roles tokens profileImage') .exec() as any as UserModel; payload.workspace = { _id: req.workspace._id, name: req.workspace.name, roles: req.workspace.members?.[0].roles || ['admin'], labels: req.workspace.labels || [] } const newCookieIdentifier = getUniqueId(); await updateToken( user, 'cookie', payload, newCookieIdentifier ); const { token: newToken } = getSignedToken( user, payload.workspace, newCookieIdentifier, String(cookieTokenExpiration / 1000) ); setCookie(res, getCookieTokenName(tenant), newToken, null, getRequestHost(req)); res.json(req.workspace).end() } catch (err) { res.status(500).json({ message: 'failed to activate workspace' }).end() } } export async function getWorkspaceMembers(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const userId = req.userPayload.sub; const _id = req.params.workspaceId; try { const query: any = { tenant, _id, 'members.user': userId }; if (req.userPayload.isPrivileged) { delete query['members.user']; } const workspace = await Workspace.findOne(query).select('members').lean().exec(); if (!workspace) { res.status(404).json({ message: 'workspace not found', from: 'get-members' }).end(); return; } let users = await User.find({ tenant, _id: workspace.members.map(member => member.user) }).select('_id email fullName firstName lastName').lean().exec(); users = users.map(user => { return { ...user, ...workspace.members.find(member => (user._id as Types.ObjectId).equals(member.user)) } }) res.status(200).json(users).end() } catch (err) { res.status(500).json({ message: 'Failed to load workspace members' }).end() } } export async function addWorkspaceMember(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const { workspaceId } = req.params; const { userId, roles } = req.body; if (!userId || !roles || !Array.isArray(roles)) { return res.status(400).json({ message: 'Invalid input. Provide "userId", "roles".' }).end(); } try { const user = await User.findOne({ tenant, _id: userId }).exec(); if (!user) { return res.status(404).json({ message: 'User not found.' }).end(); } const workspace = await Workspace.findOne({ tenant, _id: workspaceId }).exec(); if (!workspace) { return res.status(404).json({ message: 'Workspace not found.' }).end(); } if (workspace.members.some(member => member.user.toString() === user._id.toString())) { return res.status(400).json({ message: 'User is already a member of the workspace.' }).end(); } workspace.members.push({ user: user._id as Types.ObjectId, roles }); await workspace.save(); res.status(200).json({ message: 'Member added successfully.', workspace }).end(); } catch (err) { res.status(500).json({ message: 'Failed to add member.', error: err.message }).end(); } } export async function deleteWorkspaceMember(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const { userId } = req.params; try { const workspace = await Workspace.findOne({ tenant, 'members.user': userId }).exec(); if (!workspace) { return res.status(404).json({ message: 'Workspace not found.' }); } const memberIndex = workspace.members.findIndex((member: any) => { return member.user.toString() === userId; }); if (memberIndex === -1) { return res.status(404).json({ message: 'Member not found in the workspace.' }); } workspace.members.splice(memberIndex, 1); await workspace.save(); return res.status(200).json({ message: 'Member removed from workspace.', removedMemberId: userId, userId, }); } catch (err) { return res.status(500).json({ message: 'Failed to remove member.', error: err.message }); } } export async function updateWorkspaceMember(req: AuthRequest, res: Response) { const { tenant } = req.headers || {}; const { workspaceId, userId } = req.params; const { roles } = req.body; if (!roles || !Array.isArray(roles)) { return res.status(400).json({ message: 'Invalid input. "roles" must be an array.' }); } try { const workspace = await Workspace.findOne({ tenant, _id: workspaceId }).exec(); if (!workspace) { return res.status(404).json({ message: 'Workspace not found.' }); } const member = workspace.members.find((member: any) => member.user.toString() === userId); if (!member) { return res.status(404).json({ message: 'Member not found in the workspace.' }); } member.roles = [...roles]; await workspace.save(); return res.status(200).json({ message: 'Member roles updated successfully.', updatedMember: member, workspaceId, }); } catch (err) { return res.status(500).json({ message: 'Failed to update member roles.', error: err.message }); } } export async function getWorkspaceByParams(req, res, next) { const { tenant } = req.headers || {}; const _id = req.params.workspaceId; try { const userId = new ObjectId(req.userPayload.sub); const query: any = { tenant, _id, }; const isPrivilegedUser = req.userPayload.isPrivileged; if (!isPrivilegedUser) { query['members.user'] = userId; } const select = isPrivilegedUser ? 'name logo labels' : 'name logo labels members.$'; const workspace = await Workspace.findOne(query).select(select).exec(); if (!workspace) { res.status(404).json({ message: 'workspace not found', from: 'get-workspace' }).end(); return; } req.workspace = workspace; req.isWorkspacePrivileged = isPrivilegedUser || !!req.workspace.members.some(member => (member.user as Types.ObjectId).equals(userId) && member.roles.includes('admin')) next(); } catch { res.status(500).send({ message: 'failed to load workspace data' }); } } export function onlyWorkspacePrivileged(req, res, next) { if (!req.isWorkspacePrivileged) { res.status(403).send({ message: 'not authorized' }); return; } next(); }