feat: household api token auth foundation

This commit is contained in:
ginnoir
2026-07-04 18:57:31 -05:00
parent 2c0c31540e
commit ea5d1d050c
11 changed files with 465 additions and 32 deletions
+102
View File
@@ -0,0 +1,102 @@
import { createHash, randomBytes } from "crypto";
import { and, eq, isNull } from "drizzle-orm";
import { db } from "@/lib/db";
import { householdApiTokens } from "./schema";
export type HouseholdApiTokenStatus = {
hasActiveToken: boolean;
lastUsedAt: Date | null;
createdAt: Date | null;
};
export type CreateHouseholdApiTokenResult = {
token: string;
};
export function hashApiToken(raw: string): string {
return createHash("sha256").update(raw).digest("hex");
}
function generateRawToken(): string {
return randomBytes(32).toString("base64url");
}
export async function createHouseholdApiToken(
householdId: string,
createdByUserId: string,
): Promise<CreateHouseholdApiTokenResult> {
await db
.update(householdApiTokens)
.set({ revokedAt: new Date() })
.where(
and(eq(householdApiTokens.householdId, householdId), isNull(householdApiTokens.revokedAt)),
);
const rawToken = generateRawToken();
const tokenHash = hashApiToken(rawToken);
await db.insert(householdApiTokens).values({
householdId,
tokenHash,
createdBy: createdByUserId,
});
return { token: rawToken };
}
export async function revokeHouseholdApiToken(householdId: string): Promise<void> {
await db
.update(householdApiTokens)
.set({ revokedAt: new Date() })
.where(
and(eq(householdApiTokens.householdId, householdId), isNull(householdApiTokens.revokedAt)),
);
}
export async function getHouseholdApiTokenStatus(
householdId: string,
): Promise<HouseholdApiTokenStatus> {
const [row] = await db
.select({
lastUsedAt: householdApiTokens.lastUsedAt,
createdAt: householdApiTokens.createdAt,
})
.from(householdApiTokens)
.where(
and(eq(householdApiTokens.householdId, householdId), isNull(householdApiTokens.revokedAt)),
)
.limit(1);
if (!row) {
return { hasActiveToken: false, lastUsedAt: null, createdAt: null };
}
return {
hasActiveToken: true,
lastUsedAt: row.lastUsedAt,
createdAt: row.createdAt,
};
}
export async function resolveBearerToken(
rawToken: string,
): Promise<{ householdId: string; userId: null } | null> {
if (!rawToken) return null;
const tokenHash = hashApiToken(rawToken);
const [row] = await db
.select({ id: householdApiTokens.id, householdId: householdApiTokens.householdId })
.from(householdApiTokens)
.where(and(eq(householdApiTokens.tokenHash, tokenHash), isNull(householdApiTokens.revokedAt)))
.limit(1);
if (!row) return null;
void db
.update(householdApiTokens)
.set({ lastUsedAt: new Date() })
.where(eq(householdApiTokens.id, row.id));
return { householdId: row.householdId, userId: null };
}