re-vibecode

This commit is contained in:
Debian
2026-03-30 13:57:39 +00:00
parent 09d76c044b
commit bb8497d33d
29 changed files with 1040 additions and 1622 deletions
+102 -146
View File
@@ -1,21 +1,43 @@
import { Elysia } from "elysia";
import { login, getAnonymousSession, getSessionFromRequest, seedAdminAccount, createTotpSecret, enableTotp, disableTotp } from "./auth/service";
import { HttpError } from "./lib/errors";
import { errorResponse, fileResponse, jsonResponse, parseJson, emptyResponse } from "./lib/http";
import { loginAdmin, logoutAdmin, requireAdminSession, getSessionFromRequest, seedAdminAccount } from "./auth/service";
import { cleanupDatabase } from "./lib/db";
import { errorResponse, jsonResponse, emptyResponse, optionsResponse, parseJson, assertAllowedOrigin } from "./lib/http";
import { config } from "./lib/config";
import { assertAllowedOrigin, enforceLimiter, globalLimiter, loginLimiter, routeLimiter } from "./lib/security";
import { composeSchema, forwardingSchema, loginSchema, messageIdSchema, profileSchema, strictEmailSchema, totpCodeSchema } from "./lib/validation";
import { composeAndSendMessage, getMailboxFolderLabel, getMessageForMailbox, listMessagesForMailbox } from "./mail/service";
import { getAvatarFile, getForwardingSettings, getProfileSettings, saveAvatar, updateDisplayName, updateForwarding } from "./settings/service";
import {
createUserSchema,
loginSchema,
parseRouteEmail,
resetPasswordSchema,
resetTokenSchema,
updatePasswordSchema
} from "./lib/validation";
import {
consumePasswordReset,
createUser,
deleteUser,
inspectPasswordReset,
listUsers,
triggerPasswordReset,
updateUserPassword
} from "./users/service";
const app = new Elysia();
const route = (handler: (request: Request, params?: Record<string, string>) => Promise<Response> | Response) =>
const route = (
handler: (context: {
request: Request;
params: Record<string, string>;
}) => Promise<Response> | Response
) =>
async ({ request, params }: { request: Request; params: Record<string, string> }) => {
if (request.method === "OPTIONS") {
return optionsResponse(request);
}
try {
assertAllowedOrigin(request);
enforceLimiter(globalLimiter, request, "global", config.globalRateLimit, config.rateLimitWindowMs);
return await handler(request, params);
cleanupDatabase();
return await handler({ request, params });
} catch (error) {
return errorResponse(request, error);
}
@@ -23,179 +45,113 @@ const route = (handler: (request: Request, params?: Record<string, string>) => P
await seedAdminAccount();
app.onRequest(({ request }) => {
if (request.method === "OPTIONS") {
return emptyResponse(request, 204);
}
});
app.get(
"/api/health",
route(async (request) =>
route(async ({ request }) =>
jsonResponse(request, 200, {
ok: true,
mode: config.nodeEnv,
folder: getMailboxFolderLabel()
ok: true
})
)
);
app.get(
"/api/auth/session",
route(async (request) => {
try {
const session = await getSessionFromRequest(request);
return jsonResponse(request, 200, { session: session.session });
} catch {
return jsonResponse(request, 200, { session: getAnonymousSession() });
}
})
route(async ({ request }) =>
jsonResponse(request, 200, {
session: getSessionFromRequest(request)
})
)
);
app.post(
"/api/auth/login",
route(async (request) => {
if (!config.allowInsecureLogin) {
enforceLimiter(loginLimiter, request, "login", config.loginRateLimit, config.loginWindowMs);
}
route(async ({ request }) => {
const body = await parseJson(request, loginSchema);
const result = await login(body.email, body.password, body.totpCode, request);
const result = await loginAdmin(body.email, body.password);
return jsonResponse(
request,
200,
{ session: result.session }
{ session: result.session },
{ "set-cookie": result.cookie }
);
})
);
app.post(
"/api/auth/logout",
route(async (request) => {
return jsonResponse(request, 200, { session: getAnonymousSession() });
})
);
app.post(
"/api/auth/totp/setup",
route(async (request) => {
const session = await getSessionFromRequest(request);
const setup = await createTotpSecret(session.email);
return jsonResponse(request, 200, setup);
})
);
app.post(
"/api/auth/totp/enable",
route(async (request) => {
const session = await getSessionFromRequest(request);
const body = await parseJson(request, totpCodeSchema);
await enableTotp(session.email, body.code);
return emptyResponse(request, 204);
})
);
app.post(
"/api/auth/totp/disable",
route(async (request) => {
const session = await getSessionFromRequest(request);
const body = await parseJson(request, totpCodeSchema);
await disableTotp(session.email, body.code);
return emptyResponse(request, 204);
})
route(async ({ request }) =>
jsonResponse(
request,
200,
{ session: { authenticated: false, adminEmail: null } },
{ "set-cookie": logoutAdmin(request) }
)
)
);
app.get(
"/api/settings/profile",
route(async (request) => {
const session = await getSessionFromRequest(request);
return jsonResponse(request, 200, getProfileSettings(session.email));
"/api/users",
route(async ({ request }) => {
requireAdminSession(request);
return jsonResponse(request, 200, { items: await listUsers() });
})
);
app.post(
"/api/users",
route(async ({ request }) => {
const session = requireAdminSession(request);
const body = await parseJson(request, createUserSchema);
const result = await createUser(body.email, session.adminEmail);
return jsonResponse(request, 201, result);
})
);
app.patch(
"/api/settings/profile",
route(async (request) => {
const session = await getSessionFromRequest(request);
const body = await parseJson(request, profileSchema);
return jsonResponse(request, 200, updateDisplayName(session.email, body.displayName));
"/api/users/:email/password",
route(async ({ request, params }) => {
requireAdminSession(request);
const email = parseRouteEmail(params.email);
const body = await parseJson(request, updatePasswordSchema);
await updateUserPassword(email, body.password);
return emptyResponse(request, 204);
})
);
app.delete(
"/api/users/:email",
route(async ({ request, params }) => {
requireAdminSession(request);
const email = parseRouteEmail(params.email);
await deleteUser(email);
return emptyResponse(request, 204);
})
);
app.post(
"/api/settings/avatar",
route(async (request) => {
const session = await getSessionFromRequest(request);
const formData = await request.formData();
const avatar = formData.get("avatar");
if (!(avatar instanceof File)) {
throw new HttpError(400, "Avatar upload is required.");
}
const profile = await saveAvatar(session.email, avatar);
return jsonResponse(request, 200, profile);
})
);
app.get(
"/api/settings/avatar/:email",
route(async (request, params) => {
const session = await getSessionFromRequest(request);
const targetEmail = strictEmailSchema.parse(params?.email ?? "");
if (targetEmail !== session.email && session.role !== "admin") {
throw new HttpError(403, "You can only access your own avatar.");
}
const file = getAvatarFile(targetEmail);
return fileResponse(request, file, file.type || "application/octet-stream");
})
);
app.get(
"/api/settings/forwarding",
route(async (request) => {
const session = await getSessionFromRequest(request);
return jsonResponse(request, 200, getForwardingSettings(session.email));
})
);
app.put(
"/api/settings/forwarding",
route(async (request) => {
const session = await getSessionFromRequest(request);
const body = await parseJson(request, forwardingSchema);
const rule = await updateForwarding(session.email, body.enabled, body.target);
return jsonResponse(request, 200, rule);
})
);
app.get(
"/api/mail/messages",
route(async (request) => {
const session = await getSessionFromRequest(request);
enforceLimiter(routeLimiter, request, "messages", config.routeRateLimit, config.rateLimitWindowMs, session.email);
const items = await listMessagesForMailbox(session.email);
return jsonResponse(request, 200, { items });
})
);
app.get(
"/api/mail/messages/:id",
route(async (request, params) => {
const session = await getSessionFromRequest(request);
enforceLimiter(routeLimiter, request, "message-detail", config.routeRateLimit, config.rateLimitWindowMs, session.email);
const { id } = messageIdSchema.parse({ id: params?.id ?? "" });
const item = await getMessageForMailbox(session.email, id);
return jsonResponse(request, 200, item);
"/api/users/:email/password-reset",
route(async ({ request, params }) => {
const session = requireAdminSession(request);
const email = parseRouteEmail(params.email);
const result = await triggerPasswordReset(email, session.adminEmail);
return jsonResponse(request, 200, result);
})
);
app.post(
"/api/mail/send",
route(async (request) => {
const session = await getSessionFromRequest(request);
enforceLimiter(routeLimiter, request, "send-mail", 10, 60_000, session.email);
const body = await parseJson(request, composeSchema);
await composeAndSendMessage(session.email, body.to, body.subject, body.textBody);
"/api/password-resets/inspect",
route(async ({ request }) => {
const body = await parseJson(request, resetTokenSchema);
return jsonResponse(request, 200, await inspectPasswordReset(body.token));
})
);
app.post(
"/api/password-resets/consume",
route(async ({ request }) => {
const body = await parseJson(request, resetPasswordSchema);
await consumePasswordReset(body.token, body.password);
return emptyResponse(request, 204);
})
);
@@ -205,4 +161,4 @@ app.listen({
port: config.port
});
console.log(`Secure webmail API listening on http://${config.host}:${config.port}`);
console.log(`Mail admin API listening on http://${config.host}:${config.port}`);