feat(16-01): update auth middleware and services to resolve userId
- verifyApiKey returns { userId } | null instead of boolean
- verifyAccessToken returns { userId } | null instead of boolean
- Add getOrCreateUser upsert function in auth.service
- Add getOrCreateUncategorized helper in category.service
- requireAuth sets userId on Hono context for all 3 auth methods
- Remove GET bypass: all API routes require auth for userId resolution
- Keep bypass for /api/auth and /api/health paths
This commit is contained in:
@@ -54,13 +54,13 @@ app.use("/api/*", async (c, next) => {
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auth middleware for write operations (POST/PUT/PATCH/DELETE) on non-auth routes
|
// Auth middleware for all data routes (userId must be available for per-user scoping)
|
||||||
app.use("/api/*", async (c, next) => {
|
app.use("/api/*", async (c, next) => {
|
||||||
// Skip auth routes — they handle their own auth
|
// Skip auth routes — they handle their own auth
|
||||||
if (c.req.path.startsWith("/api/auth")) return next();
|
if (c.req.path.startsWith("/api/auth")) return next();
|
||||||
// Skip GET requests — read is public
|
// Skip health check
|
||||||
if (c.req.method === "GET") return next();
|
if (c.req.path === "/api/health") return next();
|
||||||
// All other methods require auth
|
// All methods require auth for userId resolution
|
||||||
return requireAuth(c, next);
|
return requireAuth(c, next);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,46 @@
|
|||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import { getCookie } from "hono/cookie";
|
import { getOrCreateUser, verifyApiKey } from "../services/auth.service";
|
||||||
import {
|
import { getOrCreateUncategorized } from "../services/category.service";
|
||||||
getSession,
|
import { verifyAccessToken } from "../services/oauth.service";
|
||||||
getUserCount,
|
|
||||||
refreshSession,
|
|
||||||
verifyApiKey,
|
|
||||||
} from "../services/auth.service";
|
|
||||||
|
|
||||||
export async function requireAuth(c: Context, next: Next) {
|
export async function requireAuth(c: Context, next: Next) {
|
||||||
const db = c.get("db");
|
const db = c.get("db");
|
||||||
|
|
||||||
// Check if any users exist at all
|
|
||||||
if (getUserCount(db) === 0) {
|
|
||||||
return c.json({ error: "setup_required" }, 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check API key first
|
// Check API key first
|
||||||
const apiKey = c.req.header("X-API-Key");
|
const apiKey = c.req.header("X-API-Key");
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
const valid = await verifyApiKey(db, apiKey);
|
const result = await verifyApiKey(db, apiKey);
|
||||||
if (valid) return next();
|
if (result) {
|
||||||
|
c.set("userId", result.userId);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
return c.json({ error: "Invalid API key" }, 401);
|
return c.json({ error: "Invalid API key" }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check session cookie
|
// Check OAuth Bearer token
|
||||||
const sessionId = getCookie(c, "gearbox_session");
|
const authHeader = c.req.header("Authorization");
|
||||||
if (sessionId) {
|
if (authHeader?.startsWith("Bearer ")) {
|
||||||
const session = getSession(db, sessionId);
|
const token = authHeader.slice(7);
|
||||||
if (session) {
|
const result = await verifyAccessToken(db, token);
|
||||||
// Refresh session expiry on use
|
if (result) {
|
||||||
refreshSession(db, sessionId);
|
c.set("userId", result.userId);
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
return c.json({ error: "Invalid or expired token" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check OIDC session (browser users via Logto)
|
||||||
|
try {
|
||||||
|
const { getAuth } = await import("@hono/oidc-auth");
|
||||||
|
const auth = await getAuth(c);
|
||||||
|
if (auth?.sub) {
|
||||||
|
const user = await getOrCreateUser(db, auth.sub);
|
||||||
|
await getOrCreateUncategorized(db, user.id);
|
||||||
|
c.set("userId", user.id);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// OIDC not configured or session invalid — fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json({ error: "Authentication required" }, 401);
|
return c.json({ error: "Authentication required" }, 401);
|
||||||
|
|||||||
@@ -1,123 +1,42 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { count, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db as prodDb } from "../../db/index.ts";
|
import { db as prodDb } from "../../db/index.ts";
|
||||||
import { apiKeys, sessions, users } from "../../db/schema.ts";
|
import { apiKeys, users } from "../../db/schema.ts";
|
||||||
|
|
||||||
type Db = typeof prodDb;
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
// ── User Management ──────────────────────────────────────────────────
|
// ── User Management ──────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function createUser(
|
export async function getOrCreateUser(
|
||||||
db: Db = prodDb,
|
db: Db,
|
||||||
username: string,
|
logtoSub: string,
|
||||||
password: string,
|
): Promise<{ id: number }> {
|
||||||
) {
|
const [user] = await db
|
||||||
const passwordHash = await Bun.password.hash(password);
|
.insert(users)
|
||||||
return db.insert(users).values({ username, passwordHash }).returning().get();
|
.values({ logtoSub })
|
||||||
}
|
.onConflictDoUpdate({
|
||||||
|
target: users.logtoSub,
|
||||||
export async function verifyPassword(
|
set: { logtoSub },
|
||||||
db: Db = prodDb,
|
})
|
||||||
username: string,
|
.returning({ id: users.id });
|
||||||
password: string,
|
return user;
|
||||||
) {
|
|
||||||
const user = db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.username, username))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const valid = await Bun.password.verify(password, user.passwordHash);
|
|
||||||
return valid ? user : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUserCount(db: Db = prodDb): number {
|
|
||||||
const result = db.select({ value: count() }).from(users).get();
|
|
||||||
return result?.value ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function changePassword(
|
|
||||||
db: Db = prodDb,
|
|
||||||
username: string,
|
|
||||||
currentPassword: string,
|
|
||||||
newPassword: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const user = await verifyPassword(db, username, currentPassword);
|
|
||||||
if (!user) return false;
|
|
||||||
|
|
||||||
const newHash = await Bun.password.hash(newPassword);
|
|
||||||
db.update(users)
|
|
||||||
.set({ passwordHash: newHash })
|
|
||||||
.where(eq(users.id, user.id))
|
|
||||||
.run();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Session Management ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function createSession(
|
|
||||||
db: Db = prodDb,
|
|
||||||
userId: number,
|
|
||||||
expiryDays = 30,
|
|
||||||
) {
|
|
||||||
const id = randomBytes(32).toString("hex");
|
|
||||||
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
return db
|
|
||||||
.insert(sessions)
|
|
||||||
.values({ id, userId, expiresAt })
|
|
||||||
.returning()
|
|
||||||
.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSession(db: Db = prodDb, sessionId: string) {
|
|
||||||
const session = db
|
|
||||||
.select()
|
|
||||||
.from(sessions)
|
|
||||||
.where(eq(sessions.id, sessionId))
|
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!session) return null;
|
|
||||||
|
|
||||||
if (session.expiresAt < new Date()) {
|
|
||||||
db.delete(sessions).where(eq(sessions.id, sessionId)).run();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteSession(db: Db = prodDb, sessionId: string) {
|
|
||||||
db.delete(sessions).where(eq(sessions.id, sessionId)).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function refreshSession(
|
|
||||||
db: Db = prodDb,
|
|
||||||
sessionId: string,
|
|
||||||
expiryDays = 30,
|
|
||||||
) {
|
|
||||||
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
|
|
||||||
db.update(sessions)
|
|
||||||
.set({ expiresAt })
|
|
||||||
.where(eq(sessions.id, sessionId))
|
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API Key Management ───────────────────────────────────────────────
|
// ── API Key Management ───────────────────────────────────────────────
|
||||||
|
|
||||||
export async function createApiKey(db: Db = prodDb, name: string) {
|
export async function createApiKey(
|
||||||
|
db: Db = prodDb,
|
||||||
|
name: string,
|
||||||
|
userId: number,
|
||||||
|
) {
|
||||||
const rawKey = randomBytes(32).toString("hex");
|
const rawKey = randomBytes(32).toString("hex");
|
||||||
const keyHash = await Bun.password.hash(rawKey);
|
const keyHash = await Bun.password.hash(rawKey);
|
||||||
const keyPrefix = rawKey.slice(0, 8);
|
const keyPrefix = rawKey.slice(0, 8);
|
||||||
|
|
||||||
const record = db
|
const [record] = await db
|
||||||
.insert(apiKeys)
|
.insert(apiKeys)
|
||||||
.values({ name, keyHash, keyPrefix })
|
.values({ name, keyHash, keyPrefix, userId })
|
||||||
.returning()
|
.returning();
|
||||||
.get();
|
|
||||||
|
|
||||||
return { ...record, rawKey };
|
return { ...record, rawKey };
|
||||||
}
|
}
|
||||||
@@ -125,23 +44,22 @@ export async function createApiKey(db: Db = prodDb, name: string) {
|
|||||||
export async function verifyApiKey(
|
export async function verifyApiKey(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
rawKey: string,
|
rawKey: string,
|
||||||
): Promise<boolean> {
|
): Promise<{ userId: number } | null> {
|
||||||
const prefix = rawKey.slice(0, 8);
|
const prefix = rawKey.slice(0, 8);
|
||||||
const candidates = db
|
const candidates = await db
|
||||||
.select()
|
.select()
|
||||||
.from(apiKeys)
|
.from(apiKeys)
|
||||||
.where(eq(apiKeys.keyPrefix, prefix))
|
.where(eq(apiKeys.keyPrefix, prefix));
|
||||||
.all();
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const valid = await Bun.password.verify(rawKey, candidate.keyHash);
|
const valid = await Bun.password.verify(rawKey, candidate.keyHash);
|
||||||
if (valid) return true;
|
if (valid) return { userId: candidate.userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listApiKeys(db: Db = prodDb) {
|
export async function listApiKeys(db: Db = prodDb, userId: number) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
id: apiKeys.id,
|
id: apiKeys.id,
|
||||||
@@ -150,9 +68,15 @@ export function listApiKeys(db: Db = prodDb) {
|
|||||||
createdAt: apiKeys.createdAt,
|
createdAt: apiKeys.createdAt,
|
||||||
})
|
})
|
||||||
.from(apiKeys)
|
.from(apiKeys)
|
||||||
.all();
|
.where(eq(apiKeys.userId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteApiKey(db: Db = prodDb, id: number) {
|
export async function deleteApiKey(
|
||||||
db.delete(apiKeys).where(eq(apiKeys.id, id)).run();
|
db: Db = prodDb,
|
||||||
|
id: number,
|
||||||
|
userId: number,
|
||||||
|
) {
|
||||||
|
await db
|
||||||
|
.delete(apiKeys)
|
||||||
|
.where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
import { asc, eq } from "drizzle-orm";
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
import { db as prodDb } from "../../db/index.ts";
|
import { db as prodDb } from "../../db/index.ts";
|
||||||
import { categories, items } from "../../db/schema.ts";
|
import { categories, items } from "../../db/schema.ts";
|
||||||
|
|
||||||
type Db = typeof prodDb;
|
type Db = typeof prodDb;
|
||||||
|
|
||||||
|
export async function getOrCreateUncategorized(
|
||||||
|
db: Db,
|
||||||
|
userId: number,
|
||||||
|
): Promise<number> {
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: categories.id })
|
||||||
|
.from(categories)
|
||||||
|
.where(and(eq(categories.userId, userId), eq(categories.name, "Uncategorized")));
|
||||||
|
if (existing) return existing.id;
|
||||||
|
const [created] = await db
|
||||||
|
.insert(categories)
|
||||||
|
.values({ name: "Uncategorized", icon: "package", userId })
|
||||||
|
.returning({ id: categories.id });
|
||||||
|
return created.id;
|
||||||
|
}
|
||||||
|
|
||||||
export function getAllCategories(db: Db = prodDb) {
|
export function getAllCategories(db: Db = prodDb) {
|
||||||
return db.select().from(categories).orderBy(asc(categories.name)).all();
|
return db.select().from(categories).orderBy(asc(categories.name)).all();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,53 +7,50 @@ type Db = typeof prodDb;
|
|||||||
|
|
||||||
// ── Client Registration ──────────────────────────────────────────────
|
// ── Client Registration ──────────────────────────────────────────────
|
||||||
|
|
||||||
export function registerClient(
|
export async function registerClient(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
clientName: string,
|
clientName: string,
|
||||||
redirectUris: string[],
|
redirectUris: string[],
|
||||||
): { clientId: string } {
|
): Promise<{ clientId: string }> {
|
||||||
const clientId = randomUUID();
|
const clientId = randomUUID();
|
||||||
const redirectUrisJson = JSON.stringify(redirectUris);
|
const redirectUrisJson = JSON.stringify(redirectUris);
|
||||||
|
|
||||||
db.insert(oauthClients)
|
await db
|
||||||
.values({ clientId, clientName, redirectUris: redirectUrisJson })
|
.insert(oauthClients)
|
||||||
.run();
|
.values({ clientId, clientName, redirectUris: redirectUrisJson });
|
||||||
|
|
||||||
return { clientId };
|
return { clientId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getClient(db: Db = prodDb, clientId: string) {
|
export async function getClient(db: Db = prodDb, clientId: string) {
|
||||||
return (
|
const [record] = await db
|
||||||
db
|
.select()
|
||||||
.select()
|
.from(oauthClients)
|
||||||
.from(oauthClients)
|
.where(eq(oauthClients.clientId, clientId));
|
||||||
.where(eq(oauthClients.clientId, clientId))
|
|
||||||
.get() ?? null
|
return record ?? null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Authorization Code ───────────────────────────────────────────────
|
// ── Authorization Code ───────────────────────────────────────────────
|
||||||
|
|
||||||
export function createAuthorizationCode(
|
export async function createAuthorizationCode(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
codeChallenge: string,
|
codeChallenge: string,
|
||||||
codeChallengeMethod: string,
|
codeChallengeMethod: string,
|
||||||
redirectUri: string,
|
redirectUri: string,
|
||||||
): { code: string } {
|
): Promise<{ code: string }> {
|
||||||
const code = randomBytes(32).toString("hex");
|
const code = randomBytes(32).toString("hex");
|
||||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
||||||
|
|
||||||
db.insert(oauthCodes)
|
await db.insert(oauthCodes).values({
|
||||||
.values({
|
code,
|
||||||
code,
|
clientId,
|
||||||
clientId,
|
codeChallenge,
|
||||||
codeChallenge,
|
codeChallengeMethod,
|
||||||
codeChallengeMethod,
|
redirectUri,
|
||||||
redirectUri,
|
expiresAt,
|
||||||
expiresAt,
|
});
|
||||||
})
|
|
||||||
.run();
|
|
||||||
|
|
||||||
return { code };
|
return { code };
|
||||||
}
|
}
|
||||||
@@ -64,16 +61,16 @@ export async function exchangeCode(
|
|||||||
codeVerifier: string,
|
codeVerifier: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
redirectUri: string,
|
redirectUri: string,
|
||||||
|
userId: number,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
expiresIn: number;
|
expiresIn: number;
|
||||||
} | null> {
|
} | null> {
|
||||||
const record = db
|
const [record] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(oauthCodes)
|
.from(oauthCodes)
|
||||||
.where(eq(oauthCodes.code, code))
|
.where(eq(oauthCodes.code, code));
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
if (record.used !== 0) return null;
|
if (record.used !== 0) return null;
|
||||||
@@ -89,17 +86,21 @@ export async function exchangeCode(
|
|||||||
if (computedChallenge !== record.codeChallenge) return null;
|
if (computedChallenge !== record.codeChallenge) return null;
|
||||||
|
|
||||||
// Mark code as used
|
// Mark code as used
|
||||||
db.update(oauthCodes).set({ used: 1 }).where(eq(oauthCodes.code, code)).run();
|
await db
|
||||||
|
.update(oauthCodes)
|
||||||
|
.set({ used: 1 })
|
||||||
|
.where(eq(oauthCodes.code, code));
|
||||||
|
|
||||||
return generateTokens(db, clientId);
|
return generateTokens(db, clientId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Token Management ─────────────────────────────────────────────────
|
// ── Token Management ─────────────────────────────────────────────────
|
||||||
|
|
||||||
function generateTokens(
|
async function generateTokens(
|
||||||
db: Db,
|
db: Db,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
): { accessToken: string; refreshToken: string; expiresIn: number } {
|
userId: number,
|
||||||
|
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
||||||
const accessToken = randomBytes(32).toString("hex");
|
const accessToken = randomBytes(32).toString("hex");
|
||||||
const refreshToken = randomBytes(32).toString("hex");
|
const refreshToken = randomBytes(32).toString("hex");
|
||||||
|
|
||||||
@@ -113,15 +114,14 @@ function generateTokens(
|
|||||||
const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour
|
const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour
|
||||||
const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
||||||
|
|
||||||
db.insert(oauthTokens)
|
await db.insert(oauthTokens).values({
|
||||||
.values({
|
accessTokenHash,
|
||||||
accessTokenHash,
|
refreshTokenHash,
|
||||||
refreshTokenHash,
|
clientId,
|
||||||
clientId,
|
userId,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
refreshExpiresAt,
|
refreshExpiresAt,
|
||||||
})
|
});
|
||||||
.run();
|
|
||||||
|
|
||||||
return { accessToken, refreshToken, expiresIn: 3600 };
|
return { accessToken, refreshToken, expiresIn: 3600 };
|
||||||
}
|
}
|
||||||
@@ -129,25 +129,25 @@ function generateTokens(
|
|||||||
export async function verifyAccessToken(
|
export async function verifyAccessToken(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
token: string,
|
token: string,
|
||||||
): Promise<boolean> {
|
): Promise<{ userId: number } | null> {
|
||||||
const tokenHash = createHash("sha256").update(token).digest("hex");
|
const tokenHash = createHash("sha256").update(token).digest("hex");
|
||||||
|
|
||||||
const record = db
|
const [record] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(oauthTokens)
|
.from(oauthTokens)
|
||||||
.where(eq(oauthTokens.accessTokenHash, tokenHash))
|
.where(eq(oauthTokens.accessTokenHash, tokenHash));
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!record) return false;
|
if (!record) return null;
|
||||||
if (record.expiresAt < new Date()) return false;
|
if (record.expiresAt < new Date()) return null;
|
||||||
|
|
||||||
return true;
|
return { userId: record.userId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAccessToken(
|
export async function refreshAccessToken(
|
||||||
db: Db = prodDb,
|
db: Db = prodDb,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
clientId: string,
|
clientId: string,
|
||||||
|
userId: number,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
@@ -155,7 +155,7 @@ export async function refreshAccessToken(
|
|||||||
} | null> {
|
} | null> {
|
||||||
const tokenHash = createHash("sha256").update(refreshToken).digest("hex");
|
const tokenHash = createHash("sha256").update(refreshToken).digest("hex");
|
||||||
|
|
||||||
const record = db
|
const [record] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(oauthTokens)
|
.from(oauthTokens)
|
||||||
.where(
|
.where(
|
||||||
@@ -163,22 +163,21 @@ export async function refreshAccessToken(
|
|||||||
eq(oauthTokens.refreshTokenHash, tokenHash),
|
eq(oauthTokens.refreshTokenHash, tokenHash),
|
||||||
eq(oauthTokens.clientId, clientId),
|
eq(oauthTokens.clientId, clientId),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.get();
|
|
||||||
|
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
if (record.refreshExpiresAt < new Date()) return null;
|
if (record.refreshExpiresAt < new Date()) return null;
|
||||||
|
|
||||||
// Delete old token pair
|
// Delete old token pair
|
||||||
db.delete(oauthTokens).where(eq(oauthTokens.id, record.id)).run();
|
await db.delete(oauthTokens).where(eq(oauthTokens.id, record.id));
|
||||||
|
|
||||||
return generateTokens(db, clientId);
|
return generateTokens(db, clientId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cleanup ──────────────────────────────────────────────────────────
|
// ── Cleanup ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function cleanExpiredOAuthData(db: Db = prodDb): void {
|
export async function cleanExpiredOAuthData(db: Db = prodDb): Promise<void> {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now)).run();
|
await db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now));
|
||||||
db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now)).run();
|
await db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user