Gograb Admin Implementation Design
Documentation for GoGrab Admin
GoGrab Admin - Implementation Design
Document: Technical Implementation Design Date: 2025-10-02 Version: 1.0.0 Status: MVP Implementation Ready
Table of Contents
- Architecture Overview
- Component Design
- Routing System
- State Management
- Authentication Flow
- Module Integration
- Data Flow
- Interfaces & Types
- Service Layer
- Implementation Guidelines
Architecture Overview
System Context
┌─────────────────────────────────────────────────────────────────┐
│ GoGrab Admin MVP │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Browser │ │ Angular 20 │ │ Mock API │ │
│ │ SPA │←→│ Standalone │←→│ (Dev Mode) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ↓ │
│ ┌───────────────┐ │
│ │ Module System │ │
│ │ (Dynamic │ │
│ │ Loading) │ │
│ └───────────────┘ │
│ ↓ │
│ ┌───────────────────────────────┐ │
│ │ @gograb-space/user-management │ │
│ │ (CRUD Components & Routes) │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Architectural Principles
- Standalone Components Only: No NgModules, 100% standalone architecture
- OnPush Change Detection: All components use OnPush strategy for performance
- Signal-First: Prefer Signals over Observables for local state
- RxJS for Async: All async operations use RxJS (no Promises)
- Module System: Dynamic module loading via
@libs-core/module-integrator - SOLID Principles: Strict adherence to Single Responsibility, DI, etc.
Layer Architecture
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ├─ Components (LoginComponent, DashboardComponent) │
│ ├─ Layout (AppShellComponent, HeaderComponent, Footer) │
│ └─ Guards (authGuard, permissionGuard, roleGuard) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
│ ├─ AuthService (login, logout, token management) │
│ ├─ DashboardService (stats fetching) │
│ └─ IntegratorService (module lifecycle orchestration) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ STATE LAYER │
│ ├─ NGXS Stores (UserState, AuthState from libraries) │
│ ├─ Component Signals (local reactive state) │
│ └─ Store Selectors (reactive state subscriptions) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ DATA LAYER │
│ ├─ Mock API (@libs-core/mock-api) │
│ ├─ HTTP Interceptors (auth, context, mock) │
│ └─ Models/DTOs (@libs-core/shared-models) │
└─────────────────────────────────────────────────────────────┘
Component Design
Component Hierarchy
AppComponent (root)
└── <router-outlet>
├── LoginComponent (/login)
│ ├── Reactive Form (email, password)
│ ├── Validation Messages
│ └── Submit Button
│
└── AppShellComponent (/ - authenticated)
├── HeaderComponent
│ ├── Logo
│ ├── User Profile Display
│ └── Logout Button
│
├── <main> (router-outlet for content)
│ ├── DashboardComponent (/dashboard)
│ │ ├── Stats Cards (4x)
│ │ └── Quick Actions
│ │
│ └── UserManagementModule (/users/*)
│ ├── UserListComponent (/users/list)
│ ├── UserFormComponent (/users/create, /users/:id/edit)
│ ├── UserDetailComponent (/users/:id)
│ └── UserDeleteConfirmComponent (modal)
│
└── FooterComponent
├── Copyright
├── Version
└── Credits
Component Specifications
1. AppComponent
Purpose: Root application component Type: Container (dumb) Responsibility: Bootstrap router-outlet only
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
template: '<router-outlet />',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {}
Complexity: Minimal (~10 lines)
2. LoginComponent
Purpose: User authentication interface
Type: Smart component (form handling + service interaction)
Location: apps/gograb-admin/src/app/features/auth/login.component.ts
Interface:
interface LoginForm {
email: FormControl<string>;
password: FormControl<string>;
}
interface LoginComponent {
// Form
loginForm: FormGroup<LoginForm>;
// State
isLoading: WritableSignal<boolean>;
errorMessage: WritableSignal<string | null>;
// Methods
onSubmit(): void;
private handleLoginSuccess(result: LoginResult): void;
private handleLoginError(error: Error): void;
}
Dependencies:
AuthService(injected)Router(injected)FormBuilder(injected)ReactiveFormsModule(imported)
State Management:
- Local signals for loading/error states
- No NGXS state (auth service handles token storage)
Validation Rules:
- Email: required, email format
- Password: required, minLength(6)
Flow:
- User enters credentials
- Form validation on blur
- Submit button disabled until valid
- On submit → show loading spinner
- AuthService.login() → Observable
- Success → navigate to /dashboard
- Error → display error message, hide spinner
Template Structure:
<div class="login-container">
<div class="login-card">
<h1>GoGrab Admin</h1>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<div class="form-field">
<label>Email</label>
<input type="email" formControlName="email" />
<span class="error" *ngIf="email validation error">...</span>
</div>
<div class="form-field">
<label>Password</label>
<input type="password" formControlName="password" />
<span class="error" *ngIf="password validation error">...</span>
</div>
<button type="submit" [disabled]="!loginForm.valid || isLoading()">
@if (isLoading()) {
<span>Logging in...</span>
} @else {
<span>Login</span>
}
</button>
@if (errorMessage(); as error) {
<div class="error-alert">{{ error }}</div>
}
</form>
</div>
</div>
Complexity: ~120 líneas TS + 40 líneas HTML
3. AppShellComponent
Purpose: Main layout structure for authenticated routes
Type: Container (layout/dumb)
Location: apps/gograb-admin/src/app/core/layout/app-shell.component.ts
Interface:
interface AppShellComponent {
// No state needed - pure layout
}
Dependencies:
HeaderComponent(imported)FooterComponent(imported)RouterOutlet(imported)
Template Structure:
<div class="app-shell">
<app-header />
<main class="main-content">
<router-outlet />
</main>
<app-footer />
</div>
Styling:
.app-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
flex: 1;
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
width: 100%;
}
Complexity: ~100 líneas (TS + HTML + SCSS)
4. HeaderComponent
Purpose: Top navigation bar with user profile and logout
Type: Smart component (user profile access)
Location: apps/gograb-admin/src/app/core/layout/header.component.ts
Interface:
interface HeaderComponent {
// State
userProfile: Signal<UserProfile | null>;
isMenuOpen: WritableSignal<boolean>;
// Methods
toggleMenu(): void;
logout(): void;
}
Dependencies:
AuthService(injected)Router(injected)Store(NGXS, injected)AsyncPipe(imported)
State Management:
userProfile: Selector from AuthStateisMenuOpen: Local signal for dropdown menu
Template Structure:
<header class="app-header">
<div class="header-content">
<div class="logo">
<span>GoGrab Admin</span>
</div>
<nav class="main-nav">
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a routerLink="/users/list" routerLinkActive="active">Users</a>
</nav>
<div class="user-menu">
@if (userProfile(); as user) {
<button (click)="toggleMenu()">
<span>{{ user.displayName }}</span>
<span class="role-badge">{{ user.role.name }}</span>
</button>
@if (isMenuOpen()) {
<div class="dropdown-menu">
<button (click)="logout()">Logout</button>
</div>
}
}
</div>
</div>
</header>
Complexity: ~80 líneas TS + 20 líneas HTML
5. FooterComponent
Purpose: Bottom page footer with copyright and version
Type: Dumb component (pure presentation)
Location: apps/gograb-admin/src/app/core/layout/footer.component.ts
Specification: See /docs/app/gograb-admin-footer-spec.md
Complexity: ~40 líneas TS + 15 líneas HTML
6. DashboardComponent
Purpose: Main dashboard with statistics overview
Type: Smart component (data fetching)
Location: apps/gograb-admin/src/app/features/dashboard/dashboard.component.ts
Interface:
interface DashboardStats {
totalUsers: number;
activeUsers: number;
newToday: number;
lastLoginAt: string;
}
interface DashboardComponent {
// State
stats: Signal<DashboardStats | null>;
isLoading: WritableSignal<boolean>;
error: WritableSignal<string | null>;
// Lifecycle
ngOnInit(): void;
// Methods
private loadStats(): void;
}
Dependencies:
DashboardService(injected)Router(injected)CommonModule(imported)
State Management:
- Local signals for stats, loading, error
- Fetch on ngOnInit
- No NGXS state needed for MVP
Template Structure:
<div class="dashboard">
<h1>Dashboard</h1>
@if (isLoading()) {
<div class="loading">Loading statistics...</div>
} @else if (error(); as err) {
<div class="error">{{ err }}</div>
} @else if (stats(); as data) {
<div class="stats-grid">
<div class="stat-card">
<h3>Total Users</h3>
<p class="stat-value">{{ data.totalUsers }}</p>
</div>
<div class="stat-card">
<h3>Active Users</h3>
<p class="stat-value">{{ data.activeUsers }}</p>
</div>
<div class="stat-card">
<h3>New Today</h3>
<p class="stat-value">{{ data.newToday }}</p>
</div>
<div class="stat-card">
<h3>Last Login</h3>
<p class="stat-value">{{ data.lastLoginAt | date }}</p>
</div>
</div>
<div class="quick-actions">
<h2>Quick Actions</h2>
<button routerLink="/users/create">Create New User</button>
<button routerLink="/users/list">View All Users</button>
</div>
}
</div>
Complexity: ~100 líneas TS + 50 líneas HTML
Routing System
Route Configuration
app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from '@libs-core/security';
import { LoginComponent } from './features/auth/login.component';
import { DashboardComponent } from './features/dashboard/dashboard.component';
import { AppShellComponent } from './core/layout/app-shell.component';
export const routes: Routes = [
// Public routes
{
path: 'login',
component: LoginComponent,
title: 'Login - GoGrab Admin'
},
// Protected routes (authenticated)
{
path: '',
component: AppShellComponent,
canActivate: [authGuard],
children: [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: DashboardComponent,
title: 'Dashboard - GoGrab Admin'
},
// Module routes registered dynamically by ModuleIntegratorService
// /users/* routes will be added at runtime
]
},
// Fallback
{
path: '**',
redirectTo: 'dashboard'
}
];
Dynamic Route Registration
Process:
provideModuleIntegratorinitializes module systemModuleDiscoveryServicefindsUserManagementModule- Module lifecycle:
configure()→initialize()→start() - During
start(),IntegratorServicecallsRouter.resetConfig() - New config = original routes + module routes (e.g.,
userManagementRoutes) - Navigation to
/users/*now works
Module Routes (from @gograb-space/user-management):
// These routes are exported from user-management library
export const userManagementRoutes: Routes = [
{ path: '', redirectTo: 'list', pathMatch: 'full' },
{
path: 'list',
component: UserListComponent,
canActivate: [authGuard, permissionGuard('users:read')]
},
{
path: 'create',
component: UserFormComponent,
canActivate: [authGuard, permissionGuard('users:create')],
data: { mode: 'create' }
},
{
path: ':id',
component: UserDetailComponent,
canActivate: [authGuard, permissionGuard('users:read')]
},
{
path: ':id/edit',
component: UserFormComponent,
canActivate: [authGuard, permissionGuard('users:update')],
data: { mode: 'edit' }
}
];
Guards
authGuard
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
const hasToken = authService.hasValidToken();
if (!hasToken) {
router.navigate(['/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
return true;
};
permissionGuard (Factory)
export function permissionGuard(permission: string): CanActivateFn {
return (route, state) => {
const permissionService = inject(PermissionService);
const router = inject(Router);
const hasPermission = permissionService.hasPermission(permission)();
if (!hasPermission) {
router.navigate(['/unauthorized']);
return false;
}
return true;
};
}
roleGuard (Factory)
export function roleGuard(role: SystemRoles): CanActivateFn {
return (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
const userRole = authService.getCurrentUser()?.role;
if (userRole?.id !== role) {
router.navigate(['/unauthorized']);
return false;
}
return true;
};
}
State Management
NGXS Store Architecture
┌──────────────────────────────────────────────────────────┐
│ NGXS STORES (from libraries) │
├──────────────────────────────────────────────────────────┤
│ AuthState │
│ ├─ userProfile: UserProfile | null │
│ ├─ isAuthenticated: boolean │
│ └─ token: string | null │
│ │
│ UserState (from @gograb-space/user-management) │
│ ├─ users: User[] │
│ ├─ selectedUser: User | null │
│ ├─ isLoading: boolean │
│ └─ error: string | null │
└──────────────────────────────────────────────────────────┘
Component State Pattern
Smart Components (LoginComponent, DashboardComponent, HeaderComponent):
- Inject services (AuthService, DashboardService)
- Use Signals for local state (isLoading, errorMessage)
- Subscribe to NGXS selectors when needed
- Dispatch actions via Store.dispatch()
Dumb Components (AppShellComponent, FooterComponent):
- No service injection (except Router for navigation if needed)
- @Input() for data
- @Output() for events
- Pure presentation logic
Signal Usage Patterns
WritableSignal (local state):
class LoginComponent {
isLoading = signal(false);
errorMessage = signal<string | null>(null);
onSubmit() {
this.isLoading.set(true);
this.errorMessage.set(null);
this.authService.login(...).subscribe({
next: () => {
this.isLoading.set(false);
this.router.navigate(['/dashboard']);
},
error: (err) => {
this.isLoading.set(false);
this.errorMessage.set(err.message);
}
});
}
}
Computed Signal (derived state):
class DashboardComponent {
private statsSignal = signal<DashboardStats | null>(null);
// Computed signal for user-friendly display
formattedStats = computed(() => {
const stats = this.statsSignal();
if (!stats) return null;
return {
...stats,
lastLoginAt: formatDistanceToNow(new Date(stats.lastLoginAt))
};
});
}
Signal from Observable (NGXS selector):
class HeaderComponent {
private store = inject(Store);
// Convert NGXS selector to Signal
userProfile = toSignal(
this.store.select(AuthState.userProfile),
{ initialValue: null }
);
}
Authentication Flow
Login Sequence Diagram
User LoginComponent AuthService MockAPI Store Router
│ │ │ │ │ │
│ Enter credentials │ │ │ │ │
│─────────────────────>│ │ │ │ │
│ │ │ │ │ │
│ Click Submit │ │ │ │ │
│─────────────────────>│ │ │ │ │
│ │ │ │ │ │
│ │ login(email, pw) │ │ │
│ │─────────────────>│ │ │ │
│ │ │ │ │ │
│ │ │ POST /api/auth/login │ │
│ │ │────────────────>│ │ │
│ │ │ │ │ │
│ │ │ { token, profile } │ │
│ │ │<────────────────│ │ │
│ │ │ │ │ │
│ │ │ storeToken() │ │ │
│ │ │──┐ │ │ │
│ │ │<─┘ │ │ │
│ │ │ │ │ │
│ │ │ dispatch(SetAuthProfile) │ │
│ │ │────────────────────────────────>│ │
│ │ │ │ │ │
│ │ LoginResult │ │ │ │
│ │<─────────────────│ │ │ │
│ │ │ │ │ │
│ │ navigate('/dashboard') │ │ │
│ │────────────────────────────────────────────────────────────────────>│
│ │ │ │ │ │
│ Dashboard loads │ │ │ │ │
│<─────────────────────────────────────────────────────────────────────────────────────────────│
Token Management
Storage:
- Location:
localStorage(viaSecureStorageService) - Key:
'auth_token' - Format: JWT string (mock or real)
Token Structure (Mock):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"profile": {
"id": "1",
"tenantId": "default-tenant",
"email": "[email protected]",
"displayName": "Admin User",
"role": {
"id": "admin",
"name": "Administrator",
"level": 100
},
"permissions": [
{ "id": "1", "name": "users:read", "resource": "users", "action": "read" },
{ "id": "2", "name": "users:create", "resource": "users", "action": "create" },
{ "id": "3", "name": "users:update", "resource": "users", "action": "update" },
{ "id": "4", "name": "users:delete", "resource": "users", "action": "delete" },
{ "id": "5", "name": "users:manage", "resource": "users", "action": "manage" }
]
}
}
Validation:
- MVP: Check token existence only
- Post-MVP: Validate JWT signature and expiry
Module Integration
Bootstrap Sequence
main.ts
│
├─> bootstrapApplication(AppComponent, appConfig)
│
└─> appConfig.providers[]
│
├─> 1. provideRouter(routes, withComponentInputBinding())
│ └─> Registers app base routes
│
├─> 2. provideModuleIntegrator({
│ autoDiscovery: true,
│ validateDependencies: true,
│ enableLifecycleLogging: true,
│ tenantId: 'default-tenant'
│ })
│ └─> Initializes IntegratorService
│ └─> ModuleDiscoveryService.discoverAndRegister()
│ └─> Finds @ModuleDecorator decorated modules
│ └─> UserManagementModule discovered
│
├─> 3. provideHttpClient(withInterceptors([
│ authInterceptor,
│ contextInterceptor,
│ mockHttpInterceptor
│ ]))
│
├─> 4. provideStore([UserState])
│ └─> NGXS state initialization
│
├─> 5. provideMockApi([UserMockApi])
│ └─> Mock API routes registration
│
├─> 6. provideEventBus()
│ └─> Event system initialization
│
└─> 7. provideLogger({
appenders: [ConsoleAppender],
level: LogLevel.DEBUG,
context: { appName: 'gograb-admin', tenantId: 'default-tenant' }
})
Module Lifecycle
IntegratorService.initializeApplication()
│
├─> 1. DISCOVERY
│ ModuleDiscoveryService.discoverAndRegister()
│ └─> Scans for @ModuleDecorator
│ └─> Finds UserManagementModule
│ └─> Adds to ModuleRegistry
│
├─> 2. CONFIGURATION (from API)
│ GET /api/tenants/default-tenant/modules/config
│ └─> Returns: [{ id: 'user-management', enabled: true, config: {...} }]
│
├─> 3. DEPENDENCY RESOLUTION
│ DependencySolverService.validateDependencies()
│ └─> Builds dependency graph
│ └─> Validates no circular dependencies
│ └─> Returns execution order
│
├─> 4. LIFECYCLE EXECUTION
│ For each module in execution order:
│
│ ├─> 4a. CONFIGURE
│ │ UserManagementModule.configure(config)
│ │ └─> Log: "Module 'user-management' configured"
│ │
│ ├─> 4b. INITIALIZE
│ │ UserManagementModule.initialize()
│ │ └─> Sets up services, state, event listeners
│ │ └─> Log: "Module 'user-management' initialized"
│ │
│ └─> 4c. START
│ UserManagementModule.start()
│ └─> Module activates
│ └─> Log: "Module 'user-management' started"
│
└─> 5. ROUTE REGISTRATION
IntegratorService.registerModuleRoutes()
└─> Router.resetConfig(baseRoutes + userManagementRoutes)
└─> /users/* routes now available
└─> Log: "Routes registered for user-management"
Console Logging Output
Expected console logs during bootstrap:
[LOG] IntegratorService initialized
[LOG] Module discovery started
[LOG] Module 'user-management' discovered
[LOG] Fetching module configuration for tenant: default-tenant
[LOG] Module configuration loaded: 1 modules
[LOG] Validating module dependencies
[LOG] Dependency validation passed
[LOG] Starting module lifecycle for: user-management
[LOG] Module 'user-management' configured
[LOG] Module 'user-management' initialized
[LOG] Module 'user-management' started
[LOG] Routes registered for user-management: 5 routes
[LOG] Application initialization complete
Data Flow
Read Operation Flow (Dashboard Stats)
DashboardComponent
│
│ ngOnInit()
│
├─> DashboardService.getStats()
│ │
│ ├─> HTTP GET /api/dashboard/stats
│ │ │
│ │ └─> MockAPI intercepts
│ │ │
│ │ └─> Returns mock stats
│ │
│ └─> Observable<DashboardStats>
│
└─> Subscribe
│
├─> Success
│ └─> stats.set(data)
│ └─> Template updates (OnPush)
│
└─> Error
└─> error.set(errorMessage)
└─> Template shows error
Write Operation Flow (User Login)
LoginComponent
│
│ onSubmit()
│
├─> AuthService.login(email, password)
│ │
│ ├─> HTTP POST /api/auth/login
│ │ │
│ │ └─> MockAPI intercepts
│ │ │
│ │ ├─> Validates credentials
│ │ └─> Returns { token, profile }
│ │
│ ├─> SecureStorageService.setItem('auth_token', token)
│ │
│ ├─> Store.dispatch(new SetAuthProfile(profile))
│ │ │
│ │ └─> AuthState reducer
│ │ └─> Updates state.userProfile
│ │
│ └─> Observable<LoginResult>
│
└─> Subscribe
│
├─> Success
│ └─> Router.navigate(['/dashboard'])
│
└─> Error
└─> errorMessage.set(error)
CRUD Operation Flow (User Management)
UserListComponent (from @gograb-space/user-management)
│
│ ngOnInit()
│
├─> Store.dispatch(new LoadUsers())
│ │
│ └─> UserState
│ │
│ ├─> UserService.getAll()
│ │ │
│ │ └─> HTTP GET /api/users
│ │ │
│ │ └─> MockAPI returns users array
│ │
│ └─> Reducer updates state.users
│
└─> users$ = Store.select(UserState.users)
│
└─> Template displays via AsyncPipe
│
@for (user of users$ | async) {
<user-card [user]="user" />
}
Interfaces & Types
Core Types
// Auth Types
interface LoginCredentials {
email: string;
password: string;
}
interface LoginResult {
success: boolean;
token?: string;
profile?: UserProfile;
error?: string;
}
interface UserProfile {
id: string;
tenantId: string;
email: string;
displayName: string;
role: Role;
permissions: Permission[];
createdAt: string;
updatedAt: string;
}
interface Role {
id: string;
name: string;
level: number;
permissions?: Permission[];
}
interface Permission {
id: string;
tenantId: string;
name: string; // Format: "resource:action" (e.g., "users:create")
resource: string; // e.g., "users", "settings"
action: string; // e.g., "create", "read", "update", "delete"
description: string;
}
// Dashboard Types
interface DashboardStats {
totalUsers: number;
activeUsers: number;
newToday: number;
lastLoginAt: string; // ISO 8601 datetime
}
// Module Integration Types
interface ModuleConfig {
autoDiscovery: boolean;
validateDependencies: boolean;
enableLifecycleLogging: boolean;
tenantId: string;
}
interface ModuleManifest {
readonly id: string;
readonly version: string;
readonly displayName?: string;
readonly permissions?: readonly string[];
}
// HTTP Response Types (from @libs-core/shared-models)
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ErrorResponse;
meta?: Record<string, unknown>;
}
interface ErrorResponse {
code: string;
message: string;
details?: Record<string, unknown>;
}
Form Types
// Login Form
interface LoginFormValue {
email: string;
password: string;
}
type LoginForm = FormGroup<{
email: FormControl<string>;
password: FormControl<string>;
}>;
Service Layer
AuthService
Location: apps/gograb-admin/src/app/core/services/auth.service.ts
Interface:
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
private store = inject(Store);
private router = inject(Router);
private storage = inject(SecureStorageService);
/**
* Authenticate user with credentials
* @param email User email
* @param password User password
* @returns Observable<LoginResult>
*/
login(email: string, password: string): Observable<LoginResult>;
/**
* Logout current user
* Clears token and navigates to login
*/
logout(): void;
/**
* Check if user has valid authentication token
* @returns boolean
*/
hasValidToken(): boolean;
/**
* Get current authenticated user profile
* @returns UserProfile | null
*/
getCurrentUser(): UserProfile | null;
/**
* Get authentication token
* @returns string | null
*/
getToken(): string | null;
}
Implementation:
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
private store = inject(Store);
private router = inject(Router);
private storage = inject(SecureStorageService);
login(email: string, password: string): Observable<LoginResult> {
return this.http.post<ApiResponse<LoginResult>>(
'/api/auth/login',
{ email, password }
).pipe(
map(response => {
if (response.success && response.data) {
const { token, profile } = response.data;
// Store token
this.storage.setItem('auth_token', token);
// Update NGXS state
this.store.dispatch(new SetAuthProfile(profile));
return { success: true, token, profile };
}
return {
success: false,
error: response.error?.message || 'Login failed'
};
}),
catchError(error => {
return of({
success: false,
error: error.message || 'Network error'
});
})
);
}
logout(): void {
this.storage.removeItem('auth_token');
this.store.dispatch(new ClearAuthProfile());
this.router.navigate(['/login']);
}
hasValidToken(): boolean {
const token = this.getToken();
// MVP: Just check existence
// Post-MVP: Validate JWT signature and expiry
return token !== null && token.length > 0;
}
getCurrentUser(): UserProfile | null {
return this.store.selectSnapshot(AuthState.userProfile);
}
getToken(): string | null {
return this.storage.getItem('auth_token');
}
}
DashboardService
Location: apps/gograb-admin/src/app/features/dashboard/dashboard.service.ts
Interface:
@Injectable({ providedIn: 'root' })
export class DashboardService {
private http = inject(HttpClient);
/**
* Fetch dashboard statistics
* @returns Observable<DashboardStats>
*/
getStats(): Observable<DashboardStats>;
}
Implementation:
@Injectable({ providedIn: 'root' })
export class DashboardService {
private http = inject(HttpClient);
getStats(): Observable<DashboardStats> {
return this.http.get<ApiResponse<DashboardStats>>(
'/api/dashboard/stats'
).pipe(
map(response => {
if (response.success && response.data) {
return response.data;
}
throw new Error(response.error?.message || 'Failed to load stats');
})
);
}
}
Implementation Guidelines
Component Development Checklist
For each component:
- Create component file with
@Componentdecorator - Set
standalone: true - Set
changeDetection: ChangeDetectionStrategy.OnPush - Import all dependencies in
importsarray - Define clear interface for component state
- Use Signals for local reactive state
- Use FormGroup for all forms (no Template-driven forms)
- Add validation rules to form controls
- Inject services via
inject()function - Subscribe to observables in lifecycle hooks
- Unsubscribe or use
takeUntilDestroyed()for cleanup - Use new control flow syntax (@if, @for, @switch)
- Add aria-labels for accessibility
- Test component manually in browser
- Verify OnPush detection works (state updates trigger view updates)
Service Development Checklist
For each service:
- Create service file with
@Injectable({ providedIn: 'root' }) - Define clear public interface (methods + return types)
- Use RxJS operators (map, catchError, tap) for data transformation
- Return Observables (never Promises)
- Handle errors with catchError operator
- Use HttpClient for all HTTP requests
- Follow API response structure (ApiResponse
) - Add JSDoc comments for public methods
- Keep services focused (Single Responsibility)
- Inject dependencies via
inject()function
State Management Checklist
For state integration:
- Import Store from @ngxs/store
- Define actions (e.g., SetAuthProfile, ClearAuthProfile)
- Dispatch actions via
Store.dispatch() - Create selectors for state slices
- Use
Store.select()orStore.selectSnapshot() - Convert observables to signals with
toSignal()if needed - Document state shape in comments
- Test state changes in browser DevTools (Redux DevTools extension)
Routing Checklist
For routing setup:
- Define routes in
app.routes.ts - Add guards to protected routes
- Set meaningful route titles
- Configure lazy loading where applicable
- Add redirects for convenience
- Handle 404 with wildcard route
- Test navigation manually (all routes accessible)
- Verify guards work (redirect when not authenticated)
- Check browser history works (back/forward buttons)
Module Integration Checklist
For module system:
- Add
provideModuleIntegratorto app.config providers - Configure auto-discovery: true
- Enable lifecycle logging for debugging
- Verify console logs show module lifecycle events
- Test dynamic route loading (navigate to /users/*)
- Verify module routes work correctly
- Check guards are applied to module routes
- Test module configuration from mock API
Testing Checklist (Manual for MVP)
For each feature:
- Happy path: Feature works as expected with valid inputs
- Error path: Feature handles errors gracefully
- Edge cases: Empty data, long text, special characters
- Responsive: Works on mobile viewport (375px)
- Accessibility: Tab navigation works, screen reader friendly
- Browser compatibility: Test in Chrome, Firefox (minimum)
- Performance: No console errors, smooth interactions
- State: App state persists correctly (refresh page)
Next Steps
This implementation design provides the complete blueprint for gograb-admin MVP. The next phase is execution:
Day 1 - Foundation
- Run Nx generator to create app
- Setup app.config.ts with all providers
- Create basic app.routes.ts
- Verify build and serve
Day 2 - Authentication
- Create LoginComponent with reactive form
- Implement AuthService
- Setup mock auth endpoint
- Create AppShellComponent
- Test login flow
Day 3 - Dashboard & Layout
- Create DashboardComponent
- Implement DashboardService
- Create HeaderComponent
- Create FooterComponent
- Configure module integrator
- Verify lifecycle logs
Day 4 - Module Integration
- Configure module routes registration
- Test /users/* navigation
- Verify CRUD operations work
- Test permissions (admin vs editor)
- Validate lifecycle logs
Day 5 - Polish & Validation
- Apply basic styling
- Add error handling
- Add loading states
- Manual end-to-end testing
- Documentation and demo prep
Document Version: 1.0.0 Last Updated: 2025-10-02 Status: Ready for Implementation