Extracted domain-specific sub-routers from monolithic `server/routes/admin.ts` and `server/routes/auth.ts` into isolated modules within `server/routes/admin/` and `server/routes/auth/` respectively. The original entry routers were updated to import and assemble these sub-routers without breaking their current HTTP interface or rate limiting/authorization middleware. Testing and linting were run ensuring perfect functionality and 100% test passing score. Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
158 lines
4.6 KiB
TypeScript
158 lines
4.6 KiB
TypeScript
import { Hono } from "jsr:@hono/hono@4";
|
|
import { encodeBase64Url } from "jsr:@std/encoding@1/base64url";
|
|
import { sqlWrapper } from "../../db.ts";
|
|
import { auditWrapper } from "../../audit.ts";
|
|
import { getAuthenticatedUser } from "../../auth-session.ts";
|
|
import { getClientIp } from "../../middleware.ts";
|
|
|
|
export const invitesAdminRoutes = new Hono();
|
|
|
|
invitesAdminRoutes.get("/", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const invites = await sqlWrapper.sql`
|
|
SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at,
|
|
a.name AS app_name, a.id AS app_id,
|
|
u.username AS used_by_username
|
|
FROM invites i
|
|
LEFT JOIN apps a ON i.app_id = a.id
|
|
LEFT JOIN users u ON i.used_by = u.id
|
|
ORDER BY i.created_at DESC
|
|
`;
|
|
return c.json({ invites });
|
|
});
|
|
|
|
invitesAdminRoutes.post("/create", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const {
|
|
appId,
|
|
role,
|
|
expiresInDays,
|
|
customCode,
|
|
usageLimitType,
|
|
maxUses,
|
|
autoActivate,
|
|
} = await c.req.json();
|
|
const assignedRole = (typeof role === "string" && role.trim())
|
|
? role.trim()
|
|
: "user";
|
|
|
|
let validatedAppId = null;
|
|
if (appId) {
|
|
const uuidRegex =
|
|
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
if (typeof appId !== "string" || !uuidRegex.test(appId)) {
|
|
return c.json({ error: "appId must be a valid UUID string" }, 400);
|
|
}
|
|
const appExists = await sqlWrapper
|
|
.sql`SELECT id FROM apps WHERE id = ${appId}`.then(
|
|
(res: any) => res[0],
|
|
);
|
|
if (!appExists) {
|
|
return c.json({ error: "Target application does not exist" }, 404);
|
|
}
|
|
validatedAppId = appId;
|
|
}
|
|
|
|
let parsedMaxUses: number | null = 1;
|
|
if (usageLimitType === "unlimited") {
|
|
parsedMaxUses = null;
|
|
} else if (usageLimitType === "limited") {
|
|
const n = parseInt(maxUses);
|
|
parsedMaxUses = (!isNaN(n) && n > 0) ? n : 5;
|
|
} else {
|
|
parsedMaxUses = 1; // single-use default
|
|
}
|
|
|
|
const shouldAutoActivate = autoActivate !== false;
|
|
|
|
const inviteCode =
|
|
(customCode && typeof customCode === "string" && customCode.trim())
|
|
? customCode.trim()
|
|
: encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));
|
|
|
|
const days = Number(expiresInDays) || 7;
|
|
if (!Number.isInteger(days) || days < 1 || days > 30) {
|
|
return c.json({
|
|
error: "expiresInDays must be an integer between 1 and 30",
|
|
}, 400);
|
|
}
|
|
|
|
const expiresAt = new Date();
|
|
expiresAt.setDate(expiresAt.getDate() + days);
|
|
|
|
try {
|
|
await sqlWrapper.sql`
|
|
INSERT INTO invites (code, app_id, role, created_by, max_uses, uses_count, auto_activate, expires_at)
|
|
VALUES (${inviteCode}, ${validatedAppId}, ${assignedRole}, ${auth.userId}, ${parsedMaxUses}, 0, ${shouldAutoActivate}, ${expiresAt})
|
|
`;
|
|
} catch (err: any) {
|
|
if (err.code === "23505") {
|
|
return c.json({ error: "An invite with this code already exists" }, 409);
|
|
}
|
|
return c.json({ error: "Failed to generate invite" }, 500);
|
|
}
|
|
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"invite_created",
|
|
validatedAppId,
|
|
{
|
|
code: inviteCode,
|
|
role: assignedRole,
|
|
max_uses: parsedMaxUses,
|
|
auto_activate: shouldAutoActivate,
|
|
expiresInDays: days,
|
|
},
|
|
getClientIp(c),
|
|
);
|
|
|
|
return c.json({
|
|
success: true,
|
|
inviteCode,
|
|
expiresAt,
|
|
maxUses: parsedMaxUses,
|
|
autoActivate: shouldAutoActivate,
|
|
});
|
|
});
|
|
|
|
invitesAdminRoutes.get("/:id/redemptions", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const inviteId = c.req.param("id");
|
|
const redemptions = await sqlWrapper.sql`
|
|
SELECT ir.id, ir.redeemed_at, u.id AS user_id, u.username, u.display_name, u.account_status
|
|
FROM invite_redemptions ir
|
|
JOIN users u ON ir.user_id = u.id
|
|
WHERE ir.invite_id = ${inviteId}
|
|
ORDER BY ir.redeemed_at DESC
|
|
`;
|
|
|
|
return c.json({ redemptions });
|
|
});
|
|
|
|
invitesAdminRoutes.delete("/:id", async (c) => {
|
|
const auth = await getAuthenticatedUser(c);
|
|
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
|
|
|
const inviteId = c.req.param("id");
|
|
const invite = await sqlWrapper
|
|
.sql`DELETE FROM invites WHERE id = ${inviteId} RETURNING id, code`
|
|
.then((res: any) => res[0]);
|
|
if (invite) {
|
|
auditWrapper.auditLog(
|
|
auth.userId,
|
|
"invite_revoked",
|
|
inviteId,
|
|
{ code: invite.code },
|
|
getClientIp(c),
|
|
);
|
|
return c.json({ success: true });
|
|
}
|
|
return c.json({ error: "Invite not found" }, 404);
|
|
});
|