Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
195 lines
6 KiB
JavaScript
195 lines
6 KiB
JavaScript
/**
|
|
* Spaces Invitations Routes (ADR-105)
|
|
* GET /:id/invitations, POST /:id/invitations,
|
|
* DELETE /:id/invitations/:invitationId, POST /:id/invitations/:invitationId/resend,
|
|
* GET /invitations/:token, POST /invitations/:token/accept
|
|
*/
|
|
|
|
import { getInvitationsBySpace, createInvitation, revokeInvitation, resendInvitation, getInvitationByToken, acceptInvitation } from '../../../services/InvitationService.js';
|
|
import { apiLogger } from '../../../utils/logger.js';
|
|
import { success, created, error, badRequest, notFound } from '../../../utils/response.js';
|
|
import { requireOwnerOrAdmin } from './visibility.js';
|
|
|
|
export default function registerInvitationRoutes(router) {
|
|
/**
|
|
* GET /spaces/:id/invitations
|
|
* List all invitations for a space.
|
|
* Requires admin+ access to the space.
|
|
*/
|
|
router.get('/:id/invitations', async (req, res) => {
|
|
try {
|
|
const result = await requireOwnerOrAdmin(req, res);
|
|
if (!result) return;
|
|
|
|
const invitations = await getInvitationsBySpace(result.space.id);
|
|
|
|
success(res, invitations);
|
|
} catch (err) {
|
|
apiLogger.error('Error fetching invitations:', err);
|
|
error(res, 'FETCH_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /spaces/:id/invitations
|
|
* Create a new invitation for a space.
|
|
* Body: { email, role }
|
|
* Requires admin+ access to the space.
|
|
*/
|
|
router.post('/:id/invitations', async (req, res) => {
|
|
try {
|
|
const result = await requireOwnerOrAdmin(req, res);
|
|
if (!result) return;
|
|
|
|
const { email, role } = req.body;
|
|
|
|
if (!email || typeof email !== 'string' || !email.includes('@')) {
|
|
return badRequest(res, 'A valid email address is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
if (!role) {
|
|
return badRequest(res, 'role is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
const invitation = await createInvitation(result.space.id, req.user.id, email, role);
|
|
|
|
created(res, invitation);
|
|
} catch (err) {
|
|
apiLogger.error('Error creating invitation:', err);
|
|
|
|
if (err.message.includes('Invalid role') || err.message.includes('required') || err.message.includes('already exists')) {
|
|
return badRequest(res, err.message, 'VALIDATION_ERROR');
|
|
}
|
|
|
|
error(res, 'CREATE_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /spaces/:id/invitations/:invitationId
|
|
* Revoke a pending invitation.
|
|
* Requires admin+ access to the space.
|
|
*/
|
|
router.delete('/:id/invitations/:invitationId', async (req, res) => {
|
|
try {
|
|
const result = await requireOwnerOrAdmin(req, res);
|
|
if (!result) return;
|
|
|
|
const invitationId = parseInt(req.params.invitationId);
|
|
|
|
if (!invitationId || isNaN(invitationId)) {
|
|
return badRequest(res, 'Valid invitationId is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
const revokeResult = await revokeInvitation(invitationId, req.user.id);
|
|
|
|
success(res, revokeResult);
|
|
} catch (err) {
|
|
apiLogger.error('Error revoking invitation:', err);
|
|
|
|
if (err.message.includes('not found')) {
|
|
return notFound(res, 'Invitation');
|
|
}
|
|
if (err.message.includes('Cannot revoke')) {
|
|
return badRequest(res, err.message, 'VALIDATION_ERROR');
|
|
}
|
|
|
|
error(res, 'DELETE_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /spaces/:id/invitations/:invitationId/resend
|
|
* Resend an invitation (regenerate token and extend expiry).
|
|
* Requires admin+ access to the space.
|
|
*/
|
|
router.post('/:id/invitations/:invitationId/resend', async (req, res) => {
|
|
try {
|
|
const result = await requireOwnerOrAdmin(req, res);
|
|
if (!result) return;
|
|
|
|
const invitationId = parseInt(req.params.invitationId);
|
|
|
|
if (!invitationId || isNaN(invitationId)) {
|
|
return badRequest(res, 'Valid invitationId is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
const updated = await resendInvitation(invitationId, req.user.id);
|
|
|
|
success(res, updated);
|
|
} catch (err) {
|
|
apiLogger.error('Error resending invitation:', err);
|
|
|
|
if (err.message.includes('not found')) {
|
|
return notFound(res, 'Invitation');
|
|
}
|
|
if (err.message.includes('Cannot resend')) {
|
|
return badRequest(res, err.message, 'VALIDATION_ERROR');
|
|
}
|
|
|
|
error(res, 'RESEND_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// Invitation Acceptance (ADR-105)
|
|
// Routes: /spaces/invitations/:token (mounted under /api/v3/spaces)
|
|
// ============================================================
|
|
|
|
/**
|
|
* GET /spaces/invitations/:token
|
|
* Get invitation details by token. Auth required to see full details.
|
|
*/
|
|
router.get('/invitations/:token', async (req, res) => {
|
|
try {
|
|
const { token } = req.params;
|
|
|
|
if (!token) {
|
|
return badRequest(res, 'Invitation token is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
const invitation = await getInvitationByToken(token);
|
|
|
|
if (!invitation) {
|
|
return notFound(res, 'Invitation');
|
|
}
|
|
|
|
success(res, invitation);
|
|
} catch (err) {
|
|
apiLogger.error('Error fetching invitation by token:', err);
|
|
error(res, 'FETCH_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /spaces/invitations/:token/accept
|
|
* Accept an invitation. Auth required.
|
|
*/
|
|
router.post('/invitations/:token/accept', async (req, res) => {
|
|
try {
|
|
const { token } = req.params;
|
|
|
|
if (!token) {
|
|
return badRequest(res, 'Invitation token is required', 'VALIDATION_ERROR');
|
|
}
|
|
|
|
const result = await acceptInvitation(token);
|
|
|
|
success(res, result);
|
|
} catch (err) {
|
|
apiLogger.error('Error accepting invitation:', err);
|
|
|
|
if (err.message.includes('not found') || err.message.includes('already used')) {
|
|
return notFound(res, 'Invitation');
|
|
}
|
|
if (err.message.includes('expired')) {
|
|
return badRequest(res, err.message, 'INVITATION_EXPIRED');
|
|
}
|
|
if (err.message.includes('must register')) {
|
|
return badRequest(res, err.message, 'USER_NOT_REGISTERED');
|
|
}
|
|
|
|
error(res, 'ACCEPT_ERROR', err.message, 500);
|
|
}
|
|
});
|
|
}
|