create-lego-one 2.0.12 → 2.0.14

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 (78) hide show
  1. package/dist/index.cjs +150 -15
  2. package/dist/index.cjs.map +1 -1
  3. package/package.json +1 -1
  4. package/template/.cursor/rules/rules.mdc +639 -0
  5. package/template/.dockerignore +58 -0
  6. package/template/.env.example +18 -0
  7. package/template/.eslintignore +5 -0
  8. package/template/.eslintrc.js +28 -0
  9. package/template/.prettierignore +6 -0
  10. package/template/.prettierrc +11 -0
  11. package/template/CLAUDE.md +634 -0
  12. package/template/Dockerfile +67 -0
  13. package/template/PROMPT.md +457 -0
  14. package/template/README.md +325 -0
  15. package/template/docker-compose.yml +48 -0
  16. package/template/docker-entrypoint.sh +23 -0
  17. package/template/docs/checkpoints/.template.md +64 -0
  18. package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
  19. package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
  20. package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
  21. package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
  22. package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
  23. package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
  24. package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
  25. package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
  26. package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
  27. package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
  28. package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
  29. package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
  30. package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
  31. package/template/docs/framework/plans/00-index.md +164 -0
  32. package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
  33. package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
  34. package/template/docs/framework/plans/03-host-kernel.md +1518 -0
  35. package/template/docs/framework/plans/04-auth-system.md +1466 -0
  36. package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
  37. package/template/docs/framework/plans/06-ui-components.md +1478 -0
  38. package/template/docs/framework/plans/07-communication-system.md +1106 -0
  39. package/template/docs/framework/plans/08-plugin-system.md +1179 -0
  40. package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
  41. package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
  42. package/template/docs/framework/plans/11-testing.md +935 -0
  43. package/template/docs/framework/plans/12-deployment.md +896 -0
  44. package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
  45. package/template/docs/framework/research/00-modernjs-audit.md +488 -0
  46. package/template/docs/framework/research/01-system-blueprint.md +721 -0
  47. package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
  48. package/template/docs/framework/research/03-host-setup.md +714 -0
  49. package/template/docs/framework/research/04-plugin-architecture.md +645 -0
  50. package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
  51. package/template/docs/framework/research/06-cli-strategy.md +615 -0
  52. package/template/docs/framework/research/07-deployment.md +629 -0
  53. package/template/docs/framework/research/README.md +282 -0
  54. package/template/docs/framework/setup/00-index.md +210 -0
  55. package/template/docs/framework/setup/01-framework-structure.md +308 -0
  56. package/template/docs/framework/setup/02-development-workflow.md +405 -0
  57. package/template/docs/framework/setup/03-environment-setup.md +215 -0
  58. package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
  59. package/template/docs/framework/setup/05-plugin-system.md +620 -0
  60. package/template/docs/framework/setup/06-communication-patterns.md +451 -0
  61. package/template/docs/framework/setup/07-plugin-development.md +582 -0
  62. package/template/docs/framework/setup/08-component-library.md +658 -0
  63. package/template/docs/framework/setup/09-data-integration.md +609 -0
  64. package/template/docs/framework/setup/10-auth-rbac.md +497 -0
  65. package/template/docs/framework/setup/11-hooks-api.md +393 -0
  66. package/template/docs/framework/setup/12-components-api.md +665 -0
  67. package/template/docs/framework/setup/13-deployment-guide.md +566 -0
  68. package/template/docs/framework/setup/README.md +548 -0
  69. package/template/host/package.json +1 -1
  70. package/template/nginx.conf +72 -0
  71. package/template/package.json +1 -1
  72. package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
  73. package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
  74. package/template/pocketbase/CHANGELOG.md +911 -0
  75. package/template/pocketbase/LICENSE.md +17 -0
  76. package/template/scripts/create-plugin.js +221 -0
  77. package/template/scripts/deploy.sh +56 -0
  78. package/template/tsconfig.base.json +26 -0
