feat(auth): implement default user ID handling when auth is disabled across routes

This commit is contained in:
Daniel Volz
2025-12-27 01:30:23 +01:00
parent 89d0c3f3f1
commit dd943f7fb2
5 changed files with 109 additions and 56 deletions
+25 -9
View File
@@ -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
+24 -16
View File
@@ -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)
)
);
+17 -9
View File
@@ -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();
+21 -11
View File
@@ -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,
});
}
+22 -11
View File
@@ -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[];