feat: auth foundation, similar tools, costs, redundancy, anonymous voting
This commit is contained in:
@@ -52,6 +52,46 @@ async function getClient(): Promise<Client | null> {
|
||||
}
|
||||
}
|
||||
|
||||
async function upsertUserFromOidc(userinfo: Record<string, unknown>): Promise<{ id: number; tier: string; role: string }> {
|
||||
const sub = String(userinfo.sub ?? "");
|
||||
if (!sub) throw new Error("Missing sub claim");
|
||||
|
||||
const email = typeof userinfo.email === "string" ? userinfo.email : undefined;
|
||||
const preferredUsername = typeof userinfo.preferred_username === "string" ? userinfo.preferred_username : undefined;
|
||||
const name = typeof userinfo.name === "string" ? userinfo.name : preferredUsername;
|
||||
const username = preferredUsername ?? email ?? `oidc-${sub.slice(0, 8)}`;
|
||||
|
||||
const [existing] = await db
|
||||
.select({ id: usersTable.id, tier: usersTable.tier, role: usersTable.role })
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.authProviderId, sub))
|
||||
.limit(1);
|
||||
|
||||
if (existing) {
|
||||
await db
|
||||
.update(usersTable)
|
||||
.set({ email, displayName: name })
|
||||
.where(eq(usersTable.id, existing.id));
|
||||
return existing;
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(usersTable)
|
||||
.values({
|
||||
username,
|
||||
email,
|
||||
displayName: name,
|
||||
role: "user",
|
||||
tier: "free",
|
||||
authProvider: "oidc",
|
||||
authProviderId: sub,
|
||||
})
|
||||
.returning({ id: usersTable.id, tier: usersTable.tier, role: usersTable.role });
|
||||
|
||||
logger.info({ username, sub }, "OIDC user created");
|
||||
return created;
|
||||
}
|
||||
|
||||
router.get("/auth/mode", (_req, res): void => {
|
||||
res.json({ mode: isOidcConfigured() ? "oidc" : "local" });
|
||||
});
|
||||
@@ -74,7 +114,7 @@ router.post("/auth/login", async (req, res): Promise<void> => {
|
||||
.where(eq(usersTable.username, String(username)))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
if (!user || !user.passwordHash) {
|
||||
res.status(401).json({ error: "Invalid username or password" });
|
||||
return;
|
||||
}
|
||||
@@ -91,6 +131,7 @@ router.post("/auth/login", async (req, res): Promise<void> => {
|
||||
preferred_username: user.username,
|
||||
email: user.email ?? undefined,
|
||||
role: (user.role as "admin" | "user") ?? "user",
|
||||
tier: (user.tier as "free" | "premium" | "enterprise") ?? "free",
|
||||
isLocal: true,
|
||||
};
|
||||
|
||||
@@ -100,6 +141,7 @@ router.post("/auth/login", async (req, res): Promise<void> => {
|
||||
name: user.username,
|
||||
preferredUsername: user.username,
|
||||
role: user.role,
|
||||
tier: user.tier,
|
||||
isLocal: true,
|
||||
});
|
||||
});
|
||||
@@ -152,13 +194,15 @@ router.get("/auth/callback", async (req, res): Promise<void> => {
|
||||
});
|
||||
|
||||
const userinfo = await client.userinfo(tokenSet.access_token!);
|
||||
const dbUser = await upsertUserFromOidc(userinfo);
|
||||
|
||||
req.session.user = {
|
||||
sub: userinfo.sub,
|
||||
sub: dbUser.id.toString(),
|
||||
email: typeof userinfo.email === "string" ? userinfo.email : undefined,
|
||||
name: typeof userinfo.name === "string" ? userinfo.name : undefined,
|
||||
preferred_username: typeof userinfo.preferred_username === "string" ? userinfo.preferred_username : undefined,
|
||||
role: "user",
|
||||
role: (dbUser.role as "admin" | "user") ?? "user",
|
||||
tier: (dbUser.tier as "free" | "premium" | "enterprise") ?? "free",
|
||||
isLocal: false,
|
||||
};
|
||||
delete req.session.codeVerifier;
|
||||
@@ -198,6 +242,7 @@ router.get("/auth/me", async (req, res): Promise<void> => {
|
||||
name: u.name ?? null,
|
||||
preferredUsername: u.preferred_username ?? null,
|
||||
role: u.role ?? "user",
|
||||
tier: u.tier ?? "free",
|
||||
isLocal: u.isLocal ?? false,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user