feat: household api token auth foundation
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AdapterAccountType } from "@auth/core/adapters";
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
index,
|
||||
@@ -209,3 +210,27 @@ export const notifications = pgTable(
|
||||
},
|
||||
(t) => [index("notifications_user_read_idx").on(t.userId, t.readAt)],
|
||||
);
|
||||
|
||||
export const householdApiTokens = pgTable(
|
||||
"household_api_tokens",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
householdId: uuid("household_id")
|
||||
.notNull()
|
||||
.references(() => households.id, { onDelete: "cascade" }),
|
||||
tokenHash: text("token_hash").notNull(),
|
||||
name: text("name").notNull().default("default"),
|
||||
createdBy: uuid("created_by")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
lastUsedAt: timestamp("last_used_at", { withTimezone: true }),
|
||||
revokedAt: timestamp("revoked_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex("household_api_tokens_active_household_uq")
|
||||
.on(t.householdId)
|
||||
.where(sql`${t.revokedAt} IS NULL`),
|
||||
index("household_api_tokens_hash_idx").on(t.tokenHash),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user