feat: add JWT auth with db-backed users
Some checks failed
Build & Release / build (push) Has been cancelled

- users table created on startup via migrate()
- POST /api/auth/setup to create first user (blocked once any user exists)
- POST /api/auth/login returns httpOnly JWT cookie (7d expiry)
- POST /api/auth/logout clears cookie
- GET /api/auth/me for auth check
- All /api/customers routes require valid JWT
- Frontend shows login form when unauthenticated
- Fix type errors in k8s, do, and pgbouncer services
This commit is contained in:
Ryan Moon
2026-04-03 07:41:36 -05:00
parent 8dbfb5810f
commit 4bd1918e3b
11 changed files with 267 additions and 20 deletions

View File

@@ -30,7 +30,7 @@ export async function createDatabaseUser(name: string): Promise<{ name: string;
const res = await doFetch(`/databases/${config.doDbClusterId}/users`, {
method: "POST",
body: JSON.stringify({ name }),
});
}) as { user: { name: string; password: string } };
return res.user;
}
@@ -43,6 +43,6 @@ export async function deleteDatabase(name: string) {
}
export async function getDatabaseHost(): Promise<string> {
const res = await doFetch(`/databases/${config.doDbClusterId}`);
const res = await doFetch(`/databases/${config.doDbClusterId}`) as { database: { connection: { host: string } } };
return res.database.connection.host;
}

View File

@@ -22,7 +22,7 @@ export async function removeCustomerFromPool(slug: string) {
async function addDbEntry(slug: string) {
const cm = await getConfigMap(NAMESPACE, "pgbouncer-config");
const ini = cm["pgbouncer.ini"];
const ini = cm["pgbouncer.ini"] ?? "";
const newLine = ` ${slug} = host=${DO_HOST} port=${DO_PORT} dbname=${slug} user=${slug} pool_mode=session pool_size=3`;
const updated = ini.replace("[pgbouncer]", `${newLine}\n [pgbouncer]`);
await patchConfigMap(NAMESPACE, "pgbouncer-config", { "pgbouncer.ini": updated });
@@ -30,21 +30,21 @@ async function addDbEntry(slug: string) {
async function removeDbEntry(slug: string) {
const cm = await getConfigMap(NAMESPACE, "pgbouncer-config");
const ini = cm["pgbouncer.ini"];
const ini = cm["pgbouncer.ini"] ?? "";
const updated = ini.split("\n").filter((l) => !l.includes(`dbname=${slug}`)).join("\n");
await patchConfigMap(NAMESPACE, "pgbouncer-config", { "pgbouncer.ini": updated });
}
async function addUserEntry(slug: string, password: string) {
const secret = await getSecret(NAMESPACE, "pgbouncer-userlist");
const userlist = secret["userlist.txt"];
const userlist = secret["userlist.txt"] ?? "";
const updated = `${userlist}\n"${slug}" "${password}"`;
await patchSecret(NAMESPACE, "pgbouncer-userlist", { "userlist.txt": updated });
}
async function removeUserEntry(slug: string) {
const secret = await getSecret(NAMESPACE, "pgbouncer-userlist");
const userlist = secret["userlist.txt"];
const userlist = secret["userlist.txt"] ?? "";
const updated = userlist.split("\n").filter((l) => !l.startsWith(`"${slug}"`)).join("\n");
await patchSecret(NAMESPACE, "pgbouncer-userlist", { "userlist.txt": updated });
}