@@ -0,0 +1,1466 @@
1
+ # Authentication System Implementation Plan
2
+
3
+ > **For AI Implementing This Plan:** This is document 04 of 13. Complete documents 01-03 first.
4
+
5
+ **Goal:** Implement complete authentication system with login, logout, session management, and protected routes using PocketBase auth collection.
6
+
7
+ **Architecture:** Admin-seeded authentication system (no public signup). Admin manages users through settings. Session persistence via PocketBase auth store + localStorage. Protected routes using higher-order components.
8
+
9
+ **Tech Stack:** PocketBase auth, React hooks, Zustand state management, Zod validation, TanStack Query
10
+
11
+ ---
12
+
13
+ ## Prerequisites
14
+
15
+ - ✅ Completed `01-infrastructure-setup.md`
16
+ - ✅ Completed `02-pocketbase-setup.md` (users collection, seed data)
17
+ - ✅ Completed `03-host-kernel.md` (host app, shared state, providers)
18
+
19
+ ---
20
+
21
+ ## Task 1: Create Auth Types and Interfaces
22
+
23
+ **Files:**
24
+ - Create: `host/src/kernel/auth/types.ts`
25
+ - Create: `host/src/kernel/auth/schemas.ts`
26
+
27
+ ### Step 1: Create auth types
28
+
29
+ **File:** `host/src/kernel/auth/types.ts`
30
+
31
+ ```typescript
32
+ import type { RecordAuth } from 'pocketbase';
33
+
34
+ export interface LoginCredentials {
35
+ email: string;
36
+ password: string;
37
+ }
38
+
39
+ export interface RegisterData {
40
+ email: string;
41
+ password: string;
42
+ passwordConfirm: string;
43
+ name: string;
44
+ }
45
+
46
+ export interface User {
47
+ id: string;
48
+ email: string;
49
+ name: string;
50
+ avatar?: string;
51
+ role?: string;
52
+ organizationId?: string;
53
+ }
54
+
55
+ export interface AuthResponse {
56
+ token: string;
57
+ user: User;
58
+ }
59
+
60
+ export interface AuthError {
61
+ message: string;
62
+ code?: number;
63
+ }
64
+
65
+ export interface SessionData {
66
+ token: string;
67
+ user: User;
68
+ organizationId?: string;
69
+ }
70
+ ```
71
+
72
+ ### Step 2: Create Zod validation schemas
73
+
74
+ **File:** `host/src/kernel/auth/schemas.ts`
75
+
76
+ ```typescript
77
+ import { z } from 'zod';
78
+
79
+ export const loginSchema = z.object({
80
+ email: z.string().email('Invalid email address'),
81
+ password: z.string().min(8, 'Password must be at least 8 characters'),
82
+ });
83
+
84
+ export type LoginFormData = z.infer<typeof loginSchema>;
85
+
86
+ export const registerSchema = z
87
+ .object({
88
+ email: z.string().email('Invalid email address'),
89
+ password: z
90
+ .string()
91
+ .min(8, 'Password must be at least 8 characters')
92
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
93
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
94
+ .regex(/[0-9]/, 'Password must contain at least one number'),
95
+ passwordConfirm: z.string(),
96
+ name: z.string().min(2, 'Name must be at least 2 characters'),
97
+ })
98
+ .refine((data) => data.password === data.passwordConfirm, {
99
+ message: 'Passwords do not match',
100
+ path: ['passwordConfirm'],
101
+ });
102
+
103
+ export type RegisterFormData = z.infer<typeof registerSchema>;
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Task 2: Create Auth Service
109
+
110
+ **Files:**
111
+ - Create: `host/src/kernel/auth/service.ts`
112
+
113
+ ### Step 1: Create auth service
114
+
115
+ **File:** `host/src/kernel/auth/service.ts`
116
+
117
+ ```typescript
118
+ import PocketBase from 'pocketbase';
119
+ import type { LoginCredentials, RegisterData, User, AuthResponse, AuthError } from './types';
120
+ import type { Record } from 'pocketbase';
121
+
122
+ // Map PocketBase auth record to our User type
123
+ function mapAuthRecordToUser(record: Record): User {
124
+ return {
125
+ id: record.id,
126
+ email: record.email,
127
+ name: record.name || record.email,
128
+ avatar: record.avatar,
129
+ role: record.role,
130
+ organizationId: record.organizationId,
131
+ };
132
+ }
133
+
134
+ export class AuthService {
135
+ constructor(private pb: PocketBase) {}
136
+
137
+ /**
138
+ * Login with email and password
139
+ */
140
+ async login(credentials: LoginCredentials): Promise<AuthResponse> {
141
+ try {
142
+ const authData = await this.pb.collection('users').authWithPassword(
143
+ credentials.email,
144
+ credentials.password
145
+ );
146
+
147
+ return {
148
+ token: authData.token,
149
+ user: mapAuthRecordToUser(authData.record),
150
+ };
151
+ } catch (error: any) {
152
+ throw this.handleError(error);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Register new user (admin only in our system)
158
+ */
159
+ async register(data: RegisterData, organizationId?: string): Promise<AuthResponse> {
160
+ try {
161
+ const record = await this.pb.collection('users').create({
162
+ email: data.email,
163
+ password: data.password,
164
+ passwordConfirm: data.passwordConfirm,
165
+ name: data.name,
166
+ organizationId,
167
+ role: 'member', // Default role
168
+ });
169
+
170
+ // Auto-login after registration
171
+ const authData = await this.pb.collection('users').authWithPassword(
172
+ data.email,
173
+ data.password
174
+ );
175
+
176
+ return {
177
+ token: authData.token,
178
+ user: mapAuthRecordToUser(authData.record),
179
+ };
180
+ } catch (error: any) {
181
+ throw this.handleError(error);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Logout current user
187
+ */
188
+ async logout(): Promise<void> {
189
+ this.pb.authStore.clear();
190
+ localStorage.removeItem('pocketbase_auth');
191
+ }
192
+
193
+ /**
194
+ * Get current authenticated user
195
+ */
196
+ getCurrentUser(): User | null {
197
+ if (!this.pb.authStore.isValid) return null;
198
+
199
+ const model = this.pb.authStore.model;
200
+ if (!model) return null;
201
+
202
+ return mapAuthRecordToUser(model);
203
+ }
204
+
205
+ /**
206
+ * Get current auth token
207
+ */
208
+ getToken(): string | null {
209
+ return this.pb.authStore.token;
210
+ }
211
+
212
+ /**
213
+ * Refresh auth token
214
+ */
215
+ async refreshToken(): Promise<string> {
216
+ try {
217
+ // PocketBase handles token refresh automatically
218
+ // This is a no-op but kept for interface consistency
219
+ return this.pb.authStore.token;
220
+ } catch (error: any) {
221
+ throw this.handleError(error);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Update user profile
227
+ */
228
+ async updateProfile(userId: string, data: Partial<User>): Promise<User> {
229
+ try {
230
+ const record = await this.pb.collection('users').update(userId, {
231
+ name: data.name,
232
+ avatar: data.avatar,
233
+ });
234
+
235
+ return mapAuthRecordToUser(record);
236
+ } catch (error: any) {
237
+ throw this.handleError(error);
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Change password
243
+ */
244
+ async changePassword(oldPassword: string, newPassword: string): Promise<void> {
245
+ try {
246
+ await this.pb.collection('users').update(this.pb.authStore.record?.id, {
247
+ oldPassword,
248
+ password: newPassword,
249
+ passwordConfirm: newPassword,
250
+ });
251
+ } catch (error: any) {
252
+ throw this.handleError(error);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Request password reset
258
+ */
259
+ async requestPasswordReset(email: string): Promise<void> {
260
+ try {
261
+ await this.pb.collection('users').requestPasswordReset(email);
262
+ } catch (error: any) {
263
+ throw this.handleError(error);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Confirm password reset
269
+ */
270
+ async confirmPasswordReset(token: string, password: string): Promise<void> {
271
+ try {
272
+ await this.pb.collection('users').confirmPasswordReset(token, password, password);
273
+ } catch (error: any) {
274
+ throw this.handleError(error);
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Handle PocketBase errors
280
+ */
281
+ private handleError(error: any): AuthError {
282
+ // PocketBase error format
283
+ if (error?.data?.message) {
284
+ return {
285
+ message: error.data.message,
286
+ code: error.status,
287
+ };
288
+ }
289
+
290
+ if (error?.message) {
291
+ return {
292
+ message: error.message,
293
+ };
294
+ }
295
+
296
+ return {
297
+ message: 'An unexpected error occurred',
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Check if user is authenticated
303
+ */
304
+ isAuthenticated(): boolean {
305
+ return this.pb.authStore.isValid;
306
+ }
307
+
308
+ /**
309
+ * Listen to auth state changes
310
+ */
311
+ onAuthChange(callback: (token: string, record: Record | null) => void): () => void {
312
+ return this.pb.authStore.onChange(callback);
313
+ }
314
+ }
315
+ ```
316
+
317
+ ---
318
+
319
+ ## Task 3: Create Auth Hooks
320
+
321
+ **Files:**
322
+ - Create: `host/src/kernel/auth/hooks.ts`
323
+
324
+ ### Step 1: Create auth hooks
325
+
326
+ **File:** `host/src/kernel/auth/hooks.ts`
327
+
328
+ ```typescript
329
+ import { useCallback, useEffect, useState } from 'react';
330
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
331
+ import { usePocketBase } from '../providers';
332
+ import { AuthService } from './service';
333
+ import type { LoginCredentials, RegisterData, User } from './types';
334
+ import { useGlobalKernelState } from '../shared-state';
335
+
336
+ /**
337
+ * Get auth service instance
338
+ */
339
+ function useAuthService(): AuthService | null {
340
+ const pb = usePocketBase();
341
+ if (!pb) return null;
342
+ return new AuthService(pb);
343
+ }
344
+
345
+ /**
346
+ * Main auth hook - provides auth state and actions
347
+ */
348
+ export function useAuth() {
349
+ const pb = usePocketBase();
350
+ const queryClient = useQueryClient();
351
+ const { setUser, setToken, clearAuth, setIsLoading } = useGlobalKernelState();
352
+ const [authService, setAuthService] = useState<AuthService | null>(null);
353
+
354
+ // Initialize auth service
355
+ useEffect(() => {
356
+ if (pb) {
357
+ setAuthService(new AuthService(pb));
358
+
359
+ // Restore session from PocketBase auth store
360
+ if (pb.authStore.isValid && pb.authStore.model) {
361
+ setUser({
362
+ id: pb.authStore.model.id,
363
+ email: pb.authStore.model.email,
364
+ name: pb.authStore.model.name || pb.authStore.model.email,
365
+ avatar: pb.authStore.model.avatar,
366
+ role: pb.authStore.model.role,
367
+ organizationId: pb.authStore.model.organizationId,
368
+ });
369
+ setToken(pb.authStore.token);
370
+ }
371
+
372
+ setIsLoading(false);
373
+ }
374
+ }, [pb, setUser, setToken, setIsLoading]);
375
+
376
+ // Listen to auth state changes
377
+ useEffect(() => {
378
+ if (!pb) return;
379
+
380
+ const unsubscribe = pb.authStore.onChange((token, model) => {
381
+ if (model && token) {
382
+ setUser({
383
+ id: model.id,
384
+ email: model.email,
385
+ name: model.name || model.email,
386
+ avatar: model.avatar,
387
+ role: model.role,
388
+ organizationId: model.organizationId,
389
+ });
390
+ setToken(token);
391
+ } else {
392
+ clearAuth();
393
+ }
394
+ });
395
+
396
+ return unsubscribe;
397
+ }, [pb, setUser, setToken, clearAuth]);
398
+
399
+ const loginMutation = useMutation({
400
+ mutationFn: async (credentials: LoginCredentials) => {
401
+ if (!authService) throw new Error('Auth service not initialized');
402
+ return authService.login(credentials);
403
+ },
404
+ onSuccess: (data) => {
405
+ setUser(data.user);
406
+ setToken(data.token);
407
+ queryClient.invalidateQueries({ queryKey: ['auth', 'user'] });
408
+ },
409
+ });
410
+
411
+ const registerMutation = useMutation({
412
+ mutationFn: async (data: { registerData: RegisterData; organizationId?: string }) => {
413
+ if (!authService) throw new Error('Auth service not initialized');
414
+ return authService.register(data.registerData, data.organizationId);
415
+ },
416
+ onSuccess: (data) => {
417
+ setUser(data.user);
418
+ setToken(data.token);
419
+ queryClient.invalidateQueries({ queryKey: ['auth', 'user'] });
420
+ },
421
+ });
422
+
423
+ const logoutMutation = useMutation({
424
+ mutationFn: async () => {
425
+ if (!authService) throw new Error('Auth service not initialized');
426
+ return authService.logout();
427
+ },
428
+ onSuccess: () => {
429
+ clearAuth();
430
+ queryClient.clear();
431
+ },
432
+ });
433
+
434
+ const login = useCallback(
435
+ (credentials: LoginCredentials) => {
436
+ return loginMutation.mutateAsync(credentials);
437
+ },
438
+ [loginMutation]
439
+ );
440
+
441
+ const register = useCallback(
442
+ (registerData: RegisterData, organizationId?: string) => {
443
+ return registerMutation.mutateAsync({ registerData: registerData, organizationId });
444
+ },
445
+ [registerMutation]
446
+ );
447
+
448
+ const logout = useCallback(() => {
449
+ return logoutMutation.mutateAsync();
450
+ }, [logoutMutation]);
451
+
452
+ return {
453
+ user: authService?.getCurrentUser() || null,
454
+ isAuthenticated: !!pb?.authStore.isValid,
455
+ isLoading: loginMutation.isPending || registerMutation.isPending || logoutMutation.isPending,
456
+ login,
457
+ register,
458
+ logout,
459
+ error: loginMutation.error || registerMutation.error || logoutMutation.error,
460
+ };
461
+ }
462
+
463
+ /**
464
+ * Get current user data
465
+ */
466
+ export function useCurrentUser() {
467
+ const { user, isAuthenticated } = useAuth();
468
+
469
+ const { data: currentUser, isLoading } = useQuery({
470
+ queryKey: ['auth', 'user'],
471
+ queryFn: () => Promise.resolve(user),
472
+ enabled: isAuthenticated,
473
+ staleTime: Infinity,
474
+ });
475
+
476
+ return {
477
+ user: currentUser,
478
+ isLoading,
479
+ isAuthenticated,
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Require authentication - redirect if not logged in
485
+ */
486
+ export function useRequireAuth() {
487
+ const { isAuthenticated, isLoading, user } = useAuth();
488
+ const [shouldRedirect, setShouldRedirect] = useState(false);
489
+
490
+ useEffect(() => {
491
+ if (!isLoading && !isAuthenticated) {
492
+ setShouldRedirect(true);
493
+ }
494
+ }, [isLoading, isAuthenticated]);
495
+
496
+ return {
497
+ user,
498
+ isAuthenticated,
499
+ isLoading,
500
+ shouldRedirect,
501
+ };
502
+ }
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Task 4: Create Protected Route Component
508
+
509
+ **Files:**
510
+ - Create: `host/src/kernel/auth/ProtectedRoute.tsx`
511
+
512
+ ### Step 1: Create protected route wrapper
513
+
514
+ **File:** `host/src/kernel/auth/ProtectedRoute.tsx`
515
+
516
+ ```typescript
517
+ import { useEffect } from 'react';
518
+ import { useNavigate } from '@modern-js/runtime/router';
519
+ import { useRequireAuth } from './hooks';
520
+ import { Skeleton } from '../components/ui/skeleton';
521
+
522
+ interface ProtectedRouteProps {
523
+ children: React.ReactNode;
524
+ redirectTo?: string;
525
+ }
526
+
527
+ export function ProtectedRoute({
528
+ children,
529
+ redirectTo = '/login',
530
+ }: ProtectedRouteProps) {
531
+ const { isAuthenticated, isLoading, shouldRedirect } = useRequireAuth();
532
+ const navigate = useNavigate();
533
+
534
+ useEffect(() => {
535
+ if (shouldRedirect) {
536
+ navigate(redirectTo);
537
+ }
538
+ }, [shouldRedirect, navigate, redirectTo]);
539
+
540
+ if (isLoading) {
541
+ return (
542
+ <div className="flex min-h-[400px] items-center justify-center">
543
+ <div className="space-y-4 text-center">
544
+ <Skeleton className="mx-auto h-12 w-12 rounded-full" />
545
+ <Skeleton className="mx-auto h-4 w-48" />
546
+ <Skeleton className="mx-auto h-4 w-32" />
547
+ </div>
548
+ </div>
549
+ );
550
+ }
551
+
552
+ if (!isAuthenticated) {
553
+ return null; // Will redirect
554
+ }
555
+
556
+ return <>{children}</>;
557
+ }
558
+ ```
559
+
560
+ ---
561
+
562
+ ## Task 5: Create Login Page
563
+
564
+ **Files:**
565
+ - Create: `host/src/routes/login.tsx`
566
+ - Create: `host/src/kernel/auth/components/LoginForm.tsx`
567
+
568
+ ### Step 1: Create login form component
569
+
570
+ **File:** `host/src/kernel/auth/components/LoginForm.tsx`
571
+
572
+ ```typescript
573
+ import { useState } from 'react';
574
+ import { useNavigate } from '@modern-js/runtime/router';
575
+ import { useAuth } from '../hooks';
576
+ import { loginSchema, type LoginFormData } from '../schemas';
577
+ import { zodResolver } from '@hookform/resolvers/zod';
578
+ import { useForm } from 'react-hook-form';
579
+ import { Button } from '../../components/ui/button';
580
+ import { Input } from '../../components/ui/input';
581
+ import { Label } from '../../components/ui/label';
582
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../../components/ui/card';
583
+ import { Alert, AlertDescription } from '../../components/ui/alert';
584
+ import { Loader2 } from 'lucide-react';
585
+
586
+ export function LoginForm() {
587
+ const navigate = useNavigate();
588
+ const { login, isLoading, error } = useAuth();
589
+ const [submitError, setSubmitError] = useState<string | null>(null);
590
+
591
+ const {
592
+ register,
593
+ handleSubmit,
594
+ formState: { errors },
595
+ } = useForm<LoginFormData>({
596
+ resolver: zodResolver(loginSchema),
597
+ });
598
+
599
+ const onSubmit = async (data: LoginFormData) => {
600
+ setSubmitError(null);
601
+ try {
602
+ await login(data);
603
+ navigate('/dashboard');
604
+ } catch (err: any) {
605
+ setSubmitError(err.message || 'Login failed. Please try again.');
606
+ }
607
+ };
608
+
609
+ return (
610
+ <Card className="w-full max-w-md">
611
+ <CardHeader>
612
+ <CardTitle className="text-2xl">Sign In</CardTitle>
613
+ <CardDescription>
614
+ Enter your credentials to access your account
615
+ </CardDescription>
616
+ </CardHeader>
617
+ <form onSubmit={handleSubmit(onSubmit)}>
618
+ <CardContent className="space-y-4">
619
+ {(submitError || error) && (
620
+ <Alert variant="destructive">
621
+ <AlertDescription>
622
+ {submitError || (error as any)?.message}
623
+ </AlertDescription>
624
+ </Alert>
625
+ )}
626
+
627
+ <div className="space-y-2">
628
+ <Label htmlFor="email">Email</Label>
629
+ <Input
630
+ id="email"
631
+ type="email"
632
+ placeholder="admin@example.com"
633
+ {...register('email')}
634
+ />
635
+ {errors.email && (
636
+ <p className="text-sm text-destructive">{errors.email.message}</p>
637
+ )}
638
+ </div>
639
+
640
+ <div className="space-y-2">
641
+ <Label htmlFor="password">Password</Label>
642
+ <Input
643
+ id="password"
644
+ type="password"
645
+ placeholder="••••••••"
646
+ {...register('password')}
647
+ />
648
+ {errors.password && (
649
+ <p className="text-sm text-destructive">{errors.password.message}</p>
650
+ )}
651
+ </div>
652
+ </CardContent>
653
+
654
+ <CardFooter>
655
+ <Button type="submit" className="w-full" disabled={isLoading}>
656
+ {isLoading ? (
657
+ <>
658
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
659
+ Signing in...
660
+ </>
661
+ ) : (
662
+ 'Sign In'
663
+ )}
664
+ </Button>
665
+ </CardFooter>
666
+ </form>
667
+ </Card>
668
+ );
669
+ }
670
+ ```
671
+
672
+ ### Step 2: Create login page route
673
+
674
+ **File:** `host/src/routes/login.tsx`
675
+
676
+ ```typescript
677
+ import { Link } from '@modern-js/runtime/router';
678
+ import { LoginForm } from '../kernel/auth/components/LoginForm';
679
+ import { useAuth } from '../kernel/auth/hooks';
680
+ import { Navigate } from '@modern-js/runtime/router';
681
+
682
+ export default function LoginPage() {
683
+ const { isAuthenticated } = useAuth();
684
+
685
+ if (isAuthenticated) {
686
+ return <Navigate to="/dashboard" replace />;
687
+ }
688
+
689
+ return (
690
+ <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center p-6">
691
+ <div className="w-full max-w-md space-y-6">
692
+ {/* Logo and branding */}
693
+ <div className="text-center">
694
+ <div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-primary text-primary-foreground text-xl font-bold">
695
+ L
696
+ </div>
697
+ <h1 className="mt-4 text-2xl font-bold">Lego-One</h1>
698
+ <p className="mt-2 text-sm text-muted-foreground">
699
+ Sign in to access your account
700
+ </p>
701
+ </div>
702
+
703
+ {/* Login form */}
704
+ <LoginForm />
705
+
706
+ {/* Help text */}
707
+ <div className="text-center text-sm text-muted-foreground">
708
+ <p>Default credentials: admin@example.com / admin123</p>
709
+ </div>
710
+ </div>
711
+ </div>
712
+ );
713
+ }
714
+ ```
715
+
716
+ ---
717
+
718
+ ## Task 6: Create Shadcn UI Components (Auth-Needed)
719
+
720
+ **Files:**
721
+ - Create: `host/src/kernel/components/ui/button.tsx`
722
+ - Create: `host/src/kernel/components/ui/input.tsx`
723
+ - Create: `host/src/kernel/components/ui/label.tsx`
724
+ - Create: `host/src/kernel/components/ui/card.tsx`
725
+ - Create: `host/src/kernel/components/ui/alert.tsx`
726
+
727
+ ### Step 1: Create button component
728
+
729
+ **File:** `host/src/kernel/components/ui/button.tsx`
730
+
731
+ ```typescript
732
+ import * as React from 'react';
733
+ import { cva, type VariantProps } from 'class-variance-authority';
734
+ import { cn } from '../../../lib/utils';
735
+
736
+ const buttonVariants = cva(
737
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
738
+ {
739
+ variants: {
740
+ variant: {
741
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
742
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
743
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
744
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
745
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
746
+ link: 'text-primary underline-offset-4 hover:underline',
747
+ },
748
+ size: {
749
+ default: 'h-10 px-4 py-2',
750
+ sm: 'h-9 rounded-md px-3',
751
+ lg: 'h-11 rounded-md px-8',
752
+ icon: 'h-10 w-10',
753
+ },
754
+ },
755
+ defaultVariants: {
756
+ variant: 'default',
757
+ size: 'default',
758
+ },
759
+ }
760
+ );
761
+
762
+ export interface ButtonProps
763
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
764
+ VariantProps<typeof buttonVariants> {
765
+ asChild?: boolean;
766
+ }
767
+
768
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
769
+ ({ className, variant, size, ...props }, ref) => {
770
+ return (
771
+ <button
772
+ className={cn(buttonVariants({ variant, size, className }))}
773
+ ref={ref}
774
+ {...props}
775
+ />
776
+ );
777
+ }
778
+ );
779
+ Button.displayName = 'Button';
780
+
781
+ export { Button, buttonVariants };
782
+ ```
783
+
784
+ ### Step 2: Create input component
785
+
786
+ **File:** `host/src/kernel/components/ui/input.tsx`
787
+
788
+ ```typescript
789
+ import * as React from 'react';
790
+ import { cn } from '../../../lib/utils';
791
+
792
+ export interface InputProps
793
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
794
+
795
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
796
+ ({ className, type, ...props }, ref) => {
797
+ return (
798
+ <input
799
+ type={type}
800
+ className={cn(
801
+ 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
802
+ className
803
+ )}
804
+ ref={ref}
805
+ {...props}
806
+ />
807
+ );
808
+ }
809
+ );
810
+ Input.displayName = 'Input';
811
+
812
+ export { Input };
813
+ ```
814
+
815
+ ### Step 3: Create label component
816
+
817
+ **File:** `host/src/kernel/components/ui/label.tsx`
818
+
819
+ ```typescript
820
+ import * as React from 'react';
821
+ import { cn } from '../../../lib/utils';
822
+
823
+ export interface LabelProps
824
+ extends React.LabelHTMLAttributes<HTMLLabelElement> {}
825
+
826
+ const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
827
+ ({ className, ...props }, ref) => (
828
+ <label
829
+ ref={ref}
830
+ className={cn(
831
+ 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
832
+ className
833
+ )}
834
+ {...props}
835
+ />
836
+ )
837
+ );
838
+ Label.displayName = 'Label';
839
+
840
+ export { Label };
841
+ ```
842
+
843
+ ### Step 4: Create card components
844
+
845
+ **File:** `host/src/kernel/components/ui/card.tsx`
846
+
847
+ ```typescript
848
+ import * as React from 'react';
849
+ import { cn } from '../../../lib/utils';
850
+
851
+ const Card = React.forwardRef<
852
+ HTMLDivElement,
853
+ React.HTMLAttributes<HTMLDivElement>
854
+ >(({ className, ...props }, ref) => (
855
+ <div
856
+ ref={ref}
857
+ className={cn(
858
+ 'rounded-lg border bg-card text-card-foreground shadow-sm',
859
+ className
860
+ )}
861
+ {...props}
862
+ />
863
+ ));
864
+ Card.displayName = 'Card';
865
+
866
+ const CardHeader = React.forwardRef<
867
+ HTMLDivElement,
868
+ React.HTMLAttributes<HTMLDivElement>
869
+ >(({ className, ...props }, ref) => (
870
+ <div
871
+ ref={ref}
872
+ className={cn('flex flex-col space-y-1.5 p-6', className)}
873
+ {...props}
874
+ />
875
+ ));
876
+ CardHeader.displayName = 'CardHeader';
877
+
878
+ const CardTitle = React.forwardRef<
879
+ HTMLParagraphElement,
880
+ React.HTMLAttributes<HTMLHeadingElement>
881
+ >(({ className, ...props }, ref) => (
882
+ <h3
883
+ ref={ref}
884
+ className={cn(
885
+ 'text-2xl font-semibold leading-none tracking-tight',
886
+ className
887
+ )}
888
+ {...props}
889
+ />
890
+ ));
891
+ CardTitle.displayName = 'CardTitle';
892
+
893
+ const CardDescription = React.forwardRef<
894
+ HTMLParagraphElement,
895
+ React.HTMLAttributes<HTMLParagraphElement>
896
+ >(({ className, ...props }, ref) => (
897
+ <p
898
+ ref={ref}
899
+ className={cn('text-sm text-muted-foreground', className)}
900
+ {...props}
901
+ />
902
+ ));
903
+ CardDescription.displayName = 'CardDescription';
904
+
905
+ const CardContent = React.forwardRef<
906
+ HTMLDivElement,
907
+ React.HTMLAttributes<HTMLDivElement>
908
+ >(({ className, ...props }, ref) => (
909
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
910
+ ));
911
+ CardContent.displayName = 'CardContent';
912
+
913
+ const CardFooter = React.forwardRef<
914
+ HTMLDivElement,
915
+ React.HTMLAttributes<HTMLDivElement>
916
+ >(({ className, ...props }, ref) => (
917
+ <div
918
+ ref={ref}
919
+ className={cn('flex items-center p-6 pt-0', className)}
920
+ {...props}
921
+ />
922
+ ));
923
+ CardFooter.displayName = 'CardFooter';
924
+
925
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
926
+ ```
927
+
928
+ ### Step 5: Create alert component
929
+
930
+ **File:** `host/src/kernel/components/ui/alert.tsx`
931
+
932
+ ```typescript
933
+ import * as React from 'react';
934
+ import { cva, type VariantProps } from 'class-variance-authority';
935
+ import { cn } from '../../../lib/utils';
936
+
937
+ const alertVariants = cva(
938
+ 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
939
+ {
940
+ variants: {
941
+ variant: {
942
+ default: 'bg-background text-foreground',
943
+ destructive:
944
+ 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
945
+ },
946
+ },
947
+ defaultVariants: {
948
+ variant: 'default',
949
+ },
950
+ }
951
+ );
952
+
953
+ const Alert = React.forwardRef<
954
+ HTMLDivElement,
955
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
956
+ >(({ className, variant, ...props }, ref) => (
957
+ <div
958
+ ref={ref}
959
+ role="alert"
960
+ className={cn(alertVariants({ variant }), className)}
961
+ {...props}
962
+ />
963
+ ));
964
+ Alert.displayName = 'Alert';
965
+
966
+ const AlertTitle = React.forwardRef<
967
+ HTMLParagraphElement,
968
+ React.HTMLAttributes<HTMLHeadingElement>
969
+ >(({ className, ...props }, ref) => (
970
+ <h5
971
+ ref={ref}
972
+ className={cn('mb-1 font-medium leading-none tracking-tight', className)}
973
+ {...props}
974
+ />
975
+ ));
976
+ AlertTitle.displayName = 'AlertTitle';
977
+
978
+ const AlertDescription = React.forwardRef<
979
+ HTMLParagraphElement,
980
+ React.HTMLAttributes<HTMLParagraphElement>
981
+ >(({ className, ...props }, ref) => (
982
+ <div
983
+ ref={ref}
984
+ className={cn('text-sm [&_p]:leading-relaxed', className)}
985
+ {...props}
986
+ />
987
+ ));
988
+ AlertDescription.displayName = 'AlertDescription';
989
+
990
+ export { Alert, AlertTitle, AlertDescription };
991
+ ```
992
+
993
+ ---
994
+
995
+ ## Task 7: Update Dependencies and Install react-hook-form
996
+
997
+ **Files:**
998
+ - Modify: `host/package.json`
999
+
1000
+ ### Step 1: Add missing dependencies
1001
+
1002
+ **File:** `host/package.json`
1003
+
1004
+ Add to dependencies:
1005
+ ```json
1006
+ {
1007
+ "dependencies": {
1008
+ "@hookform/resolvers": "^3.9.0",
1009
+ "react-hook-form": "^7.53.0"
1010
+ }
1011
+ }
1012
+ ```
1013
+
1014
+ ### Step 2: Install new dependencies
1015
+
1016
+ **Run:** From root directory
1017
+
1018
+ ```bash
1019
+ pnpm install
1020
+ ```
1021
+
1022
+ ---
1023
+
1024
+ ## Task 8: Create Logout Functionality
1025
+
1026
+ **Files:**
1027
+ - Create: `host/src/kernel/auth/components/LogoutButton.tsx`
1028
+ - Modify: `host/src/layout/Topbar.tsx`
1029
+
1030
+ ### Step 1: Create logout button component
1031
+
1032
+ **File:** `host/src/kernel/auth/components/LogoutButton.tsx`
1033
+
1034
+ ```typescript
1035
+ import { useState } from 'react';
1036
+ import { useNavigate } from '@modern-js/runtime/router';
1037
+ import { useAuth } from '../hooks';
1038
+ import { Button } from '../../components/ui/button';
1039
+ import {
1040
+ DropdownMenu,
1041
+ DropdownMenuContent,
1042
+ DropdownMenuItem,
1043
+ DropdownMenuLabel,
1044
+ DropdownMenuSeparator,
1045
+ DropdownMenuTrigger,
1046
+ } from '../../components/ui/dropdown-menu';
1047
+ import { Loader2, LogOut, Settings, User } from 'lucide-react';
1048
+ import { getInitials } from '../../lib/utils';
1049
+ import { useGlobalKernelState } from '../../shared-state';
1050
+
1051
+ export function LogoutButton() {
1052
+ const { logout, isLoading } = useAuth();
1053
+ const navigate = useNavigate();
1054
+ const { user } = useGlobalKernelState();
1055
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
1056
+
1057
+ const handleLogout = async () => {
1058
+ setIsLoggingOut(true);
1059
+ try {
1060
+ await logout();
1061
+ navigate('/login');
1062
+ } finally {
1063
+ setIsLoggingOut(false);
1064
+ }
1065
+ };
1066
+
1067
+ return (
1068
+ <DropdownMenu>
1069
+ <DropdownMenuTrigger asChild>
1070
+ <Button variant="ghost" className="relative h-9 w-9 rounded-full">
1071
+ {user?.name ? (
1072
+ <span className="flex h-full w-full items-center justify-center bg-primary text-primary-foreground text-sm font-medium">
1073
+ {getInitials(user.name)}
1074
+ </span>
1075
+ ) : (
1076
+ <User className="h-5 w-5" />
1077
+ )}
1078
+ </Button>
1079
+ </DropdownMenuTrigger>
1080
+ <DropdownMenuContent className="w-56" align="end" forceMount>
1081
+ <DropdownMenuLabel className="font-normal">
1082
+ <div className="flex flex-col space-y-1">
1083
+ <p className="text-sm font-medium leading-none">{user?.name}</p>
1084
+ <p className="text-xs leading-none text-muted-foreground">
1085
+ {user?.email}
1086
+ </p>
1087
+ </div>
1088
+ </DropdownMenuLabel>
1089
+ <DropdownMenuSeparator />
1090
+ <DropdownMenuItem onClick={() => navigate('/settings/profile')}>
1091
+ <User className="mr-2 h-4 w-4" />
1092
+ <span>Profile</span>
1093
+ </DropdownMenuItem>
1094
+ <DropdownMenuItem onClick={() => navigate('/settings')}>
1095
+ <Settings className="mr-2 h-4 w-4" />
1096
+ <span>Settings</span>
1097
+ </DropdownMenuItem>
1098
+ <DropdownMenuSeparator />
1099
+ <DropdownMenuItem
1100
+ onClick={handleLogout}
1101
+ disabled={isLoading || isLoggingOut}
1102
+ >
1103
+ {isLoggingOut ? (
1104
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1105
+ ) : (
1106
+ <LogOut className="mr-2 h-4 w-4" />
1107
+ )}
1108
+ <span>Log out</span>
1109
+ </DropdownMenuItem>
1110
+ </DropdownMenuContent>
1111
+ </DropdownMenu>
1112
+ );
1113
+ }
1114
+ ```
1115
+
1116
+ ### Step 2: Create dropdown menu component
1117
+
1118
+ **File:** `host/src/kernel/components/ui/dropdown-menu.tsx`
1119
+
1120
+ ```typescript
1121
+ import * as React from 'react';
1122
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
1123
+ import { Check, ChevronRight, Circle } from 'lucide-react';
1124
+ import { cn } from '../../../lib/utils';
1125
+
1126
+ const DropdownMenu = DropdownMenuPrimitive.Root;
1127
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
1128
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
1129
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
1130
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
1131
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
1132
+
1133
+ const DropdownMenuSubTrigger = React.forwardRef<
1134
+ React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
1135
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
1136
+ inset?: boolean;
1137
+ }
1138
+ >(({ className, inset, children, ...props }, ref) => (
1139
+ <DropdownMenuPrimitive.SubTrigger
1140
+ ref={ref}
1141
+ className={cn(
1142
+ 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
1143
+ inset && 'pl-8',
1144
+ className
1145
+ )}
1146
+ {...props}
1147
+ >
1148
+ {children}
1149
+ <ChevronRight className="ml-auto h-4 w-4" />
1150
+ </DropdownMenuPrimitive.SubTrigger>
1151
+ ));
1152
+ DropdownMenuSubTrigger.displayName =
1153
+ DropdownMenuPrimitive.SubTrigger.displayName;
1154
+
1155
+ const DropdownMenuSubContent = React.forwardRef<
1156
+ React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
1157
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
1158
+ >(({ className, ...props }, ref) => (
1159
+ <DropdownMenuPrimitive.SubContent
1160
+ ref={ref}
1161
+ className={cn(
1162
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
1163
+ className
1164
+ )}
1165
+ {...props}
1166
+ />
1167
+ ));
1168
+ DropdownMenuSubContent.displayName =
1169
+ DropdownMenuPrimitive.SubContent.displayName;
1170
+
1171
+ const DropdownMenuContent = React.forwardRef<
1172
+ React.ElementRef<typeof DropdownMenuPrimitive.Content>,
1173
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
1174
+ >(({ className, sideOffset = 4, ...props }, ref) => (
1175
+ <DropdownMenuPrimitive.Portal>
1176
+ <DropdownMenuPrimitive.Content
1177
+ ref={ref}
1178
+ sideOffset={sideOffset}
1179
+ className={cn(
1180
+ 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
1181
+ className
1182
+ )}
1183
+ {...props}
1184
+ />
1185
+ </DropdownMenuPrimitive.Portal>
1186
+ ));
1187
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
1188
+
1189
+ const DropdownMenuItem = React.forwardRef<
1190
+ React.ElementRef<typeof DropdownMenuPrimitive.Item>,
1191
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
1192
+ inset?: boolean;
1193
+ }
1194
+ >(({ className, inset, ...props }, ref) => (
1195
+ <DropdownMenuPrimitive.Item
1196
+ ref={ref}
1197
+ className={cn(
1198
+ 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
1199
+ inset && 'pl-8',
1200
+ className
1201
+ )}
1202
+ {...props}
1203
+ />
1204
+ ));
1205
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
1206
+
1207
+ const DropdownMenuCheckboxItem = React.forwardRef<
1208
+ React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
1209
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
1210
+ >(({ className, children, checked, ...props }, ref) => (
1211
+ <DropdownMenuPrimitive.CheckboxItem
1212
+ ref={ref}
1213
+ className={cn(
1214
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
1215
+ className
1216
+ )}
1217
+ checked={checked}
1218
+ {...props}
1219
+ >
1220
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
1221
+ <DropdownMenuPrimitive.ItemIndicator>
1222
+ <Check className="h-4 w-4" />
1223
+ </DropdownMenuPrimitive.ItemIndicator>
1224
+ </span>
1225
+ {children}
1226
+ </DropdownMenuPrimitive.CheckboxItem>
1227
+ ));
1228
+ DropdownMenuCheckboxItem.displayName =
1229
+ DropdownMenuPrimitive.CheckboxItem.displayName;
1230
+
1231
+ const DropdownMenuRadioItem = React.forwardRef<
1232
+ React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
1233
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
1234
+ >(({ className, children, ...props }, ref) => (
1235
+ <DropdownMenuPrimitive.RadioItem
1236
+ ref={ref}
1237
+ className={cn(
1238
+ 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
1239
+ className
1240
+ )}
1241
+ {...props}
1242
+ >
1243
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
1244
+ <DropdownMenuPrimitive.ItemIndicator>
1245
+ <Circle className="h-2 w-2 fill-current" />
1246
+ </DropdownMenuPrimitive.ItemIndicator>
1247
+ </span>
1248
+ {children}
1249
+ </DropdownMenuPrimitive.RadioItem>
1250
+ ));
1251
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
1252
+
1253
+ const DropdownMenuLabel = React.forwardRef<
1254
+ React.ElementRef<typeof DropdownMenuPrimitive.Label>,
1255
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
1256
+ inset?: boolean;
1257
+ }
1258
+ >(({ className, inset, ...props }, ref) => (
1259
+ <DropdownMenuPrimitive.Label
1260
+ ref={ref}
1261
+ className={cn(
1262
+ 'px-2 py-1.5 text-sm font-semibold',
1263
+ inset && 'pl-8',
1264
+ className
1265
+ )}
1266
+ {...props}
1267
+ />
1268
+ ));
1269
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
1270
+
1271
+ const DropdownMenuSeparator = React.forwardRef<
1272
+ React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
1273
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
1274
+ >(({ className, ...props }, ref) => (
1275
+ <DropdownMenuPrimitive.Separator
1276
+ ref={ref}
1277
+ className={cn('-mx-1 my-1 h-px bg-muted', className)}
1278
+ {...props}
1279
+ />
1280
+ ));
1281
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
1282
+
1283
+ const DropdownMenuShortcut = ({
1284
+ className,
1285
+ ...props
1286
+ }: React.HTMLAttributes<HTMLSpanElement>) => {
1287
+ return (
1288
+ <span
1289
+ className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
1290
+ {...props}
1291
+ />
1292
+ );
1293
+ };
1294
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
1295
+
1296
+ export {
1297
+ DropdownMenu,
1298
+ DropdownMenuTrigger,
1299
+ DropdownMenuContent,
1300
+ DropdownMenuItem,
1301
+ DropdownMenuCheckboxItem,
1302
+ DropdownMenuRadioItem,
1303
+ DropdownMenuLabel,
1304
+ DropdownMenuSeparator,
1305
+ DropdownMenuShortcut,
1306
+ DropdownMenuGroup,
1307
+ DropdownMenuPortal,
1308
+ DropdownMenuSub,
1309
+ DropdownMenuSubContent,
1310
+ DropdownMenuSubTrigger,
1311
+ DropdownMenuRadioGroup,
1312
+ };
1313
+ ```
1314
+
1315
+ ### Step 3: Update topbar with logout button
1316
+
1317
+ **File:** `host/src/layout/Topbar.tsx`
1318
+
1319
+ ```typescript
1320
+ import { useGlobalKernelState } from '../kernel/shared-state';
1321
+ import { Menu, Bell } from 'lucide-react';
1322
+ import { LogoutButton } from '../kernel/auth/components/LogoutButton';
1323
+ import { useAuth } from '../kernel/auth/hooks';
1324
+
1325
+ export function Topbar() {
1326
+ const { toggleMobileMenu } = useGlobalKernelState();
1327
+ const { isAuthenticated } = useAuth();
1328
+
1329
+ return (
1330
+ <header className="sticky top-0 z-20 flex h-16 items-center gap-4 border-b bg-background px-6">
1331
+ {/* Mobile menu button */}
1332
+ <button
1333
+ onClick={toggleMobileMenu}
1334
+ className="lg:hidden rounded-lg p-2 text-muted-foreground hover:bg-muted"
1335
+ >
1336
+ <Menu className="h-5 w-5" />
1337
+ </button>
1338
+
1339
+ {/* Breadcrumb/spacer */}
1340
+ <div className="flex-1" />
1341
+
1342
+ {/* Actions */}
1343
+ <div className="flex items-center gap-2">
1344
+ {isAuthenticated && (
1345
+ <button className="rounded-lg p-2 text-muted-foreground hover:bg-muted">
1346
+ <Bell className="h-5 w-5" />
1347
+ </button>
1348
+ )}
1349
+
1350
+ {isAuthenticated && <LogoutButton />}
1351
+ </div>
1352
+ </header>
1353
+ );
1354
+ }
1355
+ ```
1356
+
1357
+ ---
1358
+
1359
+ ## Task 9: Create Auth Module Barrel Export
1360
+
1361
+ **Files:**
1362
+ - Create: `host/src/kernel/auth/index.ts`
1363
+
1364
+ ### Step 1: Create barrel export
1365
+
1366
+ **File:** `host/src/kernel/auth/index.ts`
1367
+
1368
+ ```typescript
1369
+ export * from './types';
1370
+ export * from './schemas';
1371
+ export * from './service';
1372
+ export * from './hooks';
1373
+ export * from './ProtectedRoute';
1374
+ ```
1375
+
1376
+ ---
1377
+
1378
+ ## Verification
1379
+
1380
+ ### Step 1: Build the host
1381
+
1382
+ **Run:**
1383
+
1384
+ ```bash
1385
+ cd host
1386
+ pnpm run build
1387
+ ```
1388
+
1389
+ Expected: Build completes without errors.
1390
+
1391
+ ### Step 2: Start development server
1392
+
1393
+ **Run:**
1394
+
1395
+ ```bash
1396
+ cd host
1397
+ pnpm run dev
1398
+ ```
1399
+
1400
+ Expected: Server starts on http://localhost:8080
1401
+
1402
+ ### Step 3: Test login flow
1403
+
1404
+ 1. Open http://localhost:8080
1405
+ 2. Click "Sign In" button
1406
+ 3. Enter credentials: `admin@example.com` / `admin123`
1407
+ 4. Submit form
1408
+ 5. Should redirect to `/dashboard`
1409
+ 6. Verify logout button appears in topbar with user avatar
1410
+ 7. Click logout button → "Log out"
1411
+ 8. Should redirect back to `/login`
1412
+
1413
+ ### Step 4: Test protected routes
1414
+
1415
+ 1. Try accessing http://localhost:8080/dashboard while logged out
1416
+ 2. Should redirect to `/login`
1417
+ 3. After login, should access dashboard successfully
1418
+
1419
+ ---
1420
+
1421
+ ## Summary
1422
+
1423
+ After completing this document, you will have:
1424
+
1425
+ 1. ✅ Complete authentication service with PocketBase integration
1426
+ 2. ✅ Login page with form validation using Zod + react-hook-form
1427
+ 3. ✅ Protected route component for guarding authenticated pages
1428
+ 4. ✅ Auth hooks: `useAuth`, `useCurrentUser`, `useRequireAuth`
1429
+ 5. ✅ Logout functionality with dropdown menu
1430
+ 6. ✅ Session persistence via PocketBase auth store
1431
+ 7. ✅ Error handling for auth failures
1432
+ 8. ✅ All required Shadcn UI components (Button, Input, Label, Card, Alert, DropdownMenu)
1433
+
1434
+ **Next:** `05-multitenancy-rbac.md` - Implement organizations, users management, roles, and permissions system.
1435
+
1436
+ ---
1437
+
1438
+ ## Files Created
1439
+
1440
+ ```
1441
+ host/
1442
+ └── src/
1443
+ └── kernel/
1444
+ ├── auth/
1445
+ │ ├── types.ts
1446
+ │ ├── schemas.ts
1447
+ │ ├── service.ts
1448
+ │ ├── hooks.ts
1449
+ │ ├── ProtectedRoute.tsx
1450
+ │ ├── components/
1451
+ │ │ └── LoginForm.tsx
1452
+ │ └── index.ts
1453
+ ├── components/
1454
+ │ └── ui/
1455
+ │ ├── button.tsx
1456
+ │ ├── input.tsx
1457
+ │ ├── label.tsx
1458
+ │ ├── card.tsx
1459
+ │ ├── alert.tsx
1460
+ │ └── dropdown-menu.tsx
1461
+ └── layout/
1462
+ └── Topbar.tsx (modified)
1463
+
1464
+ host/src/routes/
1465
+ └── login.tsx
1466
+ ```