mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
backend: per-user settings store (/api/settings) + enable account deletion
- user_settings table (userId PK -> user, jsonb preferences) + migration - GET/PUT /api/settings (requireAuth, user-scoped; zod-validated flat map) - Better Auth user.deleteUser enabled so users can delete their own account Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE "user_settings" (
|
||||
"user_id" text PRIMARY KEY NOT NULL,
|
||||
"preferences" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user_settings" ADD CONSTRAINT "user_settings_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,13 @@
|
||||
"when": 1781022080228,
|
||||
"tag": "0009_melted_puck",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "7",
|
||||
"when": 1781111039837,
|
||||
"tag": "0010_eager_hellfire_club",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -43,6 +43,14 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
|
||||
user: {
|
||||
// Lets a signed-in user delete their own account from Settings. The
|
||||
// frontend confirms with the user's password (authClient.deleteUser).
|
||||
deleteUser: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
|
||||
emailVerification: {
|
||||
sendOnSignUp: true,
|
||||
autoSignInAfterVerification: true,
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./tasks.js";
|
||||
export * from "./activity.js";
|
||||
export * from "./messaging.js";
|
||||
export * from "./notifications.js";
|
||||
export * from "./settings.js";
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||
|
||||
import { user } from "./auth.js";
|
||||
|
||||
// Per-user preferences (notification toggles, profile extras). One row per
|
||||
// user, keyed by the Better Auth user id; the payload is a flat JSONB map so
|
||||
// the frontend can add settings without a migration.
|
||||
export const userSettings = pgTable("user_settings", {
|
||||
userId: text("user_id")
|
||||
.primaryKey()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
preferences: jsonb("preferences")
|
||||
.$type<Record<string, boolean | string>>()
|
||||
.notNull()
|
||||
.default({}),
|
||||
updatedAt: timestamp("updated_at")
|
||||
.defaultNow()
|
||||
.$onUpdate(() => new Date())
|
||||
.notNull(),
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { notesRouter } from "./routes/notes.js";
|
||||
import { notificationsRouter } from "./routes/notifications.js";
|
||||
import { patientsRouter } from "./routes/patients.js";
|
||||
import { prescriptionsRouter } from "./routes/prescriptions.js";
|
||||
import { settingsRouter } from "./routes/settings.js";
|
||||
import { staffRouter } from "./routes/staff.js";
|
||||
import { tasksRouter } from "./routes/tasks.js";
|
||||
|
||||
@@ -64,6 +65,7 @@ app.use("/api/activity", activityRouter);
|
||||
app.use("/api/analytics", analyticsRouter);
|
||||
app.use("/api/conversations", conversationsRouter);
|
||||
app.use("/api/notifications", notificationsRouter);
|
||||
app.use("/api/settings", settingsRouter);
|
||||
|
||||
app.use(notFound);
|
||||
app.use(errorHandler);
|
||||
@@ -85,4 +87,5 @@ server.listen(env.PORT, () => {
|
||||
console.log(` • stats: /api/analytics`);
|
||||
console.log(` • messages: /api/conversations (+ Socket.io)`);
|
||||
console.log(` • notifs: /api/notifications`);
|
||||
console.log(` • settings: /api/settings`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Payload accepted by PUT /api/settings: a flat map of preference keys to
|
||||
// booleans or short strings. Capped so the JSONB column can't grow unbounded.
|
||||
export const settingsInputSchema = z.object({
|
||||
preferences: z
|
||||
.record(
|
||||
z.string().min(1).max(64),
|
||||
z.union([z.boolean(), z.string().max(500)]),
|
||||
)
|
||||
.refine((prefs) => Object.keys(prefs).length <= 100, {
|
||||
message: "Too many preference keys.",
|
||||
}),
|
||||
});
|
||||
|
||||
export type SettingsInput = z.infer<typeof settingsInputSchema>;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Router } from "express";
|
||||
|
||||
import { db } from "../db/index.js";
|
||||
import { userSettings } from "../db/schema/settings.js";
|
||||
import { settingsInputSchema } from "../lib/settings-validation.js";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
export const settingsRouter = Router();
|
||||
|
||||
// Settings are per-user (not per-clinic), so only authentication is required —
|
||||
// no active organization or RBAC permission.
|
||||
settingsRouter.use(requireAuth);
|
||||
|
||||
settingsRouter.get("/", async (req, res, next) => {
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ preferences: userSettings.preferences })
|
||||
.from(userSettings)
|
||||
.where(eq(userSettings.userId, req.user!.id))
|
||||
.limit(1);
|
||||
res.json({ preferences: rows[0]?.preferences ?? {} });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
settingsRouter.put("/", async (req, res, next) => {
|
||||
try {
|
||||
const { preferences } = settingsInputSchema.parse(req.body);
|
||||
await db
|
||||
.insert(userSettings)
|
||||
.values({ userId: req.user!.id, preferences })
|
||||
.onConflictDoUpdate({
|
||||
target: userSettings.userId,
|
||||
set: { preferences, updatedAt: new Date() },
|
||||
});
|
||||
res.json({ preferences });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user