diff --git a/apps/api/.env.example b/apps/api/.env.example index f62b165..460910a 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -3,7 +3,8 @@ BETTER_AUTH_SECRET=change-me-to-a-long-random-string BETTER_AUTH_URL=http://localhost:3000 PORT=3000 -# Comma-separated browser origins allowed for CORS + better-auth (the worker SPA). +# Comma-separated browser origins allowed for CORS + better-auth (the worker SPA on 5173 +# and the admin SPA on 5174). # Add your phone's LAN origin to test on a device — no code edit needed, e.g.: # CORS_ORIGINS=http://localhost:5173,http://192.168.1.50:5173 -CORS_ORIGINS=http://localhost:5173 +CORS_ORIGINS=http://localhost:5173,http://localhost:5174 diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts index 882c883..177ba2e 100644 --- a/apps/api/src/env.ts +++ b/apps/api/src/env.ts @@ -9,6 +9,10 @@ export const env = { PORT: Number(process.env.PORT ?? 3000), // Browser origins allowed for CORS + better-auth trustedOrigins. Set CORS_ORIGINS to a // comma-separated list (e.g. "http://localhost:5173,http://192.168.1.50:5173") to let a - // phone on the LAN reach the API — no code edit needed. Defaults to the local Vite origin. - WEB_ORIGINS: webOrigins && webOrigins.length ? webOrigins : ['http://localhost:5173'], + // phone on the LAN reach the API — no code edit needed. Defaults to the local Vite origins + // for the worker (5173) and admin (5174) SPAs. + WEB_ORIGINS: + webOrigins && webOrigins.length + ? webOrigins + : ['http://localhost:5173', 'http://localhost:5174'], }; diff --git a/apps/api/src/routes/me.ts b/apps/api/src/routes/me.ts index 5c962ef..af65f9e 100644 --- a/apps/api/src/routes/me.ts +++ b/apps/api/src/routes/me.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import type { MeResponse } from '@solelog/shared'; +import type { MeResponse, Role } from '@solelog/shared'; import { auth } from '../auth'; export const me = new Hono(); @@ -14,6 +14,7 @@ me.get('/api/me', async (c) => { id: session.user.id, email: session.user.email, name: session.user.name, + role: ((session.user as { role?: string | null }).role ?? 'worker') as Role, }, }; return c.json(body); diff --git a/apps/api/test/cors.test.ts b/apps/api/test/cors.test.ts index 9a6e525..9f56822 100644 --- a/apps/api/test/cors.test.ts +++ b/apps/api/test/cors.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { createApp } from '../src/app'; const ORIGIN = 'http://localhost:5173'; +const ADMIN_ORIGIN = 'http://localhost:5174'; describe('cors', () => { it('answers a CORS preflight for the SPA origin', async () => { @@ -18,6 +19,20 @@ describe('cors', () => { expect(allowMethods).toContain('GET'); }); + it('answers a CORS preflight for the admin SPA origin', async () => { + const app = createApp(); + const res = await app.request('/api/activities', { + method: 'OPTIONS', + headers: { + Origin: ADMIN_ORIGIN, + 'Access-Control-Request-Method': 'GET', + }, + }); + expect(res.headers.get('access-control-allow-origin')).toBe(ADMIN_ORIGIN); + const allowMethods = res.headers.get('access-control-allow-methods') ?? ''; + expect(allowMethods).toContain('GET'); + }); + it('exposes set-auth-token to the SPA origin', async () => { const app = createApp(); const res = await app.request('/api/activities', { diff --git a/apps/api/test/me.test.ts b/apps/api/test/me.test.ts index f773936..fcd6264 100644 --- a/apps/api/test/me.test.ts +++ b/apps/api/test/me.test.ts @@ -19,4 +19,24 @@ describe('GET /api/me', () => { const body = await res.json(); expect(body.user.email).toBe(email); }); + + it('returns role "worker" for a worker token', async () => { + const app = createApp(); + const token = await authToken(app, 'worker-role@example.com', 'worker'); + + const res = await app.request('/api/me', { headers: bearer(token) }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.user.role).toBe('worker'); + }); + + it('returns role "admin" for an admin token', async () => { + const app = createApp(); + const token = await authToken(app, 'admin-role@example.com', 'admin'); + + const res = await app.request('/api/me', { headers: bearer(token) }); + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.user.role).toBe('admin'); + }); }); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index bd1dfbb..86401c2 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -5,10 +5,14 @@ export const HealthResponse = z.object({ }); export type HealthResponse = z.infer; +export const Role = z.enum(['worker', 'admin']); +export type Role = z.infer; + export const PublicUser = z.object({ id: z.string(), email: z.string().email(), name: z.string(), + role: Role, }); export type PublicUser = z.infer; @@ -20,9 +24,6 @@ export type MeResponse = z.infer; export const InsoleType = z.enum(['Kurk', 'Berk', '3D']); export type InsoleType = z.infer; -export const Role = z.enum(['worker', 'admin']); -export type Role = z.infer; - export const Activity = z.object({ id: z.number().int(), name: z.string(),