From dd943f7fb29867c5b19d9675042ea51f6959d417 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 27 Dec 2025 01:30:23 +0100 Subject: [PATCH] feat(auth): implement default user ID handling when auth is disabled across routes --- backend/src/db/client.ts | 34 +++++++++++++++++++------- backend/src/routes/doses.ts | 40 ++++++++++++++++++------------- backend/src/routes/medications.ts | 26 +++++++++++++------- backend/src/routes/settings.ts | 32 ++++++++++++++++--------- backend/src/routes/share.ts | 33 ++++++++++++++++--------- 5 files changed, 109 insertions(+), 56 deletions(-) diff --git a/backend/src/db/client.ts b/backend/src/db/client.ts index 6870438..3fca9a6 100644 --- a/backend/src/db/client.ts +++ b/backend/src/db/client.ts @@ -1,21 +1,20 @@ import { createClient } from "@libsql/client"; import { drizzle } from "drizzle-orm/libsql"; import { existsSync, mkdirSync } from "fs"; -import { dirname } from "path"; +import { dirname, resolve } from "path"; import dotenv from "dotenv"; dotenv.config({ path: process.env.DOTENV_PATH || ".env" }); -const url = "file:./data/medassist-ng.db"; +// Use absolute path to ensure it works in Docker +const dataDir = resolve(process.cwd(), "data"); +const dbPath = resolve(dataDir, "medassist-ng.db"); +const url = `file:${dbPath}`; // Ensure data directory exists before creating database -if (url.startsWith("file:")) { - const dbPath = url.replace("file:", ""); - const dataDir = dirname(dbPath); - if (!existsSync(dataDir)) { - mkdirSync(dataDir, { recursive: true }); - console.log(`[DB] Created data directory: ${dataDir}`); - } +if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); + console.log(`[DB] Created data directory: ${dataDir}`); } const client = createClient({ url }); @@ -145,6 +144,23 @@ async function runMigrations() { } } } + + // If auth is disabled, ensure a default user exists (ID=1) + const authEnabled = process.env.AUTH_ENABLED === "true"; + if (!authEnabled) { + try { + // Check if default user exists + const result = await client.execute("SELECT id FROM users WHERE id = 1"); + if (result.rows.length === 0) { + await client.execute( + "INSERT INTO users (id, username, auth_provider) VALUES (1, 'default', 'local')" + ); + console.log(`[DB] Created default user for auth-disabled mode`); + } + } catch (e: any) { + console.error(`[DB] Error creating default user:`, e.message); + } + } } // Export promise so server can await it before starting diff --git a/backend/src/routes/doses.ts b/backend/src/routes/doses.ts index e3583e9..5d74cbd 100644 --- a/backend/src/routes/doses.ts +++ b/backend/src/routes/doses.ts @@ -4,6 +4,7 @@ import { db } from "../db/client.js"; import { doseTracking, shareTokens } from "../db/schema.js"; import { eq, and, gte } from "drizzle-orm"; import { requireAuth } from "../plugins/auth.js"; +import { env } from "../plugins/env.js"; import type { AuthUser } from "../types/fastify.js"; // ============================================================================= @@ -17,6 +18,22 @@ const shareDoseSchema = z.object({ doseId: z.string().min(1, "doseId is required"), }); +// Helper to get user ID from request +// Returns a default user ID when auth is disabled +function getUserId(request: any, reply: any): number { + // If auth is disabled, use a default user ID (1) + if (!env.AUTH_ENABLED) { + return 1; + } + + const authUser = request.user as unknown as AuthUser | null; + if (!authUser) { + reply.status(401).send({ error: "Not authenticated" }); + throw new Error("AUTH_REQUIRED"); + } + return authUser.id; +} + // ============================================================================= // Dose Tracking Routes // ============================================================================= @@ -28,10 +45,7 @@ export async function doseRoutes(app: FastifyInstance) { "/doses/taken", { preHandler: requireAuth }, async (request, reply) => { - const authUser = request.user as unknown as AuthUser | null; - if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); - } + const userId = getUserId(request, reply); // Get doses from last 30 days (to avoid loading too much data) const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); @@ -40,7 +54,7 @@ export async function doseRoutes(app: FastifyInstance) { .from(doseTracking) .where( and( - eq(doseTracking.userId, authUser.id), + eq(doseTracking.userId, userId), gte(doseTracking.takenAt, thirtyDaysAgo) ) ); @@ -62,10 +76,7 @@ export async function doseRoutes(app: FastifyInstance) { "/doses/taken", { preHandler: requireAuth }, async (request, reply) => { - const authUser = request.user as unknown as AuthUser | null; - if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); - } + const userId = getUserId(request, reply); const parsed = markDoseSchema.safeParse(request.body); if (!parsed.success) { @@ -81,7 +92,7 @@ export async function doseRoutes(app: FastifyInstance) { .from(doseTracking) .where( and( - eq(doseTracking.userId, authUser.id), + eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId) ) ); @@ -92,7 +103,7 @@ export async function doseRoutes(app: FastifyInstance) { // Insert new record await db.insert(doseTracking).values({ - userId: authUser.id, + userId, doseId, markedBy: null, // Marked by the user themselves }); @@ -108,16 +119,13 @@ export async function doseRoutes(app: FastifyInstance) { "/doses/taken/:doseId", { preHandler: requireAuth }, async (request, reply) => { - const authUser = request.user as unknown as AuthUser | null; - if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); - } + const userId = getUserId(request, reply); const { doseId } = request.params; await db.delete(doseTracking).where( and( - eq(doseTracking.userId, authUser.id), + eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId) ) ); diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts index 4f0a9f1..ddce6e5 100644 --- a/backend/src/routes/medications.ts +++ b/backend/src/routes/medications.ts @@ -58,16 +58,24 @@ export async function medicationRoutes(app: FastifyInstance) { app.addHook("preHandler", requireAuth); // Helper to get user ID from request - function getUserId(request: any): number { + // Returns a default user ID when auth is disabled + function getUserId(request: any, reply: any): number { + // If auth is disabled, use a default user ID (1) + if (!env.AUTH_ENABLED) { + return 1; + } + const authUser = request.user as unknown as AuthUser | null; if (!authUser) { - throw new Error("User not authenticated"); + // This should never happen if requireAuth worked, but be safe + reply.status(401).send({ error: "User not authenticated", code: "AUTH_REQUIRED" }); + throw new Error("AUTH_REQUIRED"); } return authUser.id; } app.get("/medications", async (request, reply) => { - const userId = getUserId(request); + const userId = getUserId(request, reply); const rows = await db.select().from(medications).where(eq(medications.userId, userId)).orderBy(medications.id); return rows.map((row) => ({ id: row.id, @@ -95,7 +103,7 @@ export async function medicationRoutes(app: FastifyInstance) { const parsed = medicationSchema.safeParse(req.body); if (!parsed.success) return reply.status(400).send(parsed.error.format()); - const userId = getUserId(req); + const userId = getUserId(req, reply); const { name, genericName, takenBy, packCount, stripsPerPack, tabsPerStrip, looseTablets, pillWeightMg, expiryDate, notes, intakeRemindersEnabled, slices } = parsed.data; const usageJson = JSON.stringify(slices.map((s) => s.usage)); const everyJson = JSON.stringify(slices.map((s) => s.every)); @@ -155,7 +163,7 @@ export async function medicationRoutes(app: FastifyInstance) { const idNum = Number(req.params.id); if (Number.isNaN(idNum)) return reply.badRequest("Invalid id"); - const userId = getUserId(req); + const userId = getUserId(req, reply); // Verify ownership const [existing] = await db.select().from(medications).where(and(eq(medications.id, idNum), eq(medications.userId, userId))); @@ -221,7 +229,7 @@ export async function medicationRoutes(app: FastifyInstance) { const idNum = Number(req.params.id); if (Number.isNaN(idNum)) return reply.badRequest("Invalid id"); - const userId = getUserId(req); + const userId = getUserId(req, reply); // Delete associated image if exists (with ownership check) const [existing] = await db.select().from(medications).where(and(eq(medications.id, idNum), eq(medications.userId, userId))); @@ -242,7 +250,7 @@ export async function medicationRoutes(app: FastifyInstance) { const idNum = Number(req.params.id); if (Number.isNaN(idNum)) return reply.badRequest("Invalid id"); - const userId = getUserId(req); + const userId = getUserId(req, reply); const [existing] = await db.select().from(medications).where(and(eq(medications.id, idNum), eq(medications.userId, userId))); if (!existing) return reply.notFound(); @@ -276,7 +284,7 @@ export async function medicationRoutes(app: FastifyInstance) { const idNum = Number(req.params.id); if (Number.isNaN(idNum)) return reply.badRequest("Invalid id"); - const userId = getUserId(req); + const userId = getUserId(req, reply); const [existing] = await db.select().from(medications).where(and(eq(medications.id, idNum), eq(medications.userId, userId))); if (!existing) return reply.notFound(); @@ -300,7 +308,7 @@ export async function medicationRoutes(app: FastifyInstance) { return reply.badRequest("Invalid date range"); } - const userId = getUserId(req); + const userId = getUserId(req, reply); const rows = await db.select().from(medications).where(eq(medications.userId, userId)).orderBy(medications.id); const now = new Date(); diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 8e784bb..1b8fc48 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -145,14 +145,27 @@ export async function settingsRoutes(app: FastifyInstance) { // All settings routes require auth app.addHook("preHandler", requireAuth); - // Get settings for current user - app.get("/settings", async (request, reply) => { + // Helper to get user ID from request + // Returns a default user ID when auth is disabled + function getUserId(request: any, reply: any): number { + // If auth is disabled, use a default user ID (1) + if (!env.AUTH_ENABLED) { + return 1; + } + const authUser = request.user as unknown as AuthUser | null; if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); + reply.status(401).send({ error: "Not authenticated" }); + throw new Error("AUTH_REQUIRED"); } + return authUser.id; + } - const settings = await getOrCreateUserSettings(authUser.id); + // Get settings for current user + app.get("/settings", async (request, reply) => { + const userId = getUserId(request, reply); + + const settings = await getOrCreateUserSettings(userId); return reply.send({ // User notification settings (from DB) @@ -188,10 +201,7 @@ export async function settingsRoutes(app: FastifyInstance) { // Update settings for current user app.put<{ Body: SettingsBody }>("/settings", async (request, reply) => { - const authUser = request.user as unknown as AuthUser | null; - if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); - } + const userId = getUserId(request, reply); const body = request.body; @@ -204,7 +214,7 @@ export async function settingsRoutes(app: FastifyInstance) { const repeatDailyReminders = hasAnyStockReminder ? (body.repeatDailyReminders ?? false) : false; // Update or insert user settings - const existingSettings = await db.select().from(userSettings).where(eq(userSettings.userId, authUser.id)); + const existingSettings = await db.select().from(userSettings).where(eq(userSettings.userId, userId)); const settingsData = { emailEnabled: body.emailEnabled, @@ -227,10 +237,10 @@ export async function settingsRoutes(app: FastifyInstance) { if (existingSettings.length > 0) { await db.update(userSettings) .set(settingsData) - .where(eq(userSettings.userId, authUser.id)); + .where(eq(userSettings.userId, userId)); } else { await db.insert(userSettings).values({ - userId: authUser.id, + userId: userId, ...settingsData, }); } diff --git a/backend/src/routes/share.ts b/backend/src/routes/share.ts index 12069a2..73ee0db 100644 --- a/backend/src/routes/share.ts +++ b/backend/src/routes/share.ts @@ -5,6 +5,7 @@ import { db } from "../db/client.js"; import { medications, shareTokens } from "../db/schema.js"; import { eq, and } from "drizzle-orm"; import { requireAuth, optionalAuth } from "../plugins/auth.js"; +import { env } from "../plugins/env.js"; import type { AuthUser } from "../types/fastify.js"; // ============================================================================= @@ -15,6 +16,22 @@ const createShareSchema = z.object({ scheduleDays: z.number().int().min(1).max(365).default(30), }); +// Helper to get user ID from request +// Returns a default user ID when auth is disabled +function getUserId(request: any, reply: any): number { + // If auth is disabled, use a default user ID (1) + if (!env.AUTH_ENABLED) { + return 1; + } + + const authUser = request.user as unknown as AuthUser | null; + if (!authUser) { + reply.status(401).send({ error: "Not authenticated" }); + throw new Error("AUTH_REQUIRED"); + } + return authUser.id; +} + // ============================================================================= // Share Routes // ============================================================================= @@ -79,10 +96,7 @@ export async function shareRoutes(app: FastifyInstance) { "/share", { preHandler: requireAuth }, async (request, reply) => { - const authUser = request.user as unknown as AuthUser | null; - if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); - } + const userId = getUserId(request, reply); const parsed = createShareSchema.safeParse(request.body); if (!parsed.success) { @@ -97,7 +111,7 @@ export async function shareRoutes(app: FastifyInstance) { // Check if user has medications for this takenBy const [existingMed] = await db.select().from(medications).where( and( - eq(medications.userId, authUser.id), + eq(medications.userId, userId), eq(medications.takenBy, takenBy) ) ); @@ -114,7 +128,7 @@ export async function shareRoutes(app: FastifyInstance) { // Create share token await db.insert(shareTokens).values({ - userId: authUser.id, + userId: userId, token, takenBy, scheduleDays, @@ -134,15 +148,12 @@ export async function shareRoutes(app: FastifyInstance) { "/share/people", { preHandler: requireAuth }, async (request, reply) => { - const authUser = request.user as unknown as AuthUser | null; - if (!authUser) { - return reply.status(401).send({ error: "Not authenticated" }); - } + const userId = getUserId(request, reply); // Get all unique takenBy values for this user const meds = await db.select({ takenBy: medications.takenBy }) .from(medications) - .where(eq(medications.userId, authUser.id)); + .where(eq(medications.userId, userId)); const uniquePeople = [...new Set(meds.map((m) => m.takenBy).filter(Boolean))] as string[];