claude-fix: settings.ts rewrite (appSettings + fields match), Nav theme path, mount settings route
This commit is contained in:
parent
a2ce92f0db
commit
573e2c9680
@ -8,6 +8,7 @@
|
||||
"started_at": "2026-05-23T05:20:11.407108",
|
||||
"attempted_features": [
|
||||
"settings-page",
|
||||
"api-client-phase5"
|
||||
"api-client-phase5",
|
||||
"router-phase5"
|
||||
]
|
||||
}
|
||||
@ -621,3 +621,29 @@ src/routes/settings.ts(3,10): error TS2305: Module '"../db/schema"' has no expor
|
||||
undefined
|
||||
/home/dark/Developer/EmberClone/apps/api:
|
||||
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
|
||||
- `05:26:14` **INFO** Committed feature router-phase5
|
||||
- `05:26:14` **INFO** Pushed: rc=0
|
||||
|
||||
## Phase-5 Run beendet (2026-05-23 05:26:14)
|
||||
|
||||
- `05:26:14` **INFO** OK: 3, Attempted: 3, Total: 6
|
||||
- `05:26:14` **INFO** Running db:generate + db:migrate for schema changes…
|
||||
- `05:26:15` **INFO** db:generate rc=0: /EmberClone/apps/api/drizzle.config.ts'
|
||||
5 tables
|
||||
app_settings 5 columns 0 indexes 0 fks
|
||||
customers 4 columns 0 indexes 0 fks
|
||||
projects 5 columns 0 indexes 1 fks
|
||||
time_entries 7 columns 0 indexes 2 fks
|
||||
users 6 columns 0 indexes 0 fks
|
||||
|
||||
[✓] Your SQL migration file ➜ drizzle/0001_minor_fabian_cortez.sql 🚀
|
||||
|
||||
- `05:26:16` **INFO** db:migrate rc=0:
|
||||
> @emberclone/api@0.0.1 db:migrate /home/dark/Developer/EmberClone/apps/api
|
||||
> tsx src/db/migrate.ts
|
||||
|
||||
Running migrations...
|
||||
Migrations completed successfully
|
||||
Checking for admin user...
|
||||
Admin user already exists
|
||||
|
||||
|
||||
7
apps/api/drizzle/0001_minor_fabian_cortez.sql
Normal file
7
apps/api/drizzle/0001_minor_fabian_cortez.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE IF NOT EXISTS "app_settings" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"workspace_name" text DEFAULT 'EmberClone' NOT NULL,
|
||||
"default_billable" boolean DEFAULT true NOT NULL,
|
||||
"week_start" integer DEFAULT 1 NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
310
apps/api/drizzle/meta/0001_snapshot.json
Normal file
310
apps/api/drizzle/meta/0001_snapshot.json
Normal file
@ -0,0 +1,310 @@
|
||||
{
|
||||
"id": "fda40693-a479-48ca-b84b-678a33a4d995",
|
||||
"prevId": "cf31898f-9a96-4dfd-ac9e-23326daf55fb",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.app_settings": {
|
||||
"name": "app_settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"workspace_name": {
|
||||
"name": "workspace_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'EmberClone'"
|
||||
},
|
||||
"default_billable": {
|
||||
"name": "default_billable",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"week_start": {
|
||||
"name": "week_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.customers": {
|
||||
"name": "customers",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.projects": {
|
||||
"name": "projects",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"projects_customer_id_customers_id_fk": {
|
||||
"name": "projects_customer_id_customers_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "customers",
|
||||
"columnsFrom": [
|
||||
"customer_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.time_entries": {
|
||||
"name": "time_entries",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"start_time": {
|
||||
"name": "start_time",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"end_time": {
|
||||
"name": "end_time",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"time_entries_user_id_users_id_fk": {
|
||||
"name": "time_entries_user_id_users_id_fk",
|
||||
"tableFrom": "time_entries",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"time_entries_project_id_projects_id_fk": {
|
||||
"name": "time_entries_project_id_projects_id_fk",
|
||||
"tableFrom": "time_entries",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,13 @@
|
||||
"when": 1779503479059,
|
||||
"tag": "0000_empty_talon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1779506775428,
|
||||
"tag": "0001_minor_fabian_cortez",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,95 +1,45 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { db } from "../db"
|
||||
import { settings } from "../db/schema"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { z } from "zod"
|
||||
import { db } from "../db"
|
||||
import { appSettings } from "../db/schema"
|
||||
|
||||
const SettingsUpdateSchema = z.object({
|
||||
siteName: z.string().min(1).optional(),
|
||||
maintenanceMode: z.boolean().optional(),
|
||||
contactEmail: z.string().email().optional(),
|
||||
allowRegistration: z.boolean().optional()
|
||||
workspaceName: z.string().min(1).optional(),
|
||||
defaultBillable: z.boolean().optional(),
|
||||
weekStart: z.number().int().min(0).max(6).optional(),
|
||||
})
|
||||
|
||||
export default async function settingsRoutes(fastify: FastifyInstance) {
|
||||
fastify.addHook("preHandler", async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return reply.code(401).send({ message: "Unauthorized" })
|
||||
}
|
||||
})
|
||||
|
||||
const isAdmin = (request: any) => {
|
||||
return (request.user as { sub: string, role: string })?.role === "admin"
|
||||
async function getOrCreate() {
|
||||
const rows = await db.select().from(appSettings).limit(1)
|
||||
if (rows.length > 0) return rows[0]
|
||||
const [created] = await db.insert(appSettings).values({}).returning()
|
||||
return created
|
||||
}
|
||||
|
||||
fastify.get("/", async (request, reply) => {
|
||||
const [currentSettings] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.limit(1)
|
||||
|
||||
if (!currentSettings) {
|
||||
// Lazy-init default settings if none exist
|
||||
const [defaults] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
siteName: "EmberClone",
|
||||
maintenanceMode: false,
|
||||
allowRegistration: true,
|
||||
contactEmail: "admin@example.com"
|
||||
})
|
||||
.returning()
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
return currentSettings
|
||||
})
|
||||
fastify.get("/", async () => getOrCreate())
|
||||
|
||||
fastify.patch("/", async (request, reply) => {
|
||||
if (!isAdmin(request)) {
|
||||
return reply.code(403).send({ message: "Forbidden: Admin role required" })
|
||||
const payload = request.user as { sub: string; role: string }
|
||||
if (payload.role !== "admin") {
|
||||
return reply.code(403).send({ message: "Admin only" })
|
||||
}
|
||||
|
||||
const body = SettingsUpdateSchema.parse(request.body)
|
||||
|
||||
// We assume there is only one settings row (singleton pattern)
|
||||
const current = await getOrCreate()
|
||||
const [updated] = await db
|
||||
.update(settings)
|
||||
.set(body)
|
||||
.where(eq(settings.id, "global")) // Assuming a fixed ID for global settings
|
||||
.update(appSettings)
|
||||
.set({ ...body, updatedAt: new Date() })
|
||||
.where(eq(appSettings.id, current.id))
|
||||
.returning()
|
||||
|
||||
if (!updated) {
|
||||
// If no row with ID 'global' exists, we create it or update the first available
|
||||
const [existing] = await db.select().from(settings).limit(1)
|
||||
|
||||
if (!existing) {
|
||||
const [created] = await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
id: "global",
|
||||
...body,
|
||||
siteName: body.siteName ?? "EmberClone",
|
||||
maintenanceMode: body.maintenanceMode ?? false,
|
||||
allowRegistration: body.allowRegistration ?? true,
|
||||
contactEmail: body.contactEmail ?? "admin@example.com"
|
||||
})
|
||||
.returning()
|
||||
return created
|
||||
}
|
||||
|
||||
const [finalUpdate] = await db
|
||||
.update(settings)
|
||||
.set(body)
|
||||
.where(eq(settings.id, existing.id))
|
||||
.returning()
|
||||
|
||||
return finalUpdate
|
||||
}
|
||||
|
||||
return updated
|
||||
})
|
||||
}
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { api } from "../lib/api"
|
||||
import { useTheme } from "../hooks/useTheme"
|
||||
import { useTheme } from "../lib/theme"
|
||||
|
||||
export default function Nav() {
|
||||
const location = useLocation()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user