const invitesService = require("../services/group-invites.service"); const { sendError } = require("../utils/http"); const { logError } = require("../utils/logger"); const { inviteCodeLast4 } = require("../utils/redaction"); function getClientIp(req) { const forwardedFor = req.headers["x-forwarded-for"]; if (typeof forwardedFor === "string" && forwardedFor.trim()) { return forwardedFor.split(",")[0].trim(); } return req.ip || req.socket?.remoteAddress || null; } function parseRequestedGroupId(req) { const headerGroupId = req.headers["x-group-id"] || req.headers["x-household-id"]; if (headerGroupId) { const raw = Array.isArray(headerGroupId) ? headerGroupId[0] : headerGroupId; return raw; } if (req.query?.groupId !== undefined) { return req.query.groupId; } if (req.body?.groupId !== undefined) { return req.body.groupId; } return undefined; } function clampTtlDays(value) { const parsed = Number.parseInt(value, 10); if (!Number.isInteger(parsed)) return 1; return Math.max(1, Math.min(7, parsed)); } function mapServiceError(req, res, error, context, extraLog = {}) { if (error instanceof invitesService.InviteServiceError) { return sendError(res, error.statusCode, error.message, error.code); } logError(req, context, error, extraLog); return sendError(res, 500, "Failed to process invite request"); } exports.listInviteLinks = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); const links = await invitesService.listInviteLinks(req.user.id, groupId); res.json({ links }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.listInviteLinks"); } }; exports.createInviteLink = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); const ttlDays = clampTtlDays(req.body?.ttlDays); const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000); const link = await invitesService.createInviteLink( req.user.id, groupId, req.body?.policy, Boolean(req.body?.singleUse), expiresAt, req.request_id, getClientIp(req), req.headers["user-agent"] || null ); res.status(201).json({ link }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.createInviteLink"); } }; exports.revokeInviteLink = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); await invitesService.revokeInviteLink( req.user.id, groupId, req.body?.linkId, req.request_id, getClientIp(req), req.headers["user-agent"] || null ); res.json({ ok: true }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.revokeInviteLink"); } }; exports.reviveInviteLink = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); const ttlDays = clampTtlDays(req.body?.ttlDays); const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000); await invitesService.reviveInviteLink( req.user.id, groupId, req.body?.linkId, expiresAt, req.request_id, getClientIp(req), req.headers["user-agent"] || null ); res.json({ ok: true }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.reviveInviteLink"); } }; exports.deleteInviteLink = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); await invitesService.deleteInviteLink( req.user.id, groupId, req.body?.linkId, req.request_id, getClientIp(req), req.headers["user-agent"] || null ); res.json({ ok: true }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.deleteInviteLink"); } }; exports.getJoinPolicy = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); const joinPolicy = await invitesService.getGroupJoinPolicy(req.user.id, groupId); res.json({ joinPolicy }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.getJoinPolicy"); } }; exports.setJoinPolicy = async (req, res) => { try { const requestedGroupId = parseRequestedGroupId(req); const groupId = await invitesService.resolveManagedGroupId( req.user.id, requestedGroupId ); await invitesService.setGroupJoinPolicy( req.user.id, groupId, req.body?.joinPolicy, req.request_id, getClientIp(req), req.headers["user-agent"] || null ); res.json({ ok: true }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.setJoinPolicy"); } }; exports.getInviteLinkSummary = async (req, res) => { const token = req.params.token; const inviteLast4 = inviteCodeLast4(token); try { const link = await invitesService.getInviteLinkSummaryByToken( token, req.user?.id || null ); res.json({ link }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.getInviteLinkSummary", { invite_last4: inviteLast4, }); } }; exports.acceptInviteLink = async (req, res) => { const token = req.params.token; const inviteLast4 = inviteCodeLast4(token); try { const result = await invitesService.acceptInviteLink( req.user.id, token, req.request_id, getClientIp(req), req.headers["user-agent"] || null ); res.json({ result }); } catch (error) { return mapServiceError(req, res, error, "groupInvites.acceptInviteLink", { invite_last4: inviteLast4, }); } };