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",
|
"started_at": "2026-05-23T05:20:11.407108",
|
||||||
"attempted_features": [
|
"attempted_features": [
|
||||||
"settings-page",
|
"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
|
undefined
|
||||||
/home/dark/Developer/EmberClone/apps/api:
|
/home/dark/Developer/EmberClone/apps/api:
|
||||||
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
|
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,
|
"when": 1779503479059,
|
||||||
"tag": "0000_empty_talon",
|
"tag": "0000_empty_talon",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1779506775428,
|
||||||
|
"tag": "0001_minor_fabian_cortez",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,95 +1,45 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { db } from "../db"
|
|
||||||
import { settings } from "../db/schema"
|
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { db } from "../db"
|
||||||
|
import { appSettings } from "../db/schema"
|
||||||
|
|
||||||
const SettingsUpdateSchema = z.object({
|
const SettingsUpdateSchema = z.object({
|
||||||
siteName: z.string().min(1).optional(),
|
workspaceName: z.string().min(1).optional(),
|
||||||
maintenanceMode: z.boolean().optional(),
|
defaultBillable: z.boolean().optional(),
|
||||||
contactEmail: z.string().email().optional(),
|
weekStart: z.number().int().min(0).max(6).optional(),
|
||||||
allowRegistration: z.boolean().optional()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default async function settingsRoutes(fastify: FastifyInstance) {
|
export default async function settingsRoutes(fastify: FastifyInstance) {
|
||||||
fastify.addHook("preHandler", async (request, reply) => {
|
fastify.addHook("preHandler", async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
await request.jwtVerify()
|
await request.jwtVerify()
|
||||||
} catch (err) {
|
} catch {
|
||||||
return reply.code(401).send({ message: "Unauthorized" })
|
return reply.code(401).send({ message: "Unauthorized" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const isAdmin = (request: any) => {
|
async function getOrCreate() {
|
||||||
return (request.user as { sub: string, role: string })?.role === "admin"
|
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) => {
|
fastify.get("/", async () => getOrCreate())
|
||||||
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.patch("/", async (request, reply) => {
|
fastify.patch("/", async (request, reply) => {
|
||||||
if (!isAdmin(request)) {
|
const payload = request.user as { sub: string; role: string }
|
||||||
return reply.code(403).send({ message: "Forbidden: Admin role required" })
|
if (payload.role !== "admin") {
|
||||||
|
return reply.code(403).send({ message: "Admin only" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = SettingsUpdateSchema.parse(request.body)
|
const body = SettingsUpdateSchema.parse(request.body)
|
||||||
|
const current = await getOrCreate()
|
||||||
// We assume there is only one settings row (singleton pattern)
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(settings)
|
.update(appSettings)
|
||||||
.set(body)
|
.set({ ...body, updatedAt: new Date() })
|
||||||
.where(eq(settings.id, "global")) // Assuming a fixed ID for global settings
|
.where(eq(appSettings.id, current.id))
|
||||||
.returning()
|
.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
|
return updated
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { api } from "../lib/api"
|
import { api } from "../lib/api"
|
||||||
import { useTheme } from "../hooks/useTheme"
|
import { useTheme } from "../lib/theme"
|
||||||
|
|
||||||
export default function Nav() {
|
export default function Nav() {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user