103 lines
2.6 KiB
TypeScript
103 lines
2.6 KiB
TypeScript
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 };
|
|
}
|