From 574a12e6fa7707fa7c1609b489ee3176ab402af2 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 5 Apr 2026 18:25:31 +0200 Subject: [PATCH] fix: OIDC auth flow, Vite proxy, and PostgreSQL query compat - Add auth redirect in root layout for unauthenticated users - Proxy OIDC routes (/login, /callback, /logout) through Vite dev server - Strip Secure flag from OIDC cookies in dev mode (HTTP localhost) - Disable retry on auth query to prevent stale cookie loops - Fix SQLite .get()/.all()/.run() calls in category and global-item services for PostgreSQL compatibility - Add userId scoping to category service functions - Add OIDC error logging in auth middleware - Apply linter auto-formatting across affected files Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/hooks/useAuth.ts | 1 + src/client/routeTree.gen.ts | 63 ++++++++++++++++ src/client/routes/__root.tsx | 31 +++++++- src/db/schema.ts | 4 +- src/db/seed-global-items.ts | 2 +- src/server/index.ts | 26 ++++++- src/server/middleware/auth.ts | 3 +- src/server/routes/auth.ts | 4 +- src/server/routes/setups.ts | 2 +- src/server/routes/threads.ts | 5 +- src/server/services/auth.service.ts | 8 +- src/server/services/category.service.ts | 85 ++++++++++++---------- src/server/services/csv.service.ts | 2 +- src/server/services/global-item.service.ts | 44 +++++------ src/server/services/item.service.ts | 6 +- src/server/services/oauth.service.ts | 5 +- src/server/services/profile.service.ts | 7 +- src/server/services/setup.service.ts | 22 ++---- src/server/services/storage.service.ts | 12 +-- src/server/services/thread.service.ts | 12 +-- src/server/services/totals.service.ts | 2 +- tests/routes/global-items.test.ts | 6 +- tests/routes/images.test.ts | 5 +- tests/routes/oauth.test.ts | 5 +- tests/routes/profiles.test.ts | 22 +++--- tests/services/global-item.service.test.ts | 53 ++++++++++---- tests/services/image.service.test.ts | 28 +++---- tests/services/profile.service.test.ts | 5 +- tests/services/setup.service.test.ts | 48 ++---------- tests/services/storage.service.test.ts | 25 +++---- tests/services/thread.service.test.ts | 20 +---- vite.config.ts | 5 ++ 32 files changed, 315 insertions(+), 253 deletions(-) diff --git a/src/client/hooks/useAuth.ts b/src/client/hooks/useAuth.ts index 25e6754..d24324f 100644 --- a/src/client/hooks/useAuth.ts +++ b/src/client/hooks/useAuth.ts @@ -11,6 +11,7 @@ export function useAuth() { queryKey: ["auth"], queryFn: () => apiGet("/api/auth/me"), staleTime: 5 * 60 * 1000, + retry: false, }); } diff --git a/src/client/routeTree.gen.ts b/src/client/routeTree.gen.ts index 1f7b3f4..274d5b7 100644 --- a/src/client/routeTree.gen.ts +++ b/src/client/routeTree.gen.ts @@ -12,9 +12,12 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SettingsRouteImport } from './routes/settings' import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' +import { Route as GlobalItemsIndexRouteImport } from './routes/global-items/index' import { Route as CollectionIndexRouteImport } from './routes/collection/index' +import { Route as UsersUserIdRouteImport } from './routes/users/$userId' import { Route as ThreadsThreadIdRouteImport } from './routes/threads/$threadId' import { Route as SetupsSetupIdRouteImport } from './routes/setups/$setupId' +import { Route as GlobalItemsGlobalItemIdRouteImport } from './routes/global-items/$globalItemId' const SettingsRoute = SettingsRouteImport.update({ id: '/settings', @@ -31,11 +34,21 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const GlobalItemsIndexRoute = GlobalItemsIndexRouteImport.update({ + id: '/global-items/', + path: '/global-items/', + getParentRoute: () => rootRouteImport, +} as any) const CollectionIndexRoute = CollectionIndexRouteImport.update({ id: '/collection/', path: '/collection/', getParentRoute: () => rootRouteImport, } as any) +const UsersUserIdRoute = UsersUserIdRouteImport.update({ + id: '/users/$userId', + path: '/users/$userId', + getParentRoute: () => rootRouteImport, +} as any) const ThreadsThreadIdRoute = ThreadsThreadIdRouteImport.update({ id: '/threads/$threadId', path: '/threads/$threadId', @@ -46,31 +59,45 @@ const SetupsSetupIdRoute = SetupsSetupIdRouteImport.update({ path: '/setups/$setupId', getParentRoute: () => rootRouteImport, } as any) +const GlobalItemsGlobalItemIdRoute = GlobalItemsGlobalItemIdRouteImport.update({ + id: '/global-items/$globalItemId', + path: '/global-items/$globalItemId', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute '/settings': typeof SettingsRoute + '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute '/threads/$threadId': typeof ThreadsThreadIdRoute + '/users/$userId': typeof UsersUserIdRoute '/collection/': typeof CollectionIndexRoute + '/global-items/': typeof GlobalItemsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute '/settings': typeof SettingsRoute + '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute '/threads/$threadId': typeof ThreadsThreadIdRoute + '/users/$userId': typeof UsersUserIdRoute '/collection': typeof CollectionIndexRoute + '/global-items': typeof GlobalItemsIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/login': typeof LoginRoute '/settings': typeof SettingsRoute + '/global-items/$globalItemId': typeof GlobalItemsGlobalItemIdRoute '/setups/$setupId': typeof SetupsSetupIdRoute '/threads/$threadId': typeof ThreadsThreadIdRoute + '/users/$userId': typeof UsersUserIdRoute '/collection/': typeof CollectionIndexRoute + '/global-items/': typeof GlobalItemsIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -78,34 +105,46 @@ export interface FileRouteTypes { | '/' | '/login' | '/settings' + | '/global-items/$globalItemId' | '/setups/$setupId' | '/threads/$threadId' + | '/users/$userId' | '/collection/' + | '/global-items/' fileRoutesByTo: FileRoutesByTo to: | '/' | '/login' | '/settings' + | '/global-items/$globalItemId' | '/setups/$setupId' | '/threads/$threadId' + | '/users/$userId' | '/collection' + | '/global-items' id: | '__root__' | '/' | '/login' | '/settings' + | '/global-items/$globalItemId' | '/setups/$setupId' | '/threads/$threadId' + | '/users/$userId' | '/collection/' + | '/global-items/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginRoute: typeof LoginRoute SettingsRoute: typeof SettingsRoute + GlobalItemsGlobalItemIdRoute: typeof GlobalItemsGlobalItemIdRoute SetupsSetupIdRoute: typeof SetupsSetupIdRoute ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute + UsersUserIdRoute: typeof UsersUserIdRoute CollectionIndexRoute: typeof CollectionIndexRoute + GlobalItemsIndexRoute: typeof GlobalItemsIndexRoute } declare module '@tanstack/react-router' { @@ -131,6 +170,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/global-items/': { + id: '/global-items/' + path: '/global-items' + fullPath: '/global-items/' + preLoaderRoute: typeof GlobalItemsIndexRouteImport + parentRoute: typeof rootRouteImport + } '/collection/': { id: '/collection/' path: '/collection' @@ -138,6 +184,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CollectionIndexRouteImport parentRoute: typeof rootRouteImport } + '/users/$userId': { + id: '/users/$userId' + path: '/users/$userId' + fullPath: '/users/$userId' + preLoaderRoute: typeof UsersUserIdRouteImport + parentRoute: typeof rootRouteImport + } '/threads/$threadId': { id: '/threads/$threadId' path: '/threads/$threadId' @@ -152,6 +205,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SetupsSetupIdRouteImport parentRoute: typeof rootRouteImport } + '/global-items/$globalItemId': { + id: '/global-items/$globalItemId' + path: '/global-items/$globalItemId' + fullPath: '/global-items/$globalItemId' + preLoaderRoute: typeof GlobalItemsGlobalItemIdRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -159,9 +219,12 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginRoute: LoginRoute, SettingsRoute: SettingsRoute, + GlobalItemsGlobalItemIdRoute: GlobalItemsGlobalItemIdRoute, SetupsSetupIdRoute: SetupsSetupIdRoute, ThreadsThreadIdRoute: ThreadsThreadIdRoute, + UsersUserIdRoute: UsersUserIdRoute, CollectionIndexRoute: CollectionIndexRoute, + GlobalItemsIndexRoute: GlobalItemsIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/src/client/routes/__root.tsx b/src/client/routes/__root.tsx index 221a0fd..53af93e 100644 --- a/src/client/routes/__root.tsx +++ b/src/client/routes/__root.tsx @@ -2,6 +2,7 @@ import { createRootRoute, type ErrorComponentProps, Outlet, + useLocation, useMatchRoute, useNavigate, useRouter, @@ -72,7 +73,8 @@ function RootErrorBoundary({ error, reset }: ErrorComponentProps) { function RootLayout() { const navigate = useNavigate(); - const { data: auth } = useAuth(); + const location = useLocation(); + const { data: auth, isLoading: authLoading } = useAuth(); const isAuthenticated = !!auth?.user; // Item panel state @@ -99,7 +101,7 @@ function RootLayout() { const resolveCandidateId = useUIStore((s) => s.resolveCandidateId); const closeResolveDialog = useUIStore((s) => s.closeResolveDialog); - // Onboarding + // Onboarding — only check when authenticated (endpoint requires auth) const { data: onboardingComplete, isLoading: onboardingLoading } = useOnboardingComplete(); const [wizardDismissed, setWizardDismissed] = useState(false); @@ -152,7 +154,30 @@ function RootLayout() { !(collectionSearch as Record).tab || (collectionSearch as Record).tab === "gear"); - // Show a minimal loading state while checking onboarding status + // Show loading while checking auth + if (authLoading) { + return ( +
+
+
+ ); + } + + // Redirect unauthenticated users to login (server-side OIDC route) + // Allow public routes through without auth + const isPublicRoute = + location.pathname.startsWith("/users/") || location.pathname === "/login"; + + if (!isAuthenticated && !isPublicRoute) { + window.location.href = "/login"; + return ( +
+

Redirecting to login...

+
+ ); + } + + // Show loading while checking onboarding status if (onboardingLoading) { return (
diff --git a/src/db/schema.ts b/src/db/schema.ts index 24b66ca..b11d6bc 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -198,9 +198,7 @@ export const oauthCodes = pgTable("oauth_codes", { code: text("code").notNull().unique(), clientId: text("client_id").notNull(), codeChallenge: text("code_challenge").notNull(), - codeChallengeMethod: text("code_challenge_method") - .notNull() - .default("S256"), + codeChallengeMethod: text("code_challenge_method").notNull().default("S256"), redirectUri: text("redirect_uri").notNull(), expiresAt: timestamp("expires_at").notNull(), used: integer("used").notNull().default(0), diff --git a/src/db/seed-global-items.ts b/src/db/seed-global-items.ts index a747236..162a801 100644 --- a/src/db/seed-global-items.ts +++ b/src/db/seed-global-items.ts @@ -1,6 +1,6 @@ +import seedData from "./global-items-seed.json"; import { db as prodDb } from "./index.ts"; import { globalItems } from "./schema.ts"; -import seedData from "./global-items-seed.json"; type Db = typeof prodDb; diff --git a/src/server/index.ts b/src/server/index.ts index 4850e7f..739d48a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,11 +1,11 @@ -import { Hono } from "hono"; -import { serveStatic } from "hono/bun"; -import { cors } from "hono/cors"; import { oidcAuthMiddleware, processOAuthCallback, revokeSession, } from "@hono/oidc-auth"; +import { Hono } from "hono"; +import { serveStatic } from "hono/bun"; +import { cors } from "hono/cors"; import { db as prodDb } from "../db/index.ts"; import { seedDefaults } from "../db/seed.ts"; import { mcpRoutes } from "./mcp/index.ts"; @@ -15,8 +15,8 @@ import { categoryRoutes } from "./routes/categories.ts"; import { imageRoutes } from "./routes/images.ts"; import { itemRoutes } from "./routes/items.ts"; import { oauthRoutes, wellKnownRoute } from "./routes/oauth.ts"; -import { settingsRoutes } from "./routes/settings.ts"; import { profileRoutes } from "./routes/profiles.ts"; +import { settingsRoutes } from "./routes/settings.ts"; import { setupRoutes } from "./routes/setups.ts"; import { threadRoutes } from "./routes/threads.ts"; import { totalRoutes } from "./routes/totals.ts"; @@ -42,6 +42,24 @@ app.get("/api/health", (c) => { }); // ── OIDC Browser Auth (top-level, before /api/* middleware) ─────────── + +// In dev mode, strip Secure flag from OIDC cookies so they work over HTTP +if (process.env.NODE_ENV !== "production") { + app.use("*", async (c, next) => { + await next(); + const setCookies = c.res.headers.getSetCookie?.() ?? []; + if (setCookies.length > 0) { + c.res.headers.delete("Set-Cookie"); + for (const cookie of setCookies) { + c.res.headers.append( + "Set-Cookie", + cookie.replace(/;\s*Secure/gi, ""), + ); + } + } + }); +} + app.get("/login", oidcAuthMiddleware(), async (c) => c.redirect("/")); app.get("/callback", async (c) => processOAuthCallback(c)); app.get("/logout", async (c) => { diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index ca1c0e1..1c90411 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -39,7 +39,8 @@ export async function requireAuth(c: Context, next: Next) { c.set("userId", user.id); return next(); } - } catch { + } catch (err) { + console.error("[auth] OIDC auth failed:", err); // OIDC not configured or session invalid — fall through } diff --git a/src/server/routes/auth.ts b/src/server/routes/auth.ts index 8342b37..5b98ab7 100644 --- a/src/server/routes/auth.ts +++ b/src/server/routes/auth.ts @@ -1,7 +1,8 @@ -import { zValidator } from "@hono/zod-validator"; import { getAuth } from "@hono/oidc-auth"; +import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; import { z } from "zod"; +import { updateProfileSchema } from "../../shared/schemas.ts"; import { parseId } from "../lib/params.ts"; import { requireAuth } from "../middleware/auth.ts"; import { @@ -10,7 +11,6 @@ import { listApiKeys, } from "../services/auth.service.ts"; import { updateProfile } from "../services/profile.service.ts"; -import { updateProfileSchema } from "../../shared/schemas.ts"; type Env = { Variables: { db?: any; userId?: number } }; diff --git a/src/server/routes/setups.ts b/src/server/routes/setups.ts index bfb4d92..212c870 100644 --- a/src/server/routes/setups.ts +++ b/src/server/routes/setups.ts @@ -7,7 +7,6 @@ import { updateSetupSchema, } from "../../shared/schemas.ts"; import { parseId } from "../lib/params.ts"; -import { withImageUrls } from "../services/storage.service.ts"; import { getPublicSetupWithItems } from "../services/profile.service.ts"; import { createSetup, @@ -19,6 +18,7 @@ import { updateItemClassification, updateSetup, } from "../services/setup.service.ts"; +import { withImageUrls } from "../services/storage.service.ts"; type Env = { Variables: { db?: any; userId?: number } }; diff --git a/src/server/routes/threads.ts b/src/server/routes/threads.ts index a31671b..beca932 100644 --- a/src/server/routes/threads.ts +++ b/src/server/routes/threads.ts @@ -9,10 +9,7 @@ import { updateThreadSchema, } from "../../shared/schemas.ts"; import { parseId } from "../lib/params.ts"; -import { - deleteImage, - withImageUrls, -} from "../services/storage.service.ts"; +import { deleteImage, withImageUrls } from "../services/storage.service.ts"; import { createCandidate, createThread, diff --git a/src/server/services/auth.service.ts b/src/server/services/auth.service.ts index b5c9ced..5c71db2 100644 --- a/src/server/services/auth.service.ts +++ b/src/server/services/auth.service.ts @@ -1,6 +1,6 @@ import { randomBytes } from "node:crypto"; import { and, eq } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { apiKeys, users } from "../../db/schema.ts"; type Db = typeof prodDb; @@ -24,11 +24,7 @@ export async function getOrCreateUser( // ── API Key Management ─────────────────────────────────────────────── -export async function createApiKey( - db: Db, - userId: number, - name: string, -) { +export async function createApiKey(db: Db, userId: number, name: string) { const rawKey = randomBytes(32).toString("hex"); const keyHash = await Bun.password.hash(rawKey); const keyPrefix = rawKey.slice(0, 8); diff --git a/src/server/services/category.service.ts b/src/server/services/category.service.ts index b6804bb..b5705e6 100644 --- a/src/server/services/category.service.ts +++ b/src/server/services/category.service.ts @@ -20,76 +20,85 @@ export async function getOrCreateUncategorized(db: Db, userId: number) { return created; } -export function getAllCategories(db: Db = prodDb) { - return db.select().from(categories).orderBy(asc(categories.name)).all(); +export async function getAllCategories(db: Db, userId: number) { + return db + .select() + .from(categories) + .where(eq(categories.userId, userId)) + .orderBy(asc(categories.name)); } -export function createCategory( - db: Db = prodDb, +export async function createCategory( + db: Db, + userId: number, data: { name: string; icon?: string }, ) { - return db + const [row] = await db .insert(categories) .values({ name: data.name, + userId, ...(data.icon ? { icon: data.icon } : {}), }) - .returning() - .get(); + .returning(); + return row; } -export function updateCategory( - db: Db = prodDb, +export async function updateCategory( + db: Db, + userId: number, id: number, data: { name?: string; icon?: string }, ) { - const existing = db + const [existing] = await db .select({ id: categories.id }) .from(categories) - .where(eq(categories.id, id)) - .get(); + .where(and(eq(categories.id, id), eq(categories.userId, userId))); if (!existing) return null; - return db + const [row] = await db .update(categories) .set(data) - .where(eq(categories.id, id)) - .returning() - .get(); + .where(and(eq(categories.id, id), eq(categories.userId, userId))) + .returning(); + return row; } -export function deleteCategory( - db: Db = prodDb, +export async function deleteCategory( + db: Db, + userId: number, id: number, -): { success: boolean; error?: string } { - // Guard: cannot delete Uncategorized (id=1) - if (id === 1) { +): Promise<{ success: boolean; error?: string }> { + // Check if category exists and belongs to user + const [existing] = await db + .select({ id: categories.id, name: categories.name }) + .from(categories) + .where(and(eq(categories.id, id), eq(categories.userId, userId))); + + if (!existing) { + return { success: false, error: "Category not found" }; + } + + // Guard: cannot delete Uncategorized + if (existing.name === "Uncategorized") { return { success: false, error: "Cannot delete the Uncategorized category", }; } - // Check if category exists - const existing = db - .select({ id: categories.id }) - .from(categories) - .where(eq(categories.id, id)) - .get(); + // Get the user's Uncategorized category for reassignment + const uncategorized = await getOrCreateUncategorized(db, userId); - if (!existing) { - return { success: false, error: "Category not found" }; - } + // Reassign items to Uncategorized, then delete atomically + await db.transaction(async (tx) => { + await tx + .update(items) + .set({ categoryId: uncategorized.id }) + .where(eq(items.categoryId, id)); - // Reassign items to Uncategorized (id=1), then delete atomically - db.transaction(() => { - db.update(items) - .set({ categoryId: 1 }) - .where(eq(items.categoryId, id)) - .run(); - - db.delete(categories).where(eq(categories.id, id)).run(); + await tx.delete(categories).where(eq(categories.id, id)); }); return { success: true }; diff --git a/src/server/services/csv.service.ts b/src/server/services/csv.service.ts index bf0221a..7bdc520 100644 --- a/src/server/services/csv.service.ts +++ b/src/server/services/csv.service.ts @@ -1,5 +1,5 @@ import { and, eq } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { categories, items } from "../../db/schema.ts"; import { getOrCreateUncategorized } from "./category.service.ts"; diff --git a/src/server/services/global-item.service.ts b/src/server/services/global-item.service.ts index 4198fa9..e57fd58 100644 --- a/src/server/services/global-item.service.ts +++ b/src/server/services/global-item.service.ts @@ -5,12 +5,12 @@ import { globalItems, itemGlobalLinks } from "../../db/schema.ts"; type Db = typeof prodDb; /** - * Search global items by brand or model. SQLite LIKE is case-insensitive for ASCII. + * Search global items by brand or model. LIKE is case-insensitive for ASCII. * Escapes % and _ wildcard characters in user input. */ -export function searchGlobalItems(db: Db = prodDb, query?: string) { +export async function searchGlobalItems(db: Db = prodDb, query?: string) { if (!query) { - return db.select().from(globalItems).all(); + return db.select().from(globalItems); } // Escape SQL LIKE wildcards @@ -21,31 +21,28 @@ export function searchGlobalItems(db: Db = prodDb, query?: string) { .select() .from(globalItems) .where( - or( - like(globalItems.brand, pattern), - like(globalItems.model, pattern), - ), - ) - .all(); + or(like(globalItems.brand, pattern), like(globalItems.model, pattern)), + ); } /** * Get a single global item by ID with the count of user items linked to it. */ -export function getGlobalItemWithOwnerCount(db: Db = prodDb, id: number) { - const item = db +export async function getGlobalItemWithOwnerCount( + db: Db = prodDb, + id: number, +) { + const [item] = await db .select() .from(globalItems) - .where(eq(globalItems.id, id)) - .get(); + .where(eq(globalItems.id, id)); if (!item) return null; - const result = db + const [result] = await db .select({ ownerCount: count() }) .from(itemGlobalLinks) - .where(eq(itemGlobalLinks.globalItemId, id)) - .get(); + .where(eq(itemGlobalLinks.globalItemId, id)); return { ...item, ownerCount: result?.ownerCount ?? 0 }; } @@ -53,27 +50,26 @@ export function getGlobalItemWithOwnerCount(db: Db = prodDb, id: number) { /** * Link a user's item to a global item. Throws on duplicate (unique constraint on itemId). */ -export function linkItemToGlobal( +export async function linkItemToGlobal( db: Db = prodDb, itemId: number, globalItemId: number, ) { - return db + const [row] = await db .insert(itemGlobalLinks) .values({ itemId, globalItemId }) - .returning() - .get(); + .returning(); + return row; } /** * Remove the link between a user's item and any global item. */ -export function unlinkItemFromGlobal(db: Db = prodDb, itemId: number) { - const result = db +export async function unlinkItemFromGlobal(db: Db = prodDb, itemId: number) { + const result = await db .delete(itemGlobalLinks) .where(eq(itemGlobalLinks.itemId, itemId)) - .returning() - .all(); + .returning(); return result.length; } diff --git a/src/server/services/item.service.ts b/src/server/services/item.service.ts index f3c1aa7..0ef3c3b 100644 --- a/src/server/services/item.service.ts +++ b/src/server/services/item.service.ts @@ -1,5 +1,5 @@ import { and, eq } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { categories, items } from "../../db/schema.ts"; import type { CreateItem } from "../../shared/types.ts"; @@ -146,9 +146,7 @@ export async function deleteItem(db: Db, userId: number, id: number) { if (!item) return null; - await db - .delete(items) - .where(and(eq(items.id, id), eq(items.userId, userId))); + await db.delete(items).where(and(eq(items.id, id), eq(items.userId, userId))); return item; } diff --git a/src/server/services/oauth.service.ts b/src/server/services/oauth.service.ts index 32b4703..74a36a1 100644 --- a/src/server/services/oauth.service.ts +++ b/src/server/services/oauth.service.ts @@ -86,10 +86,7 @@ export async function exchangeCode( if (computedChallenge !== record.codeChallenge) return null; // Mark code as used - await db - .update(oauthCodes) - .set({ used: 1 }) - .where(eq(oauthCodes.code, code)); + await db.update(oauthCodes).set({ used: 1 }).where(eq(oauthCodes.code, code)); return generateTokens(db, clientId, userId); } diff --git a/src/server/services/profile.service.ts b/src/server/services/profile.service.ts index 779f40c..2afb514 100644 --- a/src/server/services/profile.service.ts +++ b/src/server/services/profile.service.ts @@ -1,5 +1,5 @@ import { and, eq, sql } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { categories, items, @@ -16,10 +16,7 @@ export async function updateProfile( userId: number, data: UpdateProfile, ) { - const [existing] = await db - .select() - .from(users) - .where(eq(users.id, userId)); + const [existing] = await db.select().from(users).where(eq(users.id, userId)); if (!existing) return null; // If no fields to update, return existing user diff --git a/src/server/services/setup.service.ts b/src/server/services/setup.service.ts index 08c61dd..95f502a 100644 --- a/src/server/services/setup.service.ts +++ b/src/server/services/setup.service.ts @@ -1,15 +1,11 @@ import { and, eq, inArray, sql } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { categories, items, setupItems, setups } from "../../db/schema.ts"; import type { CreateSetup, UpdateSetup } from "../../shared/types.ts"; type Db = typeof prodDb; -export async function createSetup( - db: Db, - userId: number, - data: CreateSetup, -) { +export async function createSetup(db: Db, userId: number, data: CreateSetup) { const [row] = await db .insert(setups) .values({ name: data.name, userId, isPublic: data.isPublic ?? false }) @@ -110,11 +106,7 @@ export async function updateSetup( return row; } -export async function deleteSetup( - db: Db, - userId: number, - setupId: number, -) { +export async function deleteSetup(db: Db, userId: number, setupId: number) { const [existing] = await db .select({ id: setups.id }) .from(setups) @@ -197,9 +189,7 @@ export async function updateItemClassification( await db .update(setupItems) .set({ classification }) - .where( - and(eq(setupItems.setupId, setupId), eq(setupItems.itemId, itemId)), - ); + .where(and(eq(setupItems.setupId, setupId), eq(setupItems.itemId, itemId))); } export async function removeSetupItem( @@ -217,7 +207,5 @@ export async function removeSetupItem( await db .delete(setupItems) - .where( - and(eq(setupItems.setupId, setupId), eq(setupItems.itemId, itemId)), - ); + .where(and(eq(setupItems.setupId, setupId), eq(setupItems.itemId, itemId))); } diff --git a/src/server/services/storage.service.ts b/src/server/services/storage.service.ts index d2f7674..c704694 100644 --- a/src/server/services/storage.service.ts +++ b/src/server/services/storage.service.ts @@ -62,9 +62,9 @@ export async function getImageUrl(filename: string): Promise { * Enrich a record that has an imageFilename with a presigned imageUrl. * Returns null imageUrl when imageFilename is null. */ -export async function withImageUrl< - T extends { imageFilename: string | null }, ->(record: T): Promise { +export async function withImageUrl( + record: T, +): Promise { return { ...record, imageUrl: record.imageFilename @@ -76,8 +76,8 @@ export async function withImageUrl< /** * Batch version of withImageUrl. Uses Promise.all for parallelism. */ -export async function withImageUrls< - T extends { imageFilename: string | null }, ->(records: T[]): Promise<(T & { imageUrl: string | null })[]> { +export async function withImageUrls( + records: T[], +): Promise<(T & { imageUrl: string | null })[]> { return Promise.all(records.map((record) => withImageUrl(record))); } diff --git a/src/server/services/thread.service.ts b/src/server/services/thread.service.ts index ad7383b..9aceabd 100644 --- a/src/server/services/thread.service.ts +++ b/src/server/services/thread.service.ts @@ -1,5 +1,5 @@ import { and, asc, desc, eq, max, sql } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { categories, items, @@ -15,11 +15,7 @@ import { getOrCreateUncategorized } from "./category.service.ts"; type Db = typeof prodDb; -export async function createThread( - db: Db, - userId: number, - data: CreateThread, -) { +export async function createThread(db: Db, userId: number, data: CreateThread) { const [row] = await db .insert(threads) .values({ name: data.name, categoryId: data.categoryId, userId }) @@ -258,9 +254,7 @@ export async function deleteCandidate( const [thread] = await db .select({ id: threads.id }) .from(threads) - .where( - and(eq(threads.id, candidate.threadId), eq(threads.userId, userId)), - ); + .where(and(eq(threads.id, candidate.threadId), eq(threads.userId, userId))); if (!thread) return null; await db.delete(threadCandidates).where(eq(threadCandidates.id, candidateId)); diff --git a/src/server/services/totals.service.ts b/src/server/services/totals.service.ts index d908d0c..c120caa 100644 --- a/src/server/services/totals.service.ts +++ b/src/server/services/totals.service.ts @@ -1,5 +1,5 @@ import { and, eq, sql } from "drizzle-orm"; -import { db as prodDb } from "../../db/index.ts"; +import type { db as prodDb } from "../../db/index.ts"; import { categories, items } from "../../db/schema.ts"; type Db = typeof prodDb; diff --git a/tests/routes/global-items.test.ts b/tests/routes/global-items.test.ts index 714c1b4..8097a44 100644 --- a/tests/routes/global-items.test.ts +++ b/tests/routes/global-items.test.ts @@ -30,11 +30,7 @@ function insertGlobalItem(db: TestDb, brand: string, model: string) { } function insertItem(db: TestDb, name: string) { - return db - .insert(items) - .values({ name, categoryId: 1 }) - .returning() - .get(); + return db.insert(items).values({ name, categoryId: 1 }).returning().get(); } describe("Global Item Routes", () => { diff --git a/tests/routes/images.test.ts b/tests/routes/images.test.ts index f39eaba..31ba263 100644 --- a/tests/routes/images.test.ts +++ b/tests/routes/images.test.ts @@ -16,7 +16,10 @@ mock.module("../../src/server/services/storage.service", () => ({ // Also mock image service for from-url test const mockFetchImageFromUrl = mock(() => - Promise.resolve({ filename: "test.png", sourceUrl: "https://example.com/img.png" }), + Promise.resolve({ + filename: "test.png", + sourceUrl: "https://example.com/img.png", + }), ); mock.module("../../src/server/services/image.service", () => ({ fetchImageFromUrl: mockFetchImageFromUrl, diff --git a/tests/routes/oauth.test.ts b/tests/routes/oauth.test.ts index 7c89b2c..93cc957 100644 --- a/tests/routes/oauth.test.ts +++ b/tests/routes/oauth.test.ts @@ -45,7 +45,10 @@ describe("OAuth Routes", () => { userId = testApp.userId; mockGetAuth.mockReset(); // Default: user is authenticated via OIDC - mockGetAuth.mockReturnValue({ sub: "test-user-logto-sub", email: "admin@example.com" }); + mockGetAuth.mockReturnValue({ + sub: "test-user-logto-sub", + email: "admin@example.com", + }); }); describe("GET /.well-known/oauth-authorization-server", () => { diff --git a/tests/routes/profiles.test.ts b/tests/routes/profiles.test.ts index daafce1..cf3e506 100644 --- a/tests/routes/profiles.test.ts +++ b/tests/routes/profiles.test.ts @@ -1,15 +1,17 @@ import { beforeEach, describe, expect, it } from "bun:test"; +import { zValidator } from "@hono/zod-validator"; import { eq } from "drizzle-orm"; import { Hono } from "hono"; import * as schema from "../../src/db/schema.ts"; -import { updateProfileSchema } from "../../src/shared/schemas.ts"; +import { parseId } from "../../src/server/lib/params.ts"; import { profileRoutes } from "../../src/server/routes/profiles.ts"; import { setupRoutes } from "../../src/server/routes/setups.ts"; -import { getPublicSetupWithItems } from "../../src/server/services/profile.service.ts"; -import { updateProfile } from "../../src/server/services/profile.service.ts"; +import { + getPublicSetupWithItems, + updateProfile, +} from "../../src/server/services/profile.service.ts"; +import { updateProfileSchema } from "../../src/shared/schemas.ts"; import { createTestDb } from "../helpers/db.ts"; -import { zValidator } from "@hono/zod-validator"; -import { parseId } from "../../src/server/lib/params.ts"; type Db = Awaited>["db"]; @@ -112,12 +114,10 @@ describe("Profile Routes", () => { it("includes only public setups", async () => { // Create public and private setups - await db - .insert(schema.setups) - .values([ - { name: "Public Setup", userId, isPublic: true }, - { name: "Private Setup", userId, isPublic: false }, - ]); + await db.insert(schema.setups).values([ + { name: "Public Setup", userId, isPublic: true }, + { name: "Private Setup", userId, isPublic: false }, + ]); const res = await app.request(`/api/users/${userId}/profile`); expect(res.status).toBe(200); diff --git a/tests/services/global-item.service.test.ts b/tests/services/global-item.service.test.ts index c75d108..fcc7587 100644 --- a/tests/services/global-item.service.test.ts +++ b/tests/services/global-item.service.test.ts @@ -1,12 +1,12 @@ import { beforeEach, describe, expect, it } from "bun:test"; import { globalItems, itemGlobalLinks, items } from "../../src/db/schema.ts"; +import { seedGlobalItems } from "../../src/db/seed-global-items.ts"; import { getGlobalItemWithOwnerCount, linkItemToGlobal, searchGlobalItems, unlinkItemFromGlobal, } from "../../src/server/services/global-item.service.ts"; -import { seedGlobalItems } from "../../src/db/seed-global-items.ts"; import { createTestDb } from "../helpers/db.ts"; type TestDb = ReturnType; @@ -35,11 +35,7 @@ function insertGlobalItem( } function insertItem(db: TestDb, name: string) { - return db - .insert(items) - .values({ name, categoryId: 1 }) - .returning() - .get(); + return db.insert(items).values({ name, categoryId: 1 }).returning().get(); } describe("Global Item Service", () => { @@ -51,7 +47,10 @@ describe("Global Item Service", () => { describe("searchGlobalItems", () => { it("returns all global items when no query provided", () => { - insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); + insertGlobalItem(db, { + brand: "Revelate Designs", + model: "Terrapin System", + }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db); @@ -59,7 +58,10 @@ describe("Global Item Service", () => { }); it("returns items matching brand (case-insensitive)", () => { - insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); + insertGlobalItem(db, { + brand: "Revelate Designs", + model: "Terrapin System", + }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db, "revelate"); @@ -68,7 +70,10 @@ describe("Global Item Service", () => { }); it("returns items matching model (case-insensitive)", () => { - insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); + insertGlobalItem(db, { + brand: "Revelate Designs", + model: "Terrapin System", + }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db, "HANDLEBAR"); @@ -77,7 +82,10 @@ describe("Global Item Service", () => { }); it("does not match everything with wildcard chars", () => { - insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); + insertGlobalItem(db, { + brand: "Revelate Designs", + model: "Terrapin System", + }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db, "100%"); @@ -87,7 +95,10 @@ describe("Global Item Service", () => { describe("getGlobalItemWithOwnerCount", () => { it("returns item with ownerCount 0 when no links", () => { - const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); + const gi = insertGlobalItem(db, { + brand: "MSR", + model: "PocketRocket 2", + }); const result = getGlobalItemWithOwnerCount(db, gi.id); expect(result).not.toBeNull(); @@ -96,7 +107,10 @@ describe("Global Item Service", () => { }); it("returns ownerCount matching number of linked items", () => { - const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); + const gi = insertGlobalItem(db, { + brand: "MSR", + model: "PocketRocket 2", + }); const item1 = insertItem(db, "My Stove"); const item2 = insertItem(db, "Another Stove"); @@ -120,7 +134,10 @@ describe("Global Item Service", () => { describe("linkItemToGlobal", () => { it("creates link and returns link row", () => { - const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); + const gi = insertGlobalItem(db, { + brand: "MSR", + model: "PocketRocket 2", + }); const item = insertItem(db, "My Stove"); const link = linkItemToGlobal(db, item.id, gi.id); @@ -129,7 +146,10 @@ describe("Global Item Service", () => { }); it("throws when item already linked", () => { - const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); + const gi = insertGlobalItem(db, { + brand: "MSR", + model: "PocketRocket 2", + }); const item = insertItem(db, "My Stove"); linkItemToGlobal(db, item.id, gi.id); @@ -139,7 +159,10 @@ describe("Global Item Service", () => { describe("unlinkItemFromGlobal", () => { it("removes the link", () => { - const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); + const gi = insertGlobalItem(db, { + brand: "MSR", + model: "PocketRocket 2", + }); const item = insertItem(db, "My Stove"); linkItemToGlobal(db, item.id, gi.id); diff --git a/tests/services/image.service.test.ts b/tests/services/image.service.test.ts index 3922314..a356a9e 100644 --- a/tests/services/image.service.test.ts +++ b/tests/services/image.service.test.ts @@ -21,12 +21,12 @@ const { fetchImageFromUrl } = await import( // 1x1 transparent PNG (smallest valid PNG) const TINY_PNG = new Uint8Array([ - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, - 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x62, 0x00, 0x00, 0x00, 0x02, - 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, - 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, + 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, + 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, + 0x41, 0x54, 0x78, 0x9c, 0x62, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, + 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, + 0x82, ]); let server: Server; @@ -72,17 +72,17 @@ describe("Image Service", () => { // Verify uploadImage was called with correct args expect(mockUploadImage).toHaveBeenCalledTimes(1); - const [buffer, filename, contentType] = - mockUploadImage.mock.calls[0] as unknown[]; + const [buffer, filename, contentType] = mockUploadImage.mock + .calls[0] as unknown[]; expect(buffer).toBeInstanceOf(Buffer); expect(filename).toBe(result.filename); expect(contentType).toBe("image/png"); }); it("rejects non-image content type", async () => { - await expect( - fetchImageFromUrl(`${baseUrl}/page.html`), - ).rejects.toThrow("Invalid content type"); + await expect(fetchImageFromUrl(`${baseUrl}/page.html`)).rejects.toThrow( + "Invalid content type", + ); }); it("rejects invalid URL format", async () => { @@ -98,9 +98,9 @@ describe("Image Service", () => { }); it("rejects 404 responses", async () => { - await expect( - fetchImageFromUrl(`${baseUrl}/missing.jpg`), - ).rejects.toThrow("HTTP 404"); + await expect(fetchImageFromUrl(`${baseUrl}/missing.jpg`)).rejects.toThrow( + "HTTP 404", + ); }); }); }); diff --git a/tests/services/profile.service.test.ts b/tests/services/profile.service.test.ts index 52f9278..96469d5 100644 --- a/tests/services/profile.service.test.ts +++ b/tests/services/profile.service.test.ts @@ -72,7 +72,10 @@ describe("Profile Service", () => { it("returns only public setups, not private ones", async () => { // Create one public and one private setup - const pub = await createSetup(db, userId, { name: "Public Setup", isPublic: true }); + const pub = await createSetup(db, userId, { + name: "Public Setup", + isPublic: true, + }); const priv = await createSetup(db, userId, { name: "Private Setup" }); const profile = await getPublicProfile(db, userId); diff --git a/tests/services/setup.service.test.ts b/tests/services/setup.service.test.ts index 840bdc1..6d27b40 100644 --- a/tests/services/setup.service.test.ts +++ b/tests/services/setup.service.test.ts @@ -240,13 +240,7 @@ describe("Setup Service", () => { await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]); // Change classifications - await updateItemClassification( - db, - userId, - setup.id, - item1.id, - "worn", - ); + await updateItemClassification(db, userId, setup.id, item1.id, "worn"); await updateItemClassification( db, userId, @@ -261,12 +255,8 @@ describe("Setup Service", () => { const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(2); - const item2Result = result?.items.find( - (i: any) => i.name === "Jacket", - ); - const item3Result = result?.items.find( - (i: any) => i.name === "Stove", - ); + const item2Result = result?.items.find((i: any) => i.name === "Jacket"); + const item3Result = result?.items.find((i: any) => i.name === "Stove"); expect(item2Result?.classification).toBe("consumable"); expect(item3Result?.classification).toBe("base"); }); @@ -293,13 +283,7 @@ describe("Setup Service", () => { }); await syncSetupItems(db, userId, setup.id, [item.id]); - await updateItemClassification( - db, - userId, - setup.id, - item.id, - "worn", - ); + await updateItemClassification(db, userId, setup.id, item.id, "worn"); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items[0].classification).toBe("worn"); @@ -318,13 +302,7 @@ describe("Setup Service", () => { expect(result?.items[0].classification).toBe("base"); // Update - await updateItemClassification( - db, - userId, - setup.id, - item.id, - "worn", - ); + await updateItemClassification(db, userId, setup.id, item.id, "worn"); result = await getSetupWithItems(db, userId, setup.id); expect(result?.items[0].classification).toBe("worn"); @@ -341,20 +319,8 @@ describe("Setup Service", () => { await syncSetupItems(db, userId, setup1.id, [item.id]); await syncSetupItems(db, userId, setup2.id, [item.id]); - await updateItemClassification( - db, - userId, - setup1.id, - item.id, - "worn", - ); - await updateItemClassification( - db, - userId, - setup2.id, - item.id, - "base", - ); + await updateItemClassification(db, userId, setup1.id, item.id, "worn"); + await updateItemClassification(db, userId, setup2.id, item.id, "base"); const result1 = await getSetupWithItems(db, userId, setup1.id); const result2 = await getSetupWithItems(db, userId, setup2.id); diff --git a/tests/services/storage.service.test.ts b/tests/services/storage.service.test.ts index 36edd9a..fd342ab 100644 --- a/tests/services/storage.service.test.ts +++ b/tests/services/storage.service.test.ts @@ -43,13 +43,8 @@ process.env.S3_BUCKET = "gearbox-images"; process.env.S3_REGION = "us-east-1"; // Import after mocking -const { - uploadImage, - deleteImage, - getImageUrl, - withImageUrl, - withImageUrls, -} = await import("@/server/services/storage.service"); +const { uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls } = + await import("@/server/services/storage.service"); describe("storage.service", () => { beforeEach(() => { @@ -66,7 +61,9 @@ describe("storage.service", () => { await uploadImage(buffer, "test-image.jpg", "image/jpeg"); expect(mockSend).toHaveBeenCalledTimes(1); - const command = mockSend.mock.calls[0][0] as { input: Record }; + const command = mockSend.mock.calls[0][0] as { + input: Record; + }; expect(command.input.Bucket).toBe("gearbox-images"); expect(command.input.Key).toBe("test-image.jpg"); expect(command.input.ContentType).toBe("image/jpeg"); @@ -78,7 +75,9 @@ describe("storage.service", () => { await uploadImage(arrayBuffer, "test.png", "image/png"); expect(mockSend).toHaveBeenCalledTimes(1); - const command = mockSend.mock.calls[0][0] as { input: Record }; + const command = mockSend.mock.calls[0][0] as { + input: Record; + }; expect(Buffer.isBuffer(command.input.Body)).toBe(true); }); }); @@ -88,7 +87,9 @@ describe("storage.service", () => { await deleteImage("test-image.jpg"); expect(mockSend).toHaveBeenCalledTimes(1); - const command = mockSend.mock.calls[0][0] as { input: Record }; + const command = mockSend.mock.calls[0][0] as { + input: Record; + }; expect(command.input.Bucket).toBe("gearbox-images"); expect(command.input.Key).toBe("test-image.jpg"); }); @@ -99,9 +100,7 @@ describe("storage.service", () => { const url = await getImageUrl("test-image.jpg"); expect(mockGetSignedUrl).toHaveBeenCalledTimes(1); - expect(url).toBe( - "https://minio:9000/gearbox-images/test.jpg?signed=1", - ); + expect(url).toBe("https://minio:9000/gearbox-images/test.jpg?signed=1"); }); }); diff --git a/tests/services/thread.service.test.ts b/tests/services/thread.service.test.ts index 729c56d..ec455c0 100644 --- a/tests/services/thread.service.test.ts +++ b/tests/services/thread.service.test.ts @@ -564,11 +564,7 @@ describe("Thread Service", () => { expect(result.item?.productUrl).toBe("https://example.com/tent"); // Thread should be resolved - const resolved = await getThreadWithCandidates( - db, - userId, - thread.id, - ); + const resolved = await getThreadWithCandidates(db, userId, thread.id); expect(resolved?.status).toBe("resolved"); expect(resolved?.resolvedCandidateId).toBe(candidate.id); }); @@ -585,12 +581,7 @@ describe("Thread Service", () => { await resolveThread(db, userId, thread.id, candidate.id); // Try to resolve again - const result = await resolveThread( - db, - userId, - thread.id, - candidate.id, - ); + const result = await resolveThread(db, userId, thread.id, candidate.id); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); @@ -609,12 +600,7 @@ describe("Thread Service", () => { categoryId: 1, }); - const result = await resolveThread( - db, - userId, - thread1.id, - candidate.id, - ); + const result = await resolveThread(db, userId, thread1.id, candidate.id); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); diff --git a/vite.config.ts b/vite.config.ts index 042aac9..1aae1ee 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,6 +18,11 @@ export default defineConfig({ proxy: { "/api": "http://localhost:3000", "/uploads": "http://localhost:3000", + "/login": { target: "http://localhost:3000", changeOrigin: false }, + "/callback": { target: "http://localhost:3000", changeOrigin: false }, + "/logout": { target: "http://localhost:3000", changeOrigin: false }, + "/.well-known": "http://localhost:3000", + "/oauth": "http://localhost:3000", }, }, build: {