@brika/auth 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +207 -0
  2. package/package.json +50 -0
  3. package/src/__tests__/AuthClient.test.ts +736 -0
  4. package/src/__tests__/AuthService.test.ts +140 -0
  5. package/src/__tests__/ScopeService.test.ts +156 -0
  6. package/src/__tests__/SessionService.test.ts +311 -0
  7. package/src/__tests__/UserService-avatar.test.ts +277 -0
  8. package/src/__tests__/UserService.test.ts +223 -0
  9. package/src/__tests__/canAccess.test.ts +166 -0
  10. package/src/__tests__/disabledScopes.test.ts +101 -0
  11. package/src/__tests__/middleware.test.ts +190 -0
  12. package/src/__tests__/plugin.test.ts +78 -0
  13. package/src/__tests__/requireSession.test.ts +78 -0
  14. package/src/__tests__/routes-auth.test.ts +248 -0
  15. package/src/__tests__/routes-profile.test.ts +403 -0
  16. package/src/__tests__/routes-scopes.test.ts +64 -0
  17. package/src/__tests__/routes-sessions.test.ts +235 -0
  18. package/src/__tests__/routes-users.test.ts +477 -0
  19. package/src/__tests__/serveImage.test.ts +277 -0
  20. package/src/__tests__/setup.test.ts +270 -0
  21. package/src/__tests__/verifyToken.test.ts +219 -0
  22. package/src/client/AuthClient.ts +312 -0
  23. package/src/client/http-client.ts +84 -0
  24. package/src/client/index.ts +19 -0
  25. package/src/config.ts +82 -0
  26. package/src/constants.ts +10 -0
  27. package/src/index.ts +16 -0
  28. package/src/lib/define-roles.ts +35 -0
  29. package/src/lib/define-scopes.ts +48 -0
  30. package/src/middleware/canAccess.ts +126 -0
  31. package/src/middleware/index.ts +13 -0
  32. package/src/middleware/requireAuth.ts +35 -0
  33. package/src/middleware/requireScope.ts +46 -0
  34. package/src/middleware/verifyToken.ts +52 -0
  35. package/src/plugin.ts +86 -0
  36. package/src/react/AuthProvider.tsx +105 -0
  37. package/src/react/hooks.ts +128 -0
  38. package/src/react/index.ts +51 -0
  39. package/src/react/withScopeGuard.tsx +73 -0
  40. package/src/roles.ts +40 -0
  41. package/src/schemas.ts +112 -0
  42. package/src/scopes.ts +60 -0
  43. package/src/server/index.ts +44 -0
  44. package/src/server/requireSession.ts +44 -0
  45. package/src/server/routes/auth.ts +102 -0
  46. package/src/server/routes/cookie.ts +7 -0
  47. package/src/server/routes/index.ts +32 -0
  48. package/src/server/routes/profile.ts +162 -0
  49. package/src/server/routes/scopes.ts +22 -0
  50. package/src/server/routes/sessions.ts +68 -0
  51. package/src/server/routes/setup.ts +50 -0
  52. package/src/server/routes/users.ts +175 -0
  53. package/src/server/serveImage.ts +91 -0
  54. package/src/services/AuthService.ts +80 -0
  55. package/src/services/ScopeService.ts +94 -0
  56. package/src/services/SessionService.ts +245 -0
  57. package/src/services/UserService.ts +245 -0
  58. package/src/setup.ts +99 -0
  59. package/src/tanstack/index.ts +15 -0
  60. package/src/tanstack/routeBuilder.ts +311 -0
  61. package/src/types.ts +118 -0
  62. package/tsconfig.json +8 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @brika/auth/react - Hooks
