feat(api): include role in /api/me + allow admin origin in CORS
Add `role: Role` to the shared `PublicUser` contract and return it from `GET /api/me` (defaulting to 'worker' when the session user has no role). This lets the planned admin app gate access by role. Also add the admin dev origin `http://localhost:5174` to the default `WEB_ORIGINS` (env.ts) and to `.env.example`, so the admin SPA on :5174 can reach the API at :3000 cross-origin (drives both hono/cors and better-auth trustedOrigins).
This commit is contained in:
@@ -3,7 +3,8 @@ BETTER_AUTH_SECRET=change-me-to-a-long-random-string
|
|||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
PORT=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.:
|
# 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,http://192.168.1.50:5173
|
||||||
CORS_ORIGINS=http://localhost:5173
|
CORS_ORIGINS=http://localhost:5173,http://localhost:5174
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export const env = {
|
|||||||
PORT: Number(process.env.PORT ?? 3000),
|
PORT: Number(process.env.PORT ?? 3000),
|
||||||
// Browser origins allowed for CORS + better-auth trustedOrigins. Set CORS_ORIGINS to a
|
// 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
|
// 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.
|
// phone on the LAN reach the API — no code edit needed. Defaults to the local Vite origins
|
||||||
WEB_ORIGINS: webOrigins && webOrigins.length ? webOrigins : ['http://localhost:5173'],
|
// for the worker (5173) and admin (5174) SPAs.
|
||||||
|
WEB_ORIGINS:
|
||||||
|
webOrigins && webOrigins.length
|
||||||
|
? webOrigins
|
||||||
|
: ['http://localhost:5173', 'http://localhost:5174'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import type { MeResponse } from '@solelog/shared';
|
import type { MeResponse, Role } from '@solelog/shared';
|
||||||
import { auth } from '../auth';
|
import { auth } from '../auth';
|
||||||
|
|
||||||
export const me = new Hono();
|
export const me = new Hono();
|
||||||
@@ -14,6 +14,7 @@ me.get('/api/me', async (c) => {
|
|||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
email: session.user.email,
|
email: session.user.email,
|
||||||
name: session.user.name,
|
name: session.user.name,
|
||||||
|
role: ((session.user as { role?: string | null }).role ?? 'worker') as Role,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return c.json(body);
|
return c.json(body);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { createApp } from '../src/app';
|
import { createApp } from '../src/app';
|
||||||
|
|
||||||
const ORIGIN = 'http://localhost:5173';
|
const ORIGIN = 'http://localhost:5173';
|
||||||
|
const ADMIN_ORIGIN = 'http://localhost:5174';
|
||||||
|
|
||||||
describe('cors', () => {
|
describe('cors', () => {
|
||||||
it('answers a CORS preflight for the SPA origin', async () => {
|
it('answers a CORS preflight for the SPA origin', async () => {
|
||||||
@@ -18,6 +19,20 @@ describe('cors', () => {
|
|||||||
expect(allowMethods).toContain('GET');
|
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 () => {
|
it('exposes set-auth-token to the SPA origin', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const res = await app.request('/api/activities', {
|
const res = await app.request('/api/activities', {
|
||||||
|
|||||||
@@ -19,4 +19,24 @@ describe('GET /api/me', () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.user.email).toBe(email);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ export const HealthResponse = z.object({
|
|||||||
});
|
});
|
||||||
export type HealthResponse = z.infer<typeof HealthResponse>;
|
export type HealthResponse = z.infer<typeof HealthResponse>;
|
||||||
|
|
||||||
|
export const Role = z.enum(['worker', 'admin']);
|
||||||
|
export type Role = z.infer<typeof Role>;
|
||||||
|
|
||||||
export const PublicUser = z.object({
|
export const PublicUser = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
role: Role,
|
||||||
});
|
});
|
||||||
export type PublicUser = z.infer<typeof PublicUser>;
|
export type PublicUser = z.infer<typeof PublicUser>;
|
||||||
|
|
||||||
@@ -20,9 +24,6 @@ export type MeResponse = z.infer<typeof MeResponse>;
|
|||||||
export const InsoleType = z.enum(['Kurk', 'Berk', '3D']);
|
export const InsoleType = z.enum(['Kurk', 'Berk', '3D']);
|
||||||
export type InsoleType = z.infer<typeof InsoleType>;
|
export type InsoleType = z.infer<typeof InsoleType>;
|
||||||
|
|
||||||
export const Role = z.enum(['worker', 'admin']);
|
|
||||||
export type Role = z.infer<typeof Role>;
|
|
||||||
|
|
||||||
export const Activity = z.object({
|
export const Activity = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user