Files
Primora/apps/auth/src/index.ts
T
Tomas DvorakandDevin 7548ea75d4 fix: skip email verification entirely when no mail transport exists
SMTP-less self-hosted instances still marked every account
emailVerified=false and attempted a verification mail that could
never arrive. Now: no transport → accounts are created verified,
the send is skipped, and existing unverified rows are backfilled
on startup. .env.example stops defaulting SMTP_HOST to the
dev-only mailpit service.

Verified live: fresh signup returns emailVerified=true, legacy
unverified users flip on restart, sign-in unaffected.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 15:43:26 +02:00

88 lines
2.4 KiB
TypeScript

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { logger } from "hono/logger";
import { requestId } from "hono/request-id";
import { HTTPException } from "hono/http-exception";
import { createClient } from "redis";
import { auth, promoteAdminEmails, runAuthMigrations, verifyUsersWithoutMailTransport } from "./lib/auth.js";
import { authPool } from "./lib/db.js";
import { env } from "./lib/env.js";
const redisClient = createClient({
url: env.DRAGONFLY_URL,
});
try {
await redisClient.connect();
} catch (error) {
console.warn(JSON.stringify({ level: "warn", msg: "dragonfly unavailable", error }));
}
await retry("auth_migrations", runAuthMigrations);
await verifyUsersWithoutMailTransport();
await promoteAdminEmails();
const app = new Hono();
app.use("*", requestId());
app.use("*", logger());
app.use("/auth/*", async (c, next) => {
if (!redisClient.isOpen) {
await next();
return;
}
const identifier = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
const windowKey = `rate-limit:auth:${identifier}:${new Date().toISOString().slice(0, 16)}`;
const count = await redisClient.incr(windowKey);
await redisClient.expire(windowKey, 60);
if (count > 60) {
throw new HTTPException(429, { message: "Too many auth requests" });
}
await next();
});
app.get("/health", async (c) => {
let db = "ok";
let cache = redisClient.isOpen ? "ok" : "disabled";
try {
await authPool.query("select 1");
} catch (error) {
db = error instanceof Error ? error.message : "error";
}
if (redisClient.isOpen) {
try {
await redisClient.ping();
} catch (error) {
cache = error instanceof Error ? error.message : "error";
}
}
return c.json({
status: db === "ok" ? "ok" : "degraded",
checks: { database: db, dragonfly: cache },
});
});
app.on(["GET", "POST"], "/auth/*", (c) => auth.handler(c.req.raw));
serve({
fetch: app.fetch,
port: env.AUTH_PORT,
});
async function retry(label: string, fn: () => Promise<void>) {
let lastError: unknown;
for (let attempt = 1; attempt <= 20; attempt += 1) {
try {
await fn();
return;
} catch (error) {
lastError = error;
console.warn(JSON.stringify({ level: "warn", msg: `${label}_retry`, attempt, error }));
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
throw lastError;
}