3
+ *
4
+ * React hooks for authentication
5
+ */
6
+
7
+ import { useContext, useMemo } from 'react';
8
+ import { canAccess, canAccessAll, Features } from '../middleware/canAccess';
9
+ import { Scope } from '../types';
10
+ import { AuthContext, AuthContextValue } from './AuthProvider';
11
+
12
+ /**
13
+ * Use authentication context
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const { user, login, logout } = useAuth();
18
+ * ```
19
+ */
20
+ export function useAuth(): AuthContextValue {
21
+ const context = useContext(AuthContext);
22
+
23
+ if (!context) {
24
+ throw new Error('useAuth must be used within <AuthProvider>');
25
+ }
26
+
27
+ return context;
28
+ }
29
+
30
+ /**
31
+ * Check if user has access to a scope
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const canEdit = useCanAccess(Scope.WORKFLOW_WRITE);
36
+ * ```
37
+ */
38
+ export function useCanAccess(required: Scope | Scope[] | null): boolean {
39
+ const { session } = useAuth();
40
+
41
+ return useMemo(() => {
42
+ if (!required || !session) {
43
+ return false;
44
+ }
45
+ return canAccess((session.scopes || []) as Scope[], required);
46
+ }, [required, session?.scopes]);
47
+ }
48
+
49
+ /**
50
+ * Check if user has all required scopes
51
+ *
52
+ * @example
53
+ * ```tsx
54
+ * const canManage = useCanAccessAll([Scope.ADMIN_ALL]);
55
+ * ```
56
+ */
57
+ export function useCanAccessAll(required: Scope[] | null): boolean {
58
+ const { session } = useAuth();
59
+
60
+ return useMemo(() => {
61
+ if (!required || !session) {
62
+ return false;
63
+ }
64
+ return canAccessAll((session.scopes || []) as Scope[], required);
65
+ }, [required, session?.scopes]);
66
+ }
67
+
68
+ /**
69
+ * Get feature permissions
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * const perms = useFeaturePermissions(Features.Workflow);
74
+ * if (perms.execute) {
75
+ * // show execute button
76
+ * }
77
+ * ```
78
+ */
79
+ export function useFeaturePermissions<
80
+ T extends Record<string, boolean | ((scopes: Scope[]) => boolean)>,
81
+ >(featurePermissions: T): Record<keyof T, boolean> {
82
+ const { session } = useAuth();
83
+
84
+ return useMemo(() => {
85
+ const scopes = (session?.scopes ?? []) as Scope[];
86
+ return Object.fromEntries(
87
+ Object.entries(featurePermissions).map(([key, checker]) => {
88
+ if (!session) {
89
+ return [key, false];
90
+ }
91
+ const value = typeof checker === 'function' ? checker(scopes) : checker;
92
+ return [key, value];
93
+ })
94
+ ) as Record<keyof T, boolean>;
95
+ }, [session?.scopes]);
96
+ }
97
+
98
+ /**
99
+ * Check if user is loading auth state
100
+ */
101
+ export function useAuthLoading(): boolean {
102
+ const { isLoading } = useAuth();
103
+ return isLoading;
104
+ }
105
+
106
+ /**
107
+ * Get current user
108
+ */
109
+ export function useUser() {
110
+ const { user } = useAuth();
111
+ return user;
112
+ }
113
+
114
+ /**
115
+ * Get current session
116
+ */
117
+ export function useSession() {
118
+ const { session } = useAuth();
119
+ return session;
120
+ }
121
+
122
+ /**
123
+ * Get auth error
124
+ */
125
+ export function useAuthError() {
126
+ const { error } = useAuth();
127
+ return error;
128
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @brika/auth/react
3
+ *
4
+ * React hooks and components for authentication.
5
+ * Use this in your Brika UI React app.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { AuthProvider, useAuth } from '@brika/auth/react';
10
+ *
11
+ * function App() {
12
+ * return (
13
+ * // No apiUrl needed - uses window.location.origin
14
+ * <AuthProvider>
15
+ * <Dashboard />
16
+ * </AuthProvider>
17
+ * );
18
+ * }
19
+ *
20
+ * function Dashboard() {
21
+ * const { user, login, logout } = useAuth();
22
+ * const canEdit = useCanAccess(Scope.WORKFLOW_WRITE);
23
+ *
24
+ * return (
25
+ * <>
26
+ * <h1>Hello, {user?.name}!</h1>
27
+ * {canEdit && <EditButton />}
28
+ * <button onClick={logout}>Logout</button>
29
+ * </>
30
+ * );
31
+ * }
32
+ * ```
33
+ */
34
+
35
+ export {
36
+ AuthContext,
37
+ type AuthContextValue,
38
+ AuthProvider,
39
+ type AuthProviderProps,
40
+ } from './AuthProvider';
41
+ export {
42
+ useAuth,
43
+ useAuthError,
44
+ useAuthLoading,
45
+ useCanAccess,
46
+ useCanAccessAll,
47
+ useFeaturePermissions,
48
+ useSession,
49
+ useUser,
50
+ } from './hooks';
51
+ export { type WithScopeGuardOptions, withOptionalScope, withScopeGuard } from './withScopeGuard';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @brika/auth/react - withScopeGuard HOC
3
+ *
4
+ * Higher-order component to protect components with scope requirements.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { Scope } from '../types';
9
+ import { useCanAccess } from './hooks';
10
+
11
+ export interface WithScopeGuardOptions {
12
+ fallback?: React.ReactNode;
13
+ }
14
+
15
+ const DEFAULT_FALLBACK = (
16
+ <div>
17
+ <h1>Unauthorized</h1>
18
+ <p>You don't have permission to access this page.</p>
19
+ </div>
20
+ );
21
+
22
+ /**
23
+ * HOC to protect a component with scope requirement.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * const ProtectedEditor = withScopeGuard(
28
+ * WorkflowEditor,
29
+ * Scope.WORKFLOW_WRITE,
30
+ * { fallback: <UnauthorizedPage /> }
31
+ * );
32
+ * ```
33
+ */
34
+ export function withScopeGuard<P extends object>(
35
+ Component: React.ComponentType<P>,
36
+ requiredScopes: Scope | Scope[] | null,
37
+ options?: WithScopeGuardOptions
38
+ ): React.ComponentType<P> {
39
+ const displayName = `withScopeGuard(${Component.displayName || Component.name})`;
40
+
41
+ const Wrapper = (props: P) => {
42
+ const canAccess = useCanAccess(requiredScopes);
43
+
44
+ if (!canAccess) {
45
+ return options?.fallback !== undefined ? options.fallback : DEFAULT_FALLBACK;
46
+ }
47
+
48
+ return <Component {...props} />;
49
+ };
50
+
51
+ Wrapper.displayName = displayName;
52
+
53
+ return Wrapper;
54
+ }
55
+
56
+ /**
57
+ * HOC to hide component if user lacks scope
58
+ * (renders nothing instead of fallback)
59
+ *
60
+ * @example
61
+ * ```tsx
62
+ * const OptionalButton = withOptionalScope(AdminButton, Scope.ADMIN_ALL);
63
+ * // Returns null if user doesn't have admin scope
64
+ * ```
65
+ */
66
+ export function withOptionalScope<P extends object>(
67
+ Component: React.ComponentType<P>,
68
+ requiredScopes: Scope | Scope[] | null
69
+ ): React.ComponentType<P> {
70
+ return withScopeGuard(Component, requiredScopes, {
71
+ fallback: null,
72
+ });
73
+ }
package/src/roles.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @brika/auth - Roles
3
+ *
4
+ * User roles determine the default set of scopes granted to a user.
5
+ *
6
+ * To add a new role:
7
+ * 1. Add an entry below with its default scopes
8
+ */
9
+
10
+ import { defineRoles } from './lib/define-roles';
11
+ import { Scope } from './scopes';
12
+
13
+ export const { Role, ROLE_SCOPES } = defineRoles({
14
+ ADMIN: {
15
+ value: 'admin',
16
+ defaultScopes: [Scope.ADMIN_ALL],
17
+ },
18
+ USER: {
19
+ value: 'user',
20
+ defaultScopes: [
21
+ Scope.WORKFLOW_READ,
22
+ Scope.WORKFLOW_WRITE,
23
+ Scope.WORKFLOW_EXECUTE,
24
+ Scope.BOARD_READ,
25
+ Scope.BOARD_WRITE,
26
+ Scope.PLUGIN_READ,
27
+ Scope.SETTINGS_READ,
28
+ ],
29
+ },
30
+ GUEST: {
31
+ value: 'guest',
32
+ defaultScopes: [Scope.WORKFLOW_READ, Scope.BOARD_READ, Scope.PLUGIN_READ],
33
+ },
34
+ SERVICE: {
35
+ value: 'service',
36
+ defaultScopes: [],
37
+ },
38
+ });
39
+
40
+ export type Role = (typeof Role)[keyof typeof Role];
package/src/schemas.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @brika/auth - Zod Schemas for Validation
3
+ */
4
+
5
+ import { z } from 'zod';
6
+ import { getAuthConfig } from './config';
7
+ import { Role } from './roles';
8
+ import { Scope } from './scopes';
9
+
10
+ const roleValues = Object.values(Role) as [Role, ...Role[]];
11
+ const scopeValues = Object.values(Scope) as [Scope, ...Scope[]];
12
+
13
+ export const RoleSchema = z.enum(roleValues);
14
+
15
+ export const ScopeSchema = z.enum(scopeValues);
16
+
17
+ export const EmailSchema = z.string().email('Invalid email address').toLowerCase();
18
+
19
+ export const NameSchema = z.string().min(2, 'Name must be at least 2 characters').max(255);
20
+
21
+ /**
22
+ * Password schema — reads config lazily so it respects runtime overrides.
23
+ * Uses z.string() + superRefine so all rules are evaluated at validation time.
24
+ * Max 72 chars: bcrypt silently truncates beyond this, so enforce it explicitly.
25
+ */
26
+ export const PasswordSchema = z
27
+ .string()
28
+ .max(72, 'Max 72 characters')
29
+ .superRefine((v, ctx) => {
30
+ const { password } = getAuthConfig();
31
+
32
+ if (v.length < password.minLength) {
33
+ ctx.addIssue({
34
+ code: 'custom',
35
+ message: `Min ${password.minLength} characters`,
36
+ });
37
+ }
38
+ if (password.requireUppercase && !/[A-Z]/.test(v)) {
39
+ ctx.addIssue({
40
+ code: 'custom',
41
+ message: 'Need uppercase letter (A-Z)',
42
+ });
43
+ }
44
+ if (password.requireNumbers && !/\d/.test(v)) {
45
+ ctx.addIssue({
46
+ code: 'custom',
47
+ message: 'Need number (0-9)',
48
+ });
49
+ }
50
+ if (password.requireSpecial && !password.specialChars.test(v)) {
51
+ ctx.addIssue({
52
+ code: 'custom',
53
+ message: 'Need special character (!@#$%^&*...)',
54
+ });
55
+ }
56
+ });
57
+
58
+ export const UserSchema = z.object({
59
+ id: z.string().uuid(),
60
+ email: EmailSchema,
61
+ name: NameSchema,
62
+ role: RoleSchema,
63
+ createdAt: z.date(),
64
+ updatedAt: z.date(),
65
+ isActive: z.boolean().default(true),
66
+ });
67
+
68
+ export const SessionSchema = z.object({
69
+ id: z.string(),
70
+ userId: z.string(),
71
+ userEmail: z.string().email(),
72
+ userName: z.string(),
73
+ userRole: RoleSchema,
74
+ scopes: z.array(ScopeSchema),
75
+ });
76
+
77
+ export const LoginSchema = z.object({
78
+ email: EmailSchema,
79
+ password: z.string().min(1),
80
+ });
81
+
82
+ export const CreateUserSchema = z.object({
83
+ email: EmailSchema,
84
+ name: NameSchema,
85
+ role: RoleSchema.default(Role.USER),
86
+ password: PasswordSchema.optional(),
87
+ });
88
+
89
+ export const SetupSchema = z.object({
90
+ email: EmailSchema,
91
+ name: NameSchema,
92
+ password: PasswordSchema,
93
+ });
94
+
95
+ export const CreateApiTokenSchema = z.object({
96
+ name: z.string().min(1).max(255),
97
+ scopes: z.array(ScopeSchema),
98
+ expiresAt: z.date().optional(),
99
+ });
100
+
101
+ /**
102
+ * Validate a password against the policy.
103
+ * Returns the first error message, or undefined if valid.
104
+ * Compatible with @clack/prompts validate functions.
105
+ */
106
+ export function validatePassword(value: string): string | undefined {
107
+ const result = PasswordSchema.safeParse(value);
108
+ if (result.success) {
109
+ return undefined;
110
+ }
111
+ return result.error.issues[0]?.message;
112
+ }
package/src/scopes.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @brika/auth - Scopes
3
+ *
4
+ * Permission scopes control what actions a user can perform.
5
+ * Each user has an explicit allow-list of scopes stored in DB.
6
+ * New users receive ROLE_SCOPES[role] as defaults.
7
+ *
8
+ * To add a new scope:
9
+ * 1. Add an entry to the scopes object below
10
+ * 2. Optionally add it to ROLE_SCOPES in role-scopes.ts
11
+ */
12
+
13
+ import { defineScopes } from './lib/define-scopes';
14
+
15
+ export const { Scope, SCOPES_REGISTRY } = defineScopes({
16
+ scopes: {
17
+ ADMIN_ALL: {
18
+ value: 'admin:*',
19
+ description: 'Full administrative access',
20
+ },
21
+ WORKFLOW_READ: {
22
+ value: 'workflow:read',
23
+ description: 'Read workflows',
24
+ },
25
+ WORKFLOW_WRITE: {
26
+ value: 'workflow:write',
27
+ description: 'Create and edit workflows',
28
+ },
29
+ WORKFLOW_EXECUTE: {
30
+ value: 'workflow:execute',
31
+ description: 'Execute workflows',
32
+ },
33
+ BOARD_READ: {
34
+ value: 'board:read',
35
+ description: 'Read boards',
36
+ },
37
+ BOARD_WRITE: {
38
+ value: 'board:write',
39
+ description: 'Create and edit boards',
40
+ },
41
+ PLUGIN_READ: {
42
+ value: 'plugin:read',
43
+ description: 'List and read plugins',
44
+ },
45
+ PLUGIN_MANAGE: {
46
+ value: 'plugin:manage',
47
+ description: 'Install and uninstall plugins',
48
+ },
49
+ SETTINGS_READ: {
50
+ value: 'settings:read',
51
+ description: 'Read system settings',
52
+ },
53
+ SETTINGS_WRITE: {
54
+ value: 'settings:write',
55
+ description: 'Modify system settings',
56
+ },
57
+ },
58
+ });
59
+
60
+ export type Scope = (typeof Scope)[keyof typeof Scope];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @brika/auth/server
3
+ *
4
+ * Server-side authentication module.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { auth } from '@brika/auth/server';
9
+ *
10
+ * // Hub: with API server
11
+ * await bootstrap()
12
+ * .use(auth({ dataDir, server: inject(ApiServer) }))
13
+ * .start();
14
+ *
15
+ * // CLI: services + DB only
16
+ * await bootstrapCLI()
17
+ * .use(auth({ dataDir }))
18
+ * .start();
19
+ * ```
20
+ */
21
+
22
+ export {
23
+ canAccess,
24
+ canAccessAll,
25
+ createPermissionChecker,
26
+ Features,
27
+ } from '../middleware/canAccess';
28
+ // 🛡️ Middleware
29
+ export { type AuthContext, requireAuth } from '../middleware/requireAuth';
30
+ export { requireScope } from '../middleware/requireScope';
31
+ export { verifyToken } from '../middleware/verifyToken';
32
+ export { auth } from '../plugin';
33
+ // 🏗️ Services
34
+ export { AuthService } from '../services/AuthService';
35
+ export { ScopeService } from '../services/ScopeService';
36
+ export { SessionService } from '../services/SessionService';
37
+ export { UserService } from '../services/UserService';
38
+ // 🎯 Setup & Plugin
39
+ export { openAuthDatabase, setupAuthServices } from '../setup';
40
+ // 🛠️ Route Helpers
41
+ export { requireSession } from './requireSession';
42
+ // 🌐 Routes
43
+ export { allAuthRoutes as authRoutes } from './routes/index';
44
+ export { serveImage } from './serveImage';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @brika/auth/server - requireSession
3
+ *
4
+ * Extract authenticated session from route context.
5
+ * Throws 401 Unauthorized if no session.
6
+ * Optionally validates scope — throws 403 Forbidden if missing required scope.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // Just auth check
11
+ * const session = requireSession(ctx);
12
+ *
13
+ * // Auth + scope check (throws 403 if missing)
14
+ * const session = requireSession(ctx, Scope.ADMIN_ALL);
15
+ *
16
+ * // Auth + any-of scope check
17
+ * const session = requireSession(ctx, [Scope.WORKFLOW_READ, Scope.ADMIN_ALL]);
18
+ * ```
19
+ */
20
+
21
+ import { Forbidden, Unauthorized } from '@brika/router';
22
+ import { canAccess } from '../middleware/canAccess';
23
+ import type { Scope, Session } from '../types';
24
+
25
+ export function requireSession(
26
+ ctx: {
27
+ get(key: string): unknown;
28
+ },
29
+ scope?: Scope | Scope[]
30
+ ): Session {
31
+ const session = ctx.get('session') as Session | null;
32
+ if (!session) {
33
+ throw new Unauthorized();
34
+ }
35
+
36
+ if (scope !== undefined) {
37
+ if (!canAccess(session.scopes, scope)) {
38
+ const required = Array.isArray(scope) ? scope : [scope];
39
+ throw new Forbidden(`Insufficient permissions. Required: ${required.join(', ')}`);
40
+ }
41
+ }
42
+
43
+ return session;
44
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Auth routes — login, logout, session info
3
+ */
4
+
5
+ import { inject } from '@brika/di';
6
+ import { rateLimit, route } from '@brika/router';
7
+ import { LoginSchema } from '../../schemas';
8
+ import { AuthService } from '../../services/AuthService';
9
+ import { UserService } from '../../services/UserService';
10
+ import type { Session } from '../../types';
11
+ import { requireSession } from '../requireSession';
12
+ import { sessionCookie } from './cookie';
13
+
14
+ /** POST /login — Login with email and password */
15
+ const login = route.post({
16
+ path: '/login',
17
+ middleware: [
18
+ rateLimit({
19
+ window: 60,
20
+ max: 5,
21
+ }),
22
+ ],
23
+ body: LoginSchema,
24
+ handler: async (ctx) => {
25
+ const authService = inject(AuthService);
26
+ const { email, password } = ctx.body;
27
+ const ip =
28
+ ctx.req.headers.get('x-forwarded-for') ?? ctx.req.headers.get('x-real-ip') ?? undefined;
29
+ const userAgent = ctx.req.headers.get('user-agent') ?? undefined;
30
+
31
+ try {
32
+ const result = await authService.login(email, password, ip, userAgent);
33
+
34
+ return new Response(
35
+ JSON.stringify({
36
+ user: result.user,
37
+ }),
38
+ {
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ 'Set-Cookie': sessionCookie(result.token, result.expiresIn),
42
+ },
43
+ }
44
+ );
45
+ } catch {
46
+ return new Response(
47
+ JSON.stringify({
48
+ error: 'Invalid credentials',
49
+ }),
50
+ {
51
+ status: 401,
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ }
56
+ );
57
+ }
58
+ },
59
+ });
60
+
61
+ /** POST /logout — Logout and revoke current session */
62
+ const logout = route.post({
63
+ path: '/logout',
64
+ handler: (ctx) => {
65
+ const authService = inject(AuthService);
66
+ const session = ctx.get('session') as Session | null;
67
+
68
+ if (session) {
69
+ authService.logout(session.id);
70
+ }
71
+
72
+ return new Response(
73
+ JSON.stringify({
74
+ ok: true,
75
+ }),
76
+ {
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'Set-Cookie': sessionCookie('', 0),
80
+ },
81
+ }
82
+ );
83
+ },
84
+ });
85
+
86
+ /** GET /session — Get current session info */
87
+ const sessionInfo = route.get({
88
+ path: '/session',
89
+ handler: (ctx) => {
90
+ const session = requireSession(ctx);
91
+ const userService = inject(UserService);
92
+ const user = userService.getUser(session.userId);
93
+
94
+ return {
95
+ user,
96
+ scopes: session.scopes,
97
+ };
98
+ },
99
+ });
100
+
101
+ export const authPublicRoutes = [login];
102
+ export const authProtectedRoutes = [logout, sessionInfo];
@@ -0,0 +1,7 @@
1
+ import { getAuthConfig } from '../../config';
2
+
3
+ /** Build a Set-Cookie header value for the session token. */
4
+ export function sessionCookie(token: string, maxAge: number): string {
5
+ const name = getAuthConfig().session.cookieName;
6
+ return `${name}=${token}; HttpOnly; Secure; Path=/api; Max-Age=${maxAge}; SameSite=Lax`;
7
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @brika/auth/server - All Routes
3
+ *
4
+ * Combines auth, session, profile, user, and scope routes.
5
+ * Login and scopes are public; everything else requires authentication.
6
+ */
7
+
8
+ import { combineRoutes, group } from '@brika/router';
9
+ import { requireAuth } from '../../middleware/requireAuth';
10
+ import { authProtectedRoutes, authPublicRoutes } from './auth';
11
+ import { profileRoutes } from './profile';
12
+ import { scopeRoutes } from './scopes';
13
+ import { sessionRoutes } from './sessions';
14
+ import { setupRoutes } from './setup';
15
+ import { userRoutes } from './users';
16
+
17
+ export const allAuthRoutes = combineRoutes(
18
+ group({
19
+ prefix: '/api/auth',
20
+ routes: [authPublicRoutes, scopeRoutes],
21
+ }),
22
+ group({
23
+ prefix: '/api/auth/setup',
24
+ routes: [setupRoutes],
25
+ }),
26
+ group({
27
+ prefix: '/api/auth',
28
+ middleware: [requireAuth()],
29
+ routes: [authProtectedRoutes, sessionRoutes, profileRoutes],
30
+ }),
31
+ userRoutes
32
+ );