feat(14-03): convert auth/oauth/csv services to async, await seedDefaults
- auth.service.ts: 10 functions async, removed .all()/.get()/.run() - oauth.service.ts: 7 functions async, boolean conversion (used: true/false) - csv.service.ts: export/import functions async, removed .all()/.get()/.run() - server index.ts: seedDefaults() now awaited for async DB - PGlite smoke test confirms async services work end-to-end
This commit is contained in:
@@ -16,7 +16,7 @@ import { threadRoutes } from "./routes/threads.ts";
|
||||
import { totalRoutes } from "./routes/totals.ts";
|
||||
|
||||
// Seed default data on startup
|
||||
seedDefaults();
|
||||
await seedDefaults();
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ export async function createUser(
|
||||
password: string,
|
||||
) {
|
||||
const passwordHash = await Bun.password.hash(password);
|
||||
return db.insert(users).values({ username, passwordHash }).returning().get();
|
||||
const [row] = await db
|
||||
.insert(users)
|
||||
.values({ username, passwordHash })
|
||||
.returning();
|
||||
return row;
|
||||
}
|
||||
|
||||
export async function verifyPassword(
|
||||
@@ -21,11 +25,10 @@ export async function verifyPassword(
|
||||
username: string,
|
||||
password: string,
|
||||
) {
|
||||
const user = db
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
.get();
|
||||
.where(eq(users.username, username));
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
@@ -33,8 +36,8 @@ export async function verifyPassword(
|
||||
return valid ? user : null;
|
||||
}
|
||||
|
||||
export function getUserCount(db: Db = prodDb): number {
|
||||
const result = db.select({ value: count() }).from(users).get();
|
||||
export async function getUserCount(db: Db = prodDb): Promise<number> {
|
||||
const [result] = await db.select({ value: count() }).from(users);
|
||||
return result?.value ?? 0;
|
||||
}
|
||||
|
||||
@@ -48,17 +51,17 @@ export async function changePassword(
|
||||
if (!user) return false;
|
||||
|
||||
const newHash = await Bun.password.hash(newPassword);
|
||||
db.update(users)
|
||||
await db
|
||||
.update(users)
|
||||
.set({ passwordHash: newHash })
|
||||
.where(eq(users.id, user.id))
|
||||
.run();
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Session Management ───────────────────────────────────────────────
|
||||
|
||||
export function createSession(
|
||||
export async function createSession(
|
||||
db: Db = prodDb,
|
||||
userId: number,
|
||||
expiryDays = 30,
|
||||
@@ -66,44 +69,44 @@ export function createSession(
|
||||
const id = randomBytes(32).toString("hex");
|
||||
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
return db
|
||||
const [row] = await db
|
||||
.insert(sessions)
|
||||
.values({ id, userId, expiresAt })
|
||||
.returning()
|
||||
.get();
|
||||
.returning();
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
export function getSession(db: Db = prodDb, sessionId: string) {
|
||||
const session = db
|
||||
export async function getSession(db: Db = prodDb, sessionId: string) {
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.id, sessionId))
|
||||
.get();
|
||||
.where(eq(sessions.id, sessionId));
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
if (session.expiresAt < new Date()) {
|
||||
db.delete(sessions).where(eq(sessions.id, sessionId)).run();
|
||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export function deleteSession(db: Db = prodDb, sessionId: string) {
|
||||
db.delete(sessions).where(eq(sessions.id, sessionId)).run();
|
||||
export async function deleteSession(db: Db = prodDb, sessionId: string) {
|
||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||
}
|
||||
|
||||
export function refreshSession(
|
||||
export async function refreshSession(
|
||||
db: Db = prodDb,
|
||||
sessionId: string,
|
||||
expiryDays = 30,
|
||||
) {
|
||||
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1000);
|
||||
db.update(sessions)
|
||||
await db
|
||||
.update(sessions)
|
||||
.set({ expiresAt })
|
||||
.where(eq(sessions.id, sessionId))
|
||||
.run();
|
||||
.where(eq(sessions.id, sessionId));
|
||||
}
|
||||
|
||||
// ── API Key Management ───────────────────────────────────────────────
|
||||
@@ -113,11 +116,10 @@ export async function createApiKey(db: Db = prodDb, name: string) {
|
||||
const keyHash = await Bun.password.hash(rawKey);
|
||||
const keyPrefix = rawKey.slice(0, 8);
|
||||
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.insert(apiKeys)
|
||||
.values({ name, keyHash, keyPrefix })
|
||||
.returning()
|
||||
.get();
|
||||
.returning();
|
||||
|
||||
return { ...record, rawKey };
|
||||
}
|
||||
@@ -127,11 +129,10 @@ export async function verifyApiKey(
|
||||
rawKey: string,
|
||||
): Promise<boolean> {
|
||||
const prefix = rawKey.slice(0, 8);
|
||||
const candidates = db
|
||||
const candidates = await db
|
||||
.select()
|
||||
.from(apiKeys)
|
||||
.where(eq(apiKeys.keyPrefix, prefix))
|
||||
.all();
|
||||
.where(eq(apiKeys.keyPrefix, prefix));
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const valid = await Bun.password.verify(rawKey, candidate.keyHash);
|
||||
@@ -141,7 +142,7 @@ export async function verifyApiKey(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function listApiKeys(db: Db = prodDb) {
|
||||
export async function listApiKeys(db: Db = prodDb) {
|
||||
return db
|
||||
.select({
|
||||
id: apiKeys.id,
|
||||
@@ -149,10 +150,9 @@ export function listApiKeys(db: Db = prodDb) {
|
||||
keyPrefix: apiKeys.keyPrefix,
|
||||
createdAt: apiKeys.createdAt,
|
||||
})
|
||||
.from(apiKeys)
|
||||
.all();
|
||||
.from(apiKeys);
|
||||
}
|
||||
|
||||
export function deleteApiKey(db: Db = prodDb, id: number) {
|
||||
db.delete(apiKeys).where(eq(apiKeys.id, id)).run();
|
||||
export async function deleteApiKey(db: Db = prodDb, id: number) {
|
||||
await db.delete(apiKeys).where(eq(apiKeys.id, id));
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ function parseCsv(content: string): { headers: string[]; rows: string[][] } {
|
||||
|
||||
// ─── Export ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export function exportItemsCsv(db: Db = prodDb): string {
|
||||
const rows = db
|
||||
export async function exportItemsCsv(db: Db = prodDb): Promise<string> {
|
||||
const rows = await db
|
||||
.select({
|
||||
name: items.name,
|
||||
quantity: items.quantity,
|
||||
@@ -96,8 +96,7 @@ export function exportItemsCsv(db: Db = prodDb): string {
|
||||
productUrl: items.productUrl,
|
||||
})
|
||||
.from(items)
|
||||
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||
.all();
|
||||
.innerJoin(categories, eq(items.categoryId, categories.id));
|
||||
|
||||
const header =
|
||||
"name,quantity,weightGrams,priceCents,category,notes,productUrl";
|
||||
@@ -124,10 +123,10 @@ export interface ImportResult {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export function importItemsCsv(
|
||||
export async function importItemsCsv(
|
||||
db: Db = prodDb,
|
||||
csvContent: string,
|
||||
): ImportResult {
|
||||
): Promise<ImportResult> {
|
||||
const { headers, rows } = parseCsv(csvContent);
|
||||
|
||||
const result: ImportResult = {
|
||||
@@ -152,10 +151,9 @@ export function importItemsCsv(
|
||||
|
||||
// Build a local category cache (name → id) seeded from the DB
|
||||
const categoryCache = new Map<string, number>();
|
||||
const existingCategories = db
|
||||
const existingCategories = await db
|
||||
.select({ id: categories.id, name: categories.name })
|
||||
.from(categories)
|
||||
.all();
|
||||
.from(categories);
|
||||
for (const cat of existingCategories) {
|
||||
categoryCache.set(cat.name.toLowerCase(), cat.id);
|
||||
}
|
||||
@@ -181,11 +179,10 @@ export function importItemsCsv(
|
||||
categoryId = categoryCache.get(cacheKey)!;
|
||||
} else {
|
||||
// Create a new category
|
||||
const inserted = db
|
||||
const [inserted] = await db
|
||||
.insert(categories)
|
||||
.values({ name: categoryName, icon: "package" })
|
||||
.returning()
|
||||
.get();
|
||||
.returning();
|
||||
categoryId = inserted.id;
|
||||
categoryCache.set(cacheKey, categoryId);
|
||||
result.createdCategories.push(categoryName);
|
||||
@@ -222,8 +219,7 @@ export function importItemsCsv(
|
||||
const notes = notesIdx >= 0 ? row[notesIdx]?.trim() || null : null;
|
||||
const productUrl = urlIdx >= 0 ? row[urlIdx]?.trim() || null : null;
|
||||
|
||||
db.insert(items)
|
||||
.values({
|
||||
await db.insert(items).values({
|
||||
name,
|
||||
quantity,
|
||||
weightGrams,
|
||||
@@ -233,8 +229,7 @@ export function importItemsCsv(
|
||||
productUrl,
|
||||
imageFilename: null,
|
||||
imageSourceUrl: null,
|
||||
})
|
||||
.run();
|
||||
});
|
||||
|
||||
result.imported++;
|
||||
} catch (err) {
|
||||
|
||||
@@ -7,53 +7,50 @@ type Db = typeof prodDb;
|
||||
|
||||
// ── Client Registration ──────────────────────────────────────────────
|
||||
|
||||
export function registerClient(
|
||||
export async function registerClient(
|
||||
db: Db = prodDb,
|
||||
clientName: string,
|
||||
redirectUris: string[],
|
||||
): { clientId: string } {
|
||||
): Promise<{ clientId: string }> {
|
||||
const clientId = randomUUID();
|
||||
const redirectUrisJson = JSON.stringify(redirectUris);
|
||||
|
||||
db.insert(oauthClients)
|
||||
.values({ clientId, clientName, redirectUris: redirectUrisJson })
|
||||
.run();
|
||||
await db
|
||||
.insert(oauthClients)
|
||||
.values({ clientId, clientName, redirectUris: redirectUrisJson });
|
||||
|
||||
return { clientId };
|
||||
}
|
||||
|
||||
export function getClient(db: Db = prodDb, clientId: string) {
|
||||
return (
|
||||
db
|
||||
export async function getClient(db: Db = prodDb, clientId: string) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(oauthClients)
|
||||
.where(eq(oauthClients.clientId, clientId))
|
||||
.get() ?? null
|
||||
);
|
||||
.where(eq(oauthClients.clientId, clientId));
|
||||
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
// ── Authorization Code ───────────────────────────────────────────────
|
||||
|
||||
export function createAuthorizationCode(
|
||||
export async function createAuthorizationCode(
|
||||
db: Db = prodDb,
|
||||
clientId: string,
|
||||
codeChallenge: string,
|
||||
codeChallengeMethod: string,
|
||||
redirectUri: string,
|
||||
): { code: string } {
|
||||
): Promise<{ code: string }> {
|
||||
const code = randomBytes(32).toString("hex");
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
|
||||
|
||||
db.insert(oauthCodes)
|
||||
.values({
|
||||
await db.insert(oauthCodes).values({
|
||||
code,
|
||||
clientId,
|
||||
codeChallenge,
|
||||
codeChallengeMethod,
|
||||
redirectUri,
|
||||
expiresAt,
|
||||
})
|
||||
.run();
|
||||
});
|
||||
|
||||
return { code };
|
||||
}
|
||||
@@ -69,14 +66,13 @@ export async function exchangeCode(
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
} | null> {
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthCodes)
|
||||
.where(eq(oauthCodes.code, code))
|
||||
.get();
|
||||
.where(eq(oauthCodes.code, code));
|
||||
|
||||
if (!record) return null;
|
||||
if (record.used !== 0) return null;
|
||||
if (record.used !== false) return null;
|
||||
if (record.clientId !== clientId) return null;
|
||||
if (record.redirectUri !== redirectUri) return null;
|
||||
if (record.expiresAt < new Date()) return null;
|
||||
@@ -89,17 +85,20 @@ export async function exchangeCode(
|
||||
if (computedChallenge !== record.codeChallenge) return null;
|
||||
|
||||
// Mark code as used
|
||||
db.update(oauthCodes).set({ used: 1 }).where(eq(oauthCodes.code, code)).run();
|
||||
await db
|
||||
.update(oauthCodes)
|
||||
.set({ used: true })
|
||||
.where(eq(oauthCodes.code, code));
|
||||
|
||||
return generateTokens(db, clientId);
|
||||
}
|
||||
|
||||
// ── Token Management ─────────────────────────────────────────────────
|
||||
|
||||
function generateTokens(
|
||||
async function generateTokens(
|
||||
db: Db,
|
||||
clientId: string,
|
||||
): { accessToken: string; refreshToken: string; expiresIn: number } {
|
||||
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
||||
const accessToken = randomBytes(32).toString("hex");
|
||||
const refreshToken = randomBytes(32).toString("hex");
|
||||
|
||||
@@ -113,15 +112,13 @@ function generateTokens(
|
||||
const expiresAt = new Date(Date.now() + 3600 * 1000); // 1 hour
|
||||
const refreshExpiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
||||
|
||||
db.insert(oauthTokens)
|
||||
.values({
|
||||
await db.insert(oauthTokens).values({
|
||||
accessTokenHash,
|
||||
refreshTokenHash,
|
||||
clientId,
|
||||
expiresAt,
|
||||
refreshExpiresAt,
|
||||
})
|
||||
.run();
|
||||
});
|
||||
|
||||
return { accessToken, refreshToken, expiresIn: 3600 };
|
||||
}
|
||||
@@ -132,11 +129,10 @@ export async function verifyAccessToken(
|
||||
): Promise<boolean> {
|
||||
const tokenHash = createHash("sha256").update(token).digest("hex");
|
||||
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthTokens)
|
||||
.where(eq(oauthTokens.accessTokenHash, tokenHash))
|
||||
.get();
|
||||
.where(eq(oauthTokens.accessTokenHash, tokenHash));
|
||||
|
||||
if (!record) return false;
|
||||
if (record.expiresAt < new Date()) return false;
|
||||
@@ -155,7 +151,7 @@ export async function refreshAccessToken(
|
||||
} | null> {
|
||||
const tokenHash = createHash("sha256").update(refreshToken).digest("hex");
|
||||
|
||||
const record = db
|
||||
const [record] = await db
|
||||
.select()
|
||||
.from(oauthTokens)
|
||||
.where(
|
||||
@@ -163,22 +159,21 @@ export async function refreshAccessToken(
|
||||
eq(oauthTokens.refreshTokenHash, tokenHash),
|
||||
eq(oauthTokens.clientId, clientId),
|
||||
),
|
||||
)
|
||||
.get();
|
||||
);
|
||||
|
||||
if (!record) return null;
|
||||
if (record.refreshExpiresAt < new Date()) return null;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// ── Cleanup ──────────────────────────────────────────────────────────
|
||||
|
||||
export function cleanExpiredOAuthData(db: Db = prodDb): void {
|
||||
export async function cleanExpiredOAuthData(db: Db = prodDb): Promise<void> {
|
||||
const now = new Date();
|
||||
db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now)).run();
|
||||
db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now)).run();
|
||||
await db.delete(oauthCodes).where(lt(oauthCodes.expiresAt, now));
|
||||
await db.delete(oauthTokens).where(lt(oauthTokens.expiresAt, now));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user