diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..cf1c099 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,142 @@ +# BioStream - AI Coding Agent Instructions + +## Project Overview +BioStream is an Angular 19 standalone application with TypeScript, featuring role-based authentication and multi-user workflows. The app supports five user roles: User, Administrator, FieldAgent, FinanceUser, and ITSupportUser. + +## Architecture & Key Patterns + +### Component Architecture +- **Standalone Components**: All components use Angular 19's standalone API (no NgModules) +- **Signal-based State**: Uses Angular signals for reactive state management instead of RxJS BehaviorSubject where possible +- **Route-level Lazy Loading**: Components are lazy-loaded via `loadComponent` in routes + +### Authentication Flow +1. **Registration** → Email verification link sent +2. **Email Verification** → Account activated +3. **Login** → OTP sent to email (check console for demo OTP) +4. **OTP Verification** → User authenticated and redirected to role-based dashboard + +### User Roles & Dashboards +Each role has a dedicated dashboard route: +- `Administrator` → `/admin/dashboard` +- `FieldAgent` → `/field-agent/dashboard` +- `FinanceUser` → `/finance/dashboard` +- `ITSupportUser` → `/it-support/dashboard` +- `User` → `/user/dashboard` + +### Guards & Security +- `authGuard`: Protects authenticated routes +- `guestGuard`: Prevents authenticated users from accessing login/register +- `roleGuard(roles)`: Enforces role-based access control + +## Key Files & Locations + +### Core Services +- `src/app/services/auth.service.ts` - Authentication, session management, mock user database +- `src/app/services/email.service.ts` - Email templates (OTP, verification, welcome emails) + +### Models +- `src/app/models/user.model.ts` - User interface, UserRole enum, auth request/response types + +### Guards +- `src/app/guards/auth.guard.ts` - All route protection logic + +### Components Structure +``` +src/app/components/ +├── login/ # Login with demo credentials +├── register/ # Multi-role registration +├── verify-otp/ # OTP verification with resend +├── verify-email/ # Email verification handler +└── dashboards/ # Role-specific landing pages + ├── admin-dashboard.component.ts + ├── field-agent-dashboard.component.ts + ├── finance-dashboard.component.ts + ├── it-support-dashboard.component.ts + └── user-dashboard.component.ts +``` + +## Development Patterns + +### Component Style +- Use inline templates and styles for components +- Leverage `@if`, `@for`, `@defer` control flow syntax (not *ngIf, *ngFor) +- Import `CommonModule` and `FormsModule` as needed + +### State Management +```typescript +// Prefer signals over observables for local state +private loading = signal(false); +private errorMessage = signal(''); +``` + +### Authentication Service Usage +```typescript +// Check authentication +if (authService.isAuthenticated()) { } + +// Check role +if (authService.hasRole(UserRole.Administrator)) { } + +// Get current user +const user = authService.getCurrentUser(); + +// Get role dashboard route +const route = authService.getRoleDashboardRoute(user.role); +``` + +## Mock Data & Demo Accounts + +The app includes pre-configured demo accounts (see `auth.service.ts` → `initializeMockUsers`): +- **Admin**: admin@biostream.com / admin123 +- **Field Agent**: field@biostream.com / field123 +- **Finance**: finance@biostream.com / finance123 +- **IT Support**: it@biostream.com / it123 +- **User**: user@biostream.com / user123 + +OTPs are logged to console during development. + +## Build & Run + +```bash +# Development server +npm start + +# Production build +npm run build + +# Run tests +npm test +``` + +## Adding New Features + +### Adding a New User Role +1. Add role to `UserRole` enum in `user.model.ts` +2. Create dashboard component in `components/dashboards/` +3. Add route in `app.routes.ts` with `roleGuard` +4. Update `getRoleDashboardRoute()` in `auth.service.ts` + +### Adding Protected Routes +```typescript +{ + path: 'protected-page', + loadComponent: () => import('./component').then(m => m.Component), + canActivate: [roleGuard([UserRole.Administrator, UserRole.User])] +} +``` + +## Common Patterns to Follow + +- **Lazy load routes**: Always use `loadComponent` for route components +- **Use signals for component state**: Prefer `signal()` over class properties for reactive data +- **Inline auth checks in templates**: Use `@if (authService.isAuthenticated())` +- **Role-based UI**: Show/hide features based on `authService.hasRole()` or `hasAnyRole()` +- **Logout everywhere**: Include logout button in all dashboards + +## Important Notes + +- This is a **mock authentication system** - no backend integration yet +- OTP and verification tokens are stored in localStorage (client-side only) +- All email sends are simulated and logged to console +- Session persistence uses localStorage for demo purposes diff --git a/angular.json b/angular.json index 8e608be..58791e9 100644 --- a/angular.json +++ b/angular.json @@ -2,7 +2,8 @@ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { - "packageManager": "npm" + "packageManager": "npm", + "analytics": "b939cba9-d047-4104-9335-993c60a34d95" }, "newProjectRoot": "projects", "projects": { diff --git a/package-lock.json b/package-lock.json index b7526a7..f4cd008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -460,6 +460,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.2.tgz", "integrity": "sha512-dOi7w0dsUCJ5ZFnXD2eR/8LWy9/XAzXuo9zU6zu7qP4vimjTQRs11IawnuC+jaAQtCFiySshzEPPsuAw9bPkOA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -476,6 +477,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.2.tgz", "integrity": "sha512-Rs69yqT1M+l0DqAAZcGDt2TntKAPyldEViq3GQHbkM1W4f/hoRgBRsE6StxvP6wszW6VVHH3uQQdyeZV8Z4rpw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -489,6 +491,7 @@ "integrity": "sha512-+6lyvDV0rY1qbc9+rzFCBZDGCfJU0ah3p+4Tu0YYgKRbpbwvqj/O4cG1mLknEuQ2G61Y/tTKnTa4ng1XNtqVyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -521,6 +524,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.2.tgz", "integrity": "sha512-jj2lYmwMKYY7tmZ7ml8rXJRKwkVMJamFIf6VQuIlSFK79Pmn6AeUhZwDlrAmK7sY9kakEKUmslSg0XLL3bfiyw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -565,6 +569,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.2.tgz", "integrity": "sha512-Qygk215mRK2S1tvD6B5dy3ekMidGmmLktxr5i01YC8synHYcex7HK18JcWuCrFbY6NbCnHsMD3bYi0mwhag+Sg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -686,6 +691,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1036,6 +1042,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1079,6 +1086,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1795,6 +1803,7 @@ "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", @@ -3885,7 +3894,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", @@ -4384,6 +4394,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4549,6 +4560,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -5247,6 +5259,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5951,6 +5964,7 @@ "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -6051,6 +6065,7 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -7528,6 +7543,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8152,7 +8168,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "4.0.0", @@ -8190,6 +8207,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8322,6 +8340,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -8881,6 +8900,7 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -9318,6 +9338,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/app/app.html b/src/app/app.html index e0118a1..67e7bd4 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,342 +1 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title() }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'}, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc39edb..d5f1333 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,3 +1,58 @@ import { Routes } from '@angular/router'; +import { authGuard, guestGuard, roleGuard } from './guards/auth.guard'; +import { UserRole } from './models/user.model'; -export const routes: Routes = []; +export const routes: Routes = [ + { + path: '', + redirectTo: '/login', + pathMatch: 'full' + }, + { + path: 'login', + loadComponent: () => import('./components/login/login.component').then(m => m.LoginComponent), + canActivate: [guestGuard] + }, + { + path: 'register', + loadComponent: () => import('./components/register/register.component').then(m => m.RegisterComponent), + canActivate: [guestGuard] + }, + { + path: 'verify-otp', + loadComponent: () => import('./components/verify-otp/verify-otp.component').then(m => m.VerifyOtpComponent) + }, + { + path: 'verify-email', + loadComponent: () => import('./components/verify-email/verify-email.component').then(m => m.VerifyEmailComponent) + }, + { + path: 'admin/dashboard', + loadComponent: () => import('./components/dashboards/admin-dashboard.component').then(m => m.AdminDashboardComponent), + canActivate: [roleGuard([UserRole.Administrator])] + }, + { + path: 'field-agent/dashboard', + loadComponent: () => import('./components/dashboards/field-agent-dashboard.component').then(m => m.FieldAgentDashboardComponent), + canActivate: [roleGuard([UserRole.FieldAgent])] + }, + { + path: 'finance/dashboard', + loadComponent: () => import('./components/dashboards/finance-dashboard.component').then(m => m.FinanceDashboardComponent), + canActivate: [roleGuard([UserRole.FinanceUser])] + }, + { + path: 'it-support/dashboard', + loadComponent: () => import('./components/dashboards/it-support-dashboard.component').then(m => m.ITSupportDashboardComponent), + canActivate: [roleGuard([UserRole.ITSupportUser])] + }, + { + path: 'user/dashboard', + loadComponent: () => import('./components/dashboards/user-dashboard.component').then(m => m.UserDashboardComponent), + canActivate: [roleGuard([UserRole.User])] + }, + { + path: '**', + redirectTo: '/login' + } +]; diff --git a/src/app/components/dashboards/admin-dashboard.component.ts b/src/app/components/dashboards/admin-dashboard.component.ts new file mode 100644 index 0000000..8814fbc --- /dev/null +++ b/src/app/components/dashboards/admin-dashboard.component.ts @@ -0,0 +1,222 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { User } from '../../models/user.model'; + +@Component({ + selector: 'app-admin-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+

Administrator Dashboard

+ +
+ +
+

Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!

+

Administrator

+
+ +
+
+
👥
+

User Management

+

Manage all system users and permissions

+
+ 125 + Total Users +
+
+ +
+
⚙️
+

System Settings

+

Configure system-wide settings and parameters

+
+ 15 + Active Modules +
+
+ +
+
📊
+

Analytics

+

View system analytics and reports

+
+ 98% + System Uptime +
+
+ +
+
🔐
+

Security

+

Monitor security logs and access controls

+
+ 0 + Security Alerts +
+
+ +
+
💾
+

Database

+

Database management and backups

+
+ 2.4 GB + Storage Used +
+
+ +
+
📧
+

Communications

+

Email templates and notifications

+
+ 450 + Emails Sent Today +
+
+
+
+ `, + styles: [` + .dashboard { + padding: 20px; + max-width: 1400px; + margin: 0 auto; + } + + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .dashboard-header h1 { + margin: 0; + color: #333; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-danger { + background-color: #dc3545; + color: white; + } + + .btn-danger:hover { + background-color: #c82333; + } + + .welcome-section { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + } + + .welcome-section h2 { + margin: 0 0 10px 0; + } + + .role-badge { + display: inline-block; + padding: 5px 15px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + } + + .role-badge.admin { + background-color: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + } + + .card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; + } + + .card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + } + + .card-icon { + font-size: 48px; + margin-bottom: 15px; + } + + .admin-icon { + filter: hue-rotate(250deg); + } + + .card h3 { + margin: 0 0 10px 0; + color: #333; + } + + .card p { + color: #666; + margin: 0 0 20px 0; + } + + .card-stats { + display: flex; + flex-direction: column; + padding-top: 15px; + border-top: 1px solid #eee; + } + + .stat-number { + font-size: 28px; + font-weight: 700; + color: #667eea; + margin-bottom: 5px; + } + + .stat-label { + font-size: 12px; + color: #999; + text-transform: uppercase; + letter-spacing: 1px; + } + `] +}) +export class AdminDashboardComponent { + user = signal(null); + + constructor( + private authService: AuthService, + private router: Router + ) { + this.user.set(this.authService.getCurrentUser()); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/src/app/components/dashboards/field-agent-dashboard.component.ts b/src/app/components/dashboards/field-agent-dashboard.component.ts new file mode 100644 index 0000000..83abdcd --- /dev/null +++ b/src/app/components/dashboards/field-agent-dashboard.component.ts @@ -0,0 +1,218 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { User } from '../../models/user.model'; + +@Component({ + selector: 'app-field-agent-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+

Field Agent Dashboard

+ +
+ +
+

Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!

+

Field Agent

+
+ +
+
+
📍
+

Site Visits

+

Manage and track site visits

+
+ 12 + Scheduled Today +
+
+ +
+
📋
+

Sample Collection

+

Record and manage biological samples

+
+ 45 + Samples This Week +
+
+ +
+
📊
+

Field Reports

+

Submit and view field reports

+
+ 3 + Pending Reports +
+
+ +
+
🗺️
+

Route Planning

+

Optimize your field routes

+
+ 8 + Locations Today +
+
+ +
+
📸
+

Photo Documentation

+

Upload and manage site photos

+
+ 127 + Photos This Month +
+
+ +
+
⏱️
+

Time Tracking

+

Log field work hours

+
+ 32.5 + Hours This Week +
+
+
+
+ `, + styles: [` + .dashboard { + padding: 20px; + max-width: 1400px; + margin: 0 auto; + } + + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .dashboard-header h1 { + margin: 0; + color: #333; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-danger { + background-color: #dc3545; + color: white; + } + + .btn-danger:hover { + background-color: #c82333; + } + + .welcome-section { + color: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + } + + .welcome-section.field-agent { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + } + + .welcome-section h2 { + margin: 0 0 10px 0; + } + + .role-badge { + display: inline-block; + padding: 5px 15px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + background-color: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + } + + .card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; + } + + .card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + } + + .card-icon { + font-size: 48px; + margin-bottom: 15px; + } + + .card h3 { + margin: 0 0 10px 0; + color: #333; + } + + .card p { + color: #666; + margin: 0 0 20px 0; + } + + .card-stats { + display: flex; + flex-direction: column; + padding-top: 15px; + border-top: 1px solid #eee; + } + + .stat-number { + font-size: 28px; + font-weight: 700; + color: #11998e; + margin-bottom: 5px; + } + + .stat-label { + font-size: 12px; + color: #999; + text-transform: uppercase; + letter-spacing: 1px; + } + `] +}) +export class FieldAgentDashboardComponent { + user = signal(null); + + constructor( + private authService: AuthService, + private router: Router + ) { + this.user.set(this.authService.getCurrentUser()); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/src/app/components/dashboards/finance-dashboard.component.ts b/src/app/components/dashboards/finance-dashboard.component.ts new file mode 100644 index 0000000..de7ef45 --- /dev/null +++ b/src/app/components/dashboards/finance-dashboard.component.ts @@ -0,0 +1,218 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { User } from '../../models/user.model'; + +@Component({ + selector: 'app-finance-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+

Finance Dashboard

+ +
+ +
+

Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!

+

Finance User

+
+ +
+
+
💰
+

Revenue

+

Track revenue and income

+
+ $125,430 + This Month +
+
+ +
+
💸
+

Expenses

+

Monitor expenses and costs

+
+ $45,200 + This Month +
+
+ +
+
📈
+

Profit Margin

+

View profit margins and trends

+
+ 64% + Current Margin +
+
+ +
+
🧾
+

Invoices

+

Manage invoices and billing

+
+ 23 + Pending Invoices +
+
+ +
+
💳
+

Payments

+

Process and track payments

+
+ 15 + Pending Payments +
+
+ +
+
📊
+

Financial Reports

+

Generate financial reports

+
+ 8 + Reports Generated +
+
+
+
+ `, + styles: [` + .dashboard { + padding: 20px; + max-width: 1400px; + margin: 0 auto; + } + + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .dashboard-header h1 { + margin: 0; + color: #333; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-danger { + background-color: #dc3545; + color: white; + } + + .btn-danger:hover { + background-color: #c82333; + } + + .welcome-section { + color: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + } + + .welcome-section.finance { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + } + + .welcome-section h2 { + margin: 0 0 10px 0; + } + + .role-badge { + display: inline-block; + padding: 5px 15px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + background-color: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + } + + .card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; + } + + .card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + } + + .card-icon { + font-size: 48px; + margin-bottom: 15px; + } + + .card h3 { + margin: 0 0 10px 0; + color: #333; + } + + .card p { + color: #666; + margin: 0 0 20px 0; + } + + .card-stats { + display: flex; + flex-direction: column; + padding-top: 15px; + border-top: 1px solid #eee; + } + + .stat-number { + font-size: 28px; + font-weight: 700; + color: #f5576c; + margin-bottom: 5px; + } + + .stat-label { + font-size: 12px; + color: #999; + text-transform: uppercase; + letter-spacing: 1px; + } + `] +}) +export class FinanceDashboardComponent { + user = signal(null); + + constructor( + private authService: AuthService, + private router: Router + ) { + this.user.set(this.authService.getCurrentUser()); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/src/app/components/dashboards/it-support-dashboard.component.ts b/src/app/components/dashboards/it-support-dashboard.component.ts new file mode 100644 index 0000000..0cdefdc --- /dev/null +++ b/src/app/components/dashboards/it-support-dashboard.component.ts @@ -0,0 +1,218 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { User } from '../../models/user.model'; + +@Component({ + selector: 'app-it-support-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+

IT Support Dashboard

+ +
+ +
+

Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!

+

IT Support User

+
+ +
+
+
🎫
+

Support Tickets

+

Manage support requests

+
+ 18 + Open Tickets +
+
+ +
+
💻
+

System Status

+

Monitor system health

+
+ 99.9% + Uptime +
+
+ +
+
🔧
+

Maintenance

+

Schedule and track maintenance

+
+ 3 + Scheduled Tasks +
+
+ +
+
🖥️
+

Hardware

+

Manage hardware inventory

+
+ 85 + Active Devices +
+
+ +
+
📦
+

Software Licenses

+

Track software licenses

+
+ 42 + Active Licenses +
+
+ +
+
📞
+

Help Desk

+

User support and assistance

+
+ 127 + Calls This Week +
+
+
+
+ `, + styles: [` + .dashboard { + padding: 20px; + max-width: 1400px; + margin: 0 auto; + } + + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .dashboard-header h1 { + margin: 0; + color: #333; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-danger { + background-color: #dc3545; + color: white; + } + + .btn-danger:hover { + background-color: #c82333; + } + + .welcome-section { + color: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + } + + .welcome-section.it-support { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + } + + .welcome-section h2 { + margin: 0 0 10px 0; + } + + .role-badge { + display: inline-block; + padding: 5px 15px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + background-color: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + } + + .card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; + } + + .card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + } + + .card-icon { + font-size: 48px; + margin-bottom: 15px; + } + + .card h3 { + margin: 0 0 10px 0; + color: #333; + } + + .card p { + color: #666; + margin: 0 0 20px 0; + } + + .card-stats { + display: flex; + flex-direction: column; + padding-top: 15px; + border-top: 1px solid #eee; + } + + .stat-number { + font-size: 28px; + font-weight: 700; + color: #4facfe; + margin-bottom: 5px; + } + + .stat-label { + font-size: 12px; + color: #999; + text-transform: uppercase; + letter-spacing: 1px; + } + `] +}) +export class ITSupportDashboardComponent { + user = signal(null); + + constructor( + private authService: AuthService, + private router: Router + ) { + this.user.set(this.authService.getCurrentUser()); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/src/app/components/dashboards/user-dashboard.component.ts b/src/app/components/dashboards/user-dashboard.component.ts new file mode 100644 index 0000000..814cec8 --- /dev/null +++ b/src/app/components/dashboards/user-dashboard.component.ts @@ -0,0 +1,218 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { User } from '../../models/user.model'; + +@Component({ + selector: 'app-user-dashboard', + standalone: true, + imports: [CommonModule], + template: ` +
+
+

User Dashboard

+ +
+ +
+

Welcome, {{ user()?.firstName }} {{ user()?.lastName }}!

+

User

+
+ +
+
+
📊
+

My Data

+

View your personal data and statistics

+
+ 24 + Records +
+
+ +
+
📝
+

My Submissions

+

Track your submissions

+
+ 8 + Pending Review +
+
+ +
+
📧
+

Messages

+

View your messages and notifications

+
+ 5 + Unread Messages +
+
+ +
+
⚙️
+

Settings

+

Manage your account settings

+
+ + Profile Complete +
+
+ +
+
📅
+

Calendar

+

View your schedule and events

+
+ 3 + Events Today +
+
+ +
+
📄
+

Documents

+

Access your documents and files

+
+ 42 + Total Documents +
+
+
+
+ `, + styles: [` + .dashboard { + padding: 20px; + max-width: 1400px; + margin: 0 auto; + } + + .dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + } + + .dashboard-header h1 { + margin: 0; + color: #333; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-danger { + background-color: #dc3545; + color: white; + } + + .btn-danger:hover { + background-color: #c82333; + } + + .welcome-section { + color: white; + padding: 30px; + border-radius: 10px; + margin-bottom: 30px; + } + + .welcome-section.user { + background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); + } + + .welcome-section h2 { + margin: 0 0 10px 0; + } + + .role-badge { + display: inline-block; + padding: 5px 15px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + background-color: rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + .dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + } + + .card { + background: white; + border-radius: 10px; + padding: 25px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; + } + + .card:hover { + transform: translateY(-5px); + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15); + } + + .card-icon { + font-size: 48px; + margin-bottom: 15px; + } + + .card h3 { + margin: 0 0 10px 0; + color: #333; + } + + .card p { + color: #666; + margin: 0 0 20px 0; + } + + .card-stats { + display: flex; + flex-direction: column; + padding-top: 15px; + border-top: 1px solid #eee; + } + + .stat-number { + font-size: 28px; + font-weight: 700; + color: #fa709a; + margin-bottom: 5px; + } + + .stat-label { + font-size: 12px; + color: #999; + text-transform: uppercase; + letter-spacing: 1px; + } + `] +}) +export class UserDashboardComponent { + user = signal(null); + + constructor( + private authService: AuthService, + private router: Router + ) { + this.user.set(this.authService.getCurrentUser()); + } + + logout(): void { + this.authService.logout(); + } +} diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts new file mode 100644 index 0000000..e633178 --- /dev/null +++ b/src/app/components/login/login.component.ts @@ -0,0 +1,283 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink], + template: ` +
+
+
+

BioStream

+

Login

+
+ + @if (errorMessage()) { +
+ {{ errorMessage() }} +
+ } + + @if (successMessage()) { +
+ {{ successMessage() }} +
+ } + +
+
+ + +
+ +
+ + +
+ + +
+ + + +
+

Demo Accounts:

+
    +
  • Admin: admin@biostream.com / admin123
  • +
  • Field Agent: field@biostream.com / field123
  • +
  • Finance: finance@biostream.com / finance123
  • +
  • IT Support: it@biostream.com / it123
  • +
  • User: user@biostream.com / user123
  • +
+
+
+
+ `, + styles: [` + .auth-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + } + + .auth-card { + background: white; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 40px; + max-width: 450px; + width: 100%; + } + + .auth-header { + text-align: center; + margin-bottom: 30px; + } + + .auth-header h1 { + color: #667eea; + margin: 0 0 10px 0; + font-size: 32px; + } + + .auth-header h2 { + color: #333; + margin: 0; + font-size: 24px; + font-weight: 500; + } + + .alert { + padding: 12px; + border-radius: 5px; + margin-bottom: 20px; + } + + .alert-error { + background-color: #fee; + color: #c33; + border: 1px solid #fcc; + } + + .alert-success { + background-color: #efe; + color: #3c3; + border: 1px solid #cfc; + } + + .form-group { + margin-bottom: 20px; + } + + .form-group label { + display: block; + margin-bottom: 5px; + color: #555; + font-weight: 500; + } + + .form-group input { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 14px; + box-sizing: border-box; + } + + .form-group input:focus { + outline: none; + border-color: #667eea; + } + + .form-group input:disabled { + background-color: #f5f5f5; + cursor: not-allowed; + } + + .btn { + padding: 12px 24px; + border: none; + border-radius: 5px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-primary { + background-color: #667eea; + color: white; + } + + .btn-primary:hover:not(:disabled) { + background-color: #5568d3; + } + + .btn-primary:disabled { + background-color: #ccc; + cursor: not-allowed; + } + + .btn-block { + width: 100%; + } + + .auth-footer { + text-align: center; + margin-top: 20px; + } + + .auth-footer p { + color: #666; + margin: 0; + } + + .auth-footer a { + color: #667eea; + text-decoration: none; + font-weight: 500; + } + + .auth-footer a:hover { + text-decoration: underline; + } + + .demo-credentials { + margin-top: 30px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 5px; + border: 1px solid #e0e0e0; + } + + .demo-credentials h4 { + margin: 0 0 10px 0; + color: #555; + font-size: 14px; + } + + .demo-credentials ul { + margin: 0; + padding-left: 20px; + font-size: 12px; + color: #666; + } + + .demo-credentials li { + margin-bottom: 5px; + } + `] +}) +export class LoginComponent { + email = ''; + password = ''; + loading = signal(false); + errorMessage = signal(''); + successMessage = signal(''); + + constructor( + private authService: AuthService, + private router: Router + ) {} + + onSubmit(): void { + this.loading.set(true); + this.errorMessage.set(''); + this.successMessage.set(''); + + this.authService.login({ email: this.email, password: this.password }).subscribe({ + next: (response) => { + this.loading.set(false); + if (response.requiresOTP) { + this.successMessage.set('OTP sent to your email. Check console for demo OTP.'); + setTimeout(() => { + this.router.navigate(['/verify-otp'], { queryParams: { email: this.email } }); + }, 2000); + } else { + this.router.navigate([this.authService.getRoleDashboardRoute(response.user.role)]); + } + }, + error: (error) => { + this.loading.set(false); + this.errorMessage.set(error.message || 'Login failed. Please try again.'); + } + }); + } +} diff --git a/src/app/components/register/register.component.ts b/src/app/components/register/register.component.ts new file mode 100644 index 0000000..c12b06a --- /dev/null +++ b/src/app/components/register/register.component.ts @@ -0,0 +1,323 @@ +import { Component, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { EmailService } from '../../services/email.service'; +import { UserRole } from '../../models/user.model'; + +@Component({ + selector: 'app-register', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink], + template: ` +
+
+
+

BioStream

+

Register

+
+ + @if (errorMessage()) { +
+ {{ errorMessage() }} +
+ } + + @if (successMessage()) { +
+ {{ successMessage() }} +
+ } + +
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+
+ `, + styles: [` + .auth-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + } + + .auth-card { + background: white; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 40px; + max-width: 500px; + width: 100%; + } + + .auth-header { + text-align: center; + margin-bottom: 30px; + } + + .auth-header h1 { + color: #667eea; + margin: 0 0 10px 0; + font-size: 32px; + } + + .auth-header h2 { + color: #333; + margin: 0; + font-size: 24px; + font-weight: 500; + } + + .alert { + padding: 12px; + border-radius: 5px; + margin-bottom: 20px; + } + + .alert-error { + background-color: #fee; + color: #c33; + border: 1px solid #fcc; + } + + .alert-success { + background-color: #efe; + color: #3c3; + border: 1px solid #cfc; + } + + .form-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + } + + .form-group { + margin-bottom: 20px; + } + + .form-group label { + display: block; + margin-bottom: 5px; + color: #555; + font-weight: 500; + } + + .form-group input, + .form-group select { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 14px; + box-sizing: border-box; + } + + .form-group input:focus, + .form-group select:focus { + outline: none; + border-color: #667eea; + } + + .form-group input:disabled, + .form-group select:disabled { + background-color: #f5f5f5; + cursor: not-allowed; + } + + .btn { + padding: 12px 24px; + border: none; + border-radius: 5px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-primary { + background-color: #667eea; + color: white; + } + + .btn-primary:hover:not(:disabled) { + background-color: #5568d3; + } + + .btn-primary:disabled { + background-color: #ccc; + cursor: not-allowed; + } + + .btn-block { + width: 100%; + } + + .auth-footer { + text-align: center; + margin-top: 20px; + } + + .auth-footer p { + color: #666; + margin: 0; + } + + .auth-footer a { + color: #667eea; + text-decoration: none; + font-weight: 500; + } + + .auth-footer a:hover { + text-decoration: underline; + } + `] +}) +export class RegisterComponent { + firstName = ''; + lastName = ''; + email = ''; + password = ''; + role = ''; + loading = signal(false); + errorMessage = signal(''); + successMessage = signal(''); + + roles = [ + { value: UserRole.User, label: 'User' }, + { value: UserRole.Administrator, label: 'Administrator' }, + { value: UserRole.FieldAgent, label: 'Field Agent' }, + { value: UserRole.FinanceUser, label: 'Finance User' }, + { value: UserRole.ITSupportUser, label: 'IT Support User' } + ]; + + constructor( + private authService: AuthService, + private emailService: EmailService, + private router: Router + ) {} + + onSubmit(): void { + this.loading.set(true); + this.errorMessage.set(''); + this.successMessage.set(''); + + this.authService.register({ + email: this.email, + password: this.password, + firstName: this.firstName, + lastName: this.lastName, + role: this.role as UserRole + }).subscribe({ + next: (response) => { + this.loading.set(false); + this.successMessage.set(response.message); + + // Simulate sending verification email + const verificationLink = `${window.location.origin}/verify-email?token=${btoa(this.email + ':' + Date.now())}`; + this.emailService.sendVerificationEmail(this.email, verificationLink).subscribe(); + + // Redirect to login after 3 seconds + setTimeout(() => { + this.router.navigate(['/login']); + }, 3000); + }, + error: (error) => { + this.loading.set(false); + this.errorMessage.set(error.message || 'Registration failed. Please try again.'); + } + }); + } +} diff --git a/src/app/components/verify-email/verify-email.component.ts b/src/app/components/verify-email/verify-email.component.ts new file mode 100644 index 0000000..3cd12ab --- /dev/null +++ b/src/app/components/verify-email/verify-email.component.ts @@ -0,0 +1,203 @@ +import { Component, signal, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Router, ActivatedRoute, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; +import { EmailService } from '../../services/email.service'; + +@Component({ + selector: 'app-verify-email', + standalone: true, + imports: [CommonModule, RouterLink], + template: ` +
+
+
+

BioStream

+

Email Verification

+
+ + @if (loading()) { +
+
+

Verifying your email...

+
+ } + + @if (errorMessage()) { +
+

Verification Failed

+

{{ errorMessage() }}

+
+ } + + @if (successMessage()) { +
+

✓ Email Verified!

+

{{ successMessage() }}

+
+ } + + +
+
+ `, + styles: [` + .auth-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + } + + .auth-card { + background: white; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 40px; + max-width: 450px; + width: 100%; + } + + .auth-header { + text-align: center; + margin-bottom: 30px; + } + + .auth-header h1 { + color: #667eea; + margin: 0 0 10px 0; + font-size: 32px; + } + + .auth-header h2 { + color: #333; + margin: 0; + font-size: 24px; + font-weight: 500; + } + + .loading-spinner { + text-align: center; + padding: 40px 0; + } + + .spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #667eea; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; + margin: 0 auto 20px; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + .loading-spinner p { + color: #666; + margin: 0; + } + + .alert { + padding: 20px; + border-radius: 5px; + margin-bottom: 20px; + text-align: center; + } + + .alert h3 { + margin: 0 0 10px 0; + font-size: 20px; + } + + .alert p { + margin: 0; + } + + .alert-error { + background-color: #fee; + color: #c33; + border: 1px solid #fcc; + } + + .alert-success { + background-color: #efe; + color: #3c3; + border: 1px solid #cfc; + } + + .auth-footer { + text-align: center; + margin-top: 20px; + } + + .btn { + display: inline-block; + padding: 12px 24px; + border: none; + border-radius: 5px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: all 0.3s; + } + + .btn-primary { + background-color: #667eea; + color: white; + } + + .btn-primary:hover { + background-color: #5568d3; + } + `] +}) +export class VerifyEmailComponent implements OnInit { + loading = signal(true); + errorMessage = signal(''); + successMessage = signal(''); + + constructor( + private authService: AuthService, + private emailService: EmailService, + private router: Router, + private route: ActivatedRoute + ) {} + + ngOnInit(): void { + const token = this.route.snapshot.queryParams['token']; + if (!token) { + this.loading.set(false); + this.errorMessage.set('Invalid verification link'); + return; + } + + this.verifyEmail(token); + } + + private verifyEmail(token: string): void { + this.authService.verifyEmail({ token }).subscribe({ + next: (response) => { + this.loading.set(false); + this.successMessage.set(response.message); + + // Auto-redirect to login after 3 seconds + setTimeout(() => { + this.router.navigate(['/login']); + }, 3000); + }, + error: (error) => { + this.loading.set(false); + this.errorMessage.set(error.message || 'Email verification failed. The link may be invalid or expired.'); + } + }); + } +} diff --git a/src/app/components/verify-otp/verify-otp.component.ts b/src/app/components/verify-otp/verify-otp.component.ts new file mode 100644 index 0000000..1c880ad --- /dev/null +++ b/src/app/components/verify-otp/verify-otp.component.ts @@ -0,0 +1,332 @@ +import { Component, signal, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Router, ActivatedRoute, RouterLink } from '@angular/router'; +import { AuthService } from '../../services/auth.service'; + +@Component({ + selector: 'app-verify-otp', + standalone: true, + imports: [CommonModule, FormsModule, RouterLink], + template: ` +
+
+
+

BioStream

+

Verify OTP

+

Enter the 6-digit code sent to {{ email() }}

+
+ + @if (errorMessage()) { +
+ {{ errorMessage() }} +
+ } + + @if (successMessage()) { +
+ {{ successMessage() }} +
+ } + +
+
+ + +
+ + +
+ +
+

Didn't receive the code?

+ +
+ + +
+
+ `, + styles: [` + .auth-container { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 20px; + } + + .auth-card { + background: white; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + padding: 40px; + max-width: 450px; + width: 100%; + } + + .auth-header { + text-align: center; + margin-bottom: 30px; + } + + .auth-header h1 { + color: #667eea; + margin: 0 0 10px 0; + font-size: 32px; + } + + .auth-header h2 { + color: #333; + margin: 0 0 10px 0; + font-size: 24px; + font-weight: 500; + } + + .subtitle { + color: #666; + font-size: 14px; + margin: 0; + } + + .alert { + padding: 12px; + border-radius: 5px; + margin-bottom: 20px; + } + + .alert-error { + background-color: #fee; + color: #c33; + border: 1px solid #fcc; + } + + .alert-success { + background-color: #efe; + color: #3c3; + border: 1px solid #cfc; + } + + .form-group { + margin-bottom: 20px; + } + + .form-group label { + display: block; + margin-bottom: 5px; + color: #555; + font-weight: 500; + } + + .otp-input { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 5px; + font-size: 24px; + text-align: center; + letter-spacing: 10px; + font-weight: 600; + box-sizing: border-box; + } + + .otp-input:focus { + outline: none; + border-color: #667eea; + } + + .otp-input:disabled { + background-color: #f5f5f5; + cursor: not-allowed; + } + + .btn { + padding: 12px 24px; + border: none; + border-radius: 5px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + } + + .btn-primary { + background-color: #667eea; + color: white; + } + + .btn-primary:hover:not(:disabled) { + background-color: #5568d3; + } + + .btn-primary:disabled { + background-color: #ccc; + cursor: not-allowed; + } + + .btn-block { + width: 100%; + } + + .btn-link { + background: none; + color: #667eea; + padding: 0; + font-size: 14px; + } + + .btn-link:hover:not(:disabled) { + text-decoration: underline; + } + + .btn-link:disabled { + color: #999; + cursor: not-allowed; + } + + .resend-section { + text-align: center; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #eee; + } + + .resend-section p { + color: #666; + margin: 0 0 10px 0; + font-size: 14px; + } + + .auth-footer { + text-align: center; + margin-top: 20px; + } + + .auth-footer p { + color: #666; + margin: 0; + } + + .auth-footer a { + color: #667eea; + text-decoration: none; + font-weight: 500; + } + + .auth-footer a:hover { + text-decoration: underline; + } + `] +}) +export class VerifyOtpComponent implements OnInit { + email = signal(''); + otp = ''; + loading = signal(false); + resendLoading = signal(false); + resendCooldown = signal(0); + errorMessage = signal(''); + successMessage = signal(''); + + constructor( + private authService: AuthService, + private router: Router, + private route: ActivatedRoute + ) {} + + ngOnInit(): void { + const email = this.route.snapshot.queryParams['email']; + if (!email) { + this.router.navigate(['/login']); + return; + } + this.email.set(email); + } + + onSubmit(): void { + this.loading.set(true); + this.errorMessage.set(''); + this.successMessage.set(''); + + this.authService.verifyOTP({ email: this.email(), otp: this.otp }).subscribe({ + next: (response) => { + this.loading.set(false); + this.successMessage.set('OTP verified successfully! Redirecting...'); + setTimeout(() => { + this.router.navigate([this.authService.getRoleDashboardRoute(response.user.role)]); + }, 1500); + }, + error: (error) => { + this.loading.set(false); + this.errorMessage.set(error.message || 'OTP verification failed. Please try again.'); + } + }); + } + + resendOTP(): void { + this.resendLoading.set(true); + this.errorMessage.set(''); + this.successMessage.set(''); + + this.authService.resendOTP(this.email()).subscribe({ + next: (response) => { + this.resendLoading.set(false); + this.successMessage.set(response.message); + this.startCooldown(); + }, + error: (error) => { + this.resendLoading.set(false); + this.errorMessage.set(error.message || 'Failed to resend OTP. Please try again.'); + } + }); + } + + private startCooldown(): void { + this.resendCooldown.set(60); + const interval = setInterval(() => { + const current = this.resendCooldown(); + if (current <= 1) { + clearInterval(interval); + this.resendCooldown.set(0); + } else { + this.resendCooldown.set(current - 1); + } + }, 1000); + } +} diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts new file mode 100644 index 0000000..f875dbf --- /dev/null +++ b/src/app/guards/auth.guard.ts @@ -0,0 +1,77 @@ +import { inject } from '@angular/core'; +import { Router, CanActivateFn, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { UserRole } from '../models/user.model'; + +export const authGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot +) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (!authService.isAuthenticated()) { + router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); + return false; + } + + // Check if route requires specific roles + const requiredRoles = route.data['roles'] as UserRole[] | undefined; + + if (requiredRoles && requiredRoles.length > 0) { + if (!authService.hasAnyRole(requiredRoles)) { + // Redirect to user's appropriate dashboard + const user = authService.getCurrentUser(); + if (user) { + router.navigate([authService.getRoleDashboardRoute(user.role)]); + } else { + router.navigate(['/unauthorized']); + } + return false; + } + } + + return true; +}; + +export const guestGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot +) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (authService.isAuthenticated()) { + const user = authService.getCurrentUser(); + if (user) { + router.navigate([authService.getRoleDashboardRoute(user.role)]); + } + return false; + } + + return true; +}; + +export const roleGuard = (roles: UserRole[]): CanActivateFn => { + return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const authService = inject(AuthService); + const router = inject(Router); + + if (!authService.isAuthenticated()) { + router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); + return false; + } + + if (!authService.hasAnyRole(roles)) { + const user = authService.getCurrentUser(); + if (user) { + router.navigate([authService.getRoleDashboardRoute(user.role)]); + } else { + router.navigate(['/unauthorized']); + } + return false; + } + + return true; + }; +}; diff --git a/src/app/models/user.model.ts b/src/app/models/user.model.ts new file mode 100644 index 0000000..d055ee3 --- /dev/null +++ b/src/app/models/user.model.ts @@ -0,0 +1,55 @@ +export enum UserRole { + User = 'User', + Administrator = 'Administrator', + FieldAgent = 'FieldAgent', + FinanceUser = 'FinanceUser', + ITSupportUser = 'ITSupportUser' +} + +export interface User { + id: string; + email: string; + firstName: string; + lastName: string; + role: UserRole; + isEmailVerified: boolean; + isActive: boolean; + createdAt: Date; + lastLogin?: Date; +} + +export interface RegisterRequest { + email: string; + password: string; + firstName: string; + lastName: string; + role: UserRole; +} + +export interface LoginRequest { + email: string; + password: string; +} + +export interface LoginResponse { + user: User; + token: string; + requiresOTP: boolean; +} + +export interface OTPVerificationRequest { + email: string; + otp: string; +} + +export interface EmailVerificationRequest { + token: string; +} + +export interface AuthState { + user: User | null; + token: string | null; + isAuthenticated: boolean; + requiresOTP: boolean; + pendingEmail?: string; +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..227de21 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,376 @@ +import { Injectable, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { Observable, of, throwError, delay } from 'rxjs'; +import { + User, + UserRole, + RegisterRequest, + LoginRequest, + LoginResponse, + OTPVerificationRequest, + EmailVerificationRequest, + AuthState +} from '../models/user.model'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private authState = signal({ + user: null, + token: null, + isAuthenticated: false, + requiresOTP: false + }); + + // Mock OTP storage (in production, this would be server-side) + private otpStore = new Map(); + + // Mock user database + private users: Map = new Map(); + + constructor(private router: Router) { + this.loadAuthState(); + this.initializeMockUsers(); + } + + private initializeMockUsers() { + // Create default users for each role + const defaultUsers = [ + { + id: '1', + email: 'admin@biostream.com', + password: 'admin123', + firstName: 'Admin', + lastName: 'User', + role: UserRole.Administrator, + isEmailVerified: true, + isActive: true, + createdAt: new Date() + }, + { + id: '2', + email: 'field@biostream.com', + password: 'field123', + firstName: 'Field', + lastName: 'Agent', + role: UserRole.FieldAgent, + isEmailVerified: true, + isActive: true, + createdAt: new Date() + }, + { + id: '3', + email: 'finance@biostream.com', + password: 'finance123', + firstName: 'Finance', + lastName: 'User', + role: UserRole.FinanceUser, + isEmailVerified: true, + isActive: true, + createdAt: new Date() + }, + { + id: '4', + email: 'it@biostream.com', + password: 'it123', + firstName: 'IT', + lastName: 'Support', + role: UserRole.ITSupportUser, + isEmailVerified: true, + isActive: true, + createdAt: new Date() + }, + { + id: '5', + email: 'user@biostream.com', + password: 'user123', + firstName: 'Regular', + lastName: 'User', + role: UserRole.User, + isEmailVerified: true, + isActive: true, + createdAt: new Date() + } + ]; + + defaultUsers.forEach(user => { + this.users.set(user.email, user); + }); + } + + getAuthState() { + return this.authState.asReadonly(); + } + + getCurrentUser(): User | null { + return this.authState().user; + } + + isAuthenticated(): boolean { + return this.authState().isAuthenticated; + } + + hasRole(role: UserRole): boolean { + return this.authState().user?.role === role; + } + + hasAnyRole(roles: UserRole[]): boolean { + const userRole = this.authState().user?.role; + return userRole ? roles.includes(userRole) : false; + } + + register(request: RegisterRequest): Observable<{ message: string; email: string }> { + // Simulate API delay + return new Observable(observer => { + setTimeout(() => { + // Check if user already exists + if (this.users.has(request.email)) { + observer.error({ message: 'User already exists' }); + return; + } + + // Create new user + const newUser: User & { password: string } = { + id: Math.random().toString(36).substring(7), + email: request.email, + password: request.password, + firstName: request.firstName, + lastName: request.lastName, + role: request.role, + isEmailVerified: false, + isActive: false, + createdAt: new Date() + }; + + this.users.set(request.email, newUser); + + // Generate verification token (in production, this would be a proper JWT) + const verificationToken = btoa(request.email + ':' + Date.now()); + + // Store token for verification (in production, this would be server-side) + localStorage.setItem(`verification_${verificationToken}`, request.email); + + observer.next({ + message: 'Registration successful. Please check your email for verification link.', + email: request.email + }); + observer.complete(); + }, 1000); + }); + } + + login(request: LoginRequest): Observable { + return new Observable(observer => { + setTimeout(() => { + const user = this.users.get(request.email); + + if (!user || user.password !== request.password) { + observer.error({ message: 'Invalid email or password' }); + return; + } + + if (!user.isEmailVerified) { + observer.error({ message: 'Please verify your email before logging in' }); + return; + } + + if (!user.isActive) { + observer.error({ message: 'Your account is not active. Please contact support.' }); + return; + } + + // Generate OTP + const otp = this.generateOTP(); + const expires = Date.now() + 5 * 60 * 1000; // 5 minutes + this.otpStore.set(request.email, { otp, expires }); + + console.log(`OTP for ${request.email}: ${otp}`); // In production, send via email + + // Return response requiring OTP + const { password, ...userWithoutPassword } = user; + observer.next({ + user: userWithoutPassword, + token: '', + requiresOTP: true + }); + + // Update auth state for OTP verification + this.authState.set({ + user: null, + token: null, + isAuthenticated: false, + requiresOTP: true, + pendingEmail: request.email + }); + + observer.complete(); + }, 1000); + }); + } + + verifyOTP(request: OTPVerificationRequest): Observable { + return new Observable(observer => { + setTimeout(() => { + const storedOTP = this.otpStore.get(request.email); + + if (!storedOTP) { + observer.error({ message: 'OTP not found. Please login again.' }); + return; + } + + if (Date.now() > storedOTP.expires) { + this.otpStore.delete(request.email); + observer.error({ message: 'OTP has expired. Please login again.' }); + return; + } + + if (storedOTP.otp !== request.otp) { + observer.error({ message: 'Invalid OTP' }); + return; + } + + // OTP verified successfully + this.otpStore.delete(request.email); + const user = this.users.get(request.email)!; + + // Update last login + user.lastLogin = new Date(); + + // Generate token (in production, this would be a proper JWT) + const token = btoa(request.email + ':' + Date.now()); + + const { password, ...userWithoutPassword } = user; + + // Update auth state + this.authState.set({ + user: userWithoutPassword, + token, + isAuthenticated: true, + requiresOTP: false + }); + + this.saveAuthState(); + + observer.next({ + user: userWithoutPassword, + token, + requiresOTP: false + }); + + observer.complete(); + }, 1000); + }); + } + + verifyEmail(request: EmailVerificationRequest): Observable<{ message: string }> { + return new Observable(observer => { + setTimeout(() => { + const email = localStorage.getItem(`verification_${request.token}`); + + if (!email) { + observer.error({ message: 'Invalid or expired verification token' }); + return; + } + + const user = this.users.get(email); + + if (!user) { + observer.error({ message: 'User not found' }); + return; + } + + // Mark email as verified and activate user + user.isEmailVerified = true; + user.isActive = true; + + // Clean up token + localStorage.removeItem(`verification_${request.token}`); + + observer.next({ message: 'Email verified successfully. You can now login.' }); + observer.complete(); + }, 1000); + }); + } + + resendOTP(email: string): Observable<{ message: string }> { + return new Observable(observer => { + setTimeout(() => { + const user = this.users.get(email); + + if (!user) { + observer.error({ message: 'User not found' }); + return; + } + + // Generate new OTP + const otp = this.generateOTP(); + const expires = Date.now() + 5 * 60 * 1000; + this.otpStore.set(email, { otp, expires }); + + console.log(`New OTP for ${email}: ${otp}`); // In production, send via email + + observer.next({ message: 'OTP has been resent to your email' }); + observer.complete(); + }, 1000); + }); + } + + logout(): void { + this.authState.set({ + user: null, + token: null, + isAuthenticated: false, + requiresOTP: false + }); + this.clearAuthState(); + this.router.navigate(['/login']); + } + + private generateOTP(): string { + return Math.floor(100000 + Math.random() * 900000).toString(); + } + + private saveAuthState(): void { + const state = this.authState(); + if (state.isAuthenticated && state.user && state.token) { + localStorage.setItem('auth_user', JSON.stringify(state.user)); + localStorage.setItem('auth_token', state.token); + } + } + + private loadAuthState(): void { + const userStr = localStorage.getItem('auth_user'); + const token = localStorage.getItem('auth_token'); + + if (userStr && token) { + try { + const user = JSON.parse(userStr); + this.authState.set({ + user, + token, + isAuthenticated: true, + requiresOTP: false + }); + } catch (e) { + this.clearAuthState(); + } + } + } + + private clearAuthState(): void { + localStorage.removeItem('auth_user'); + localStorage.removeItem('auth_token'); + } + + getRoleDashboardRoute(role: UserRole): string { + const routes: Record = { + [UserRole.Administrator]: '/admin/dashboard', + [UserRole.FieldAgent]: '/field-agent/dashboard', + [UserRole.FinanceUser]: '/finance/dashboard', + [UserRole.ITSupportUser]: '/it-support/dashboard', + [UserRole.User]: '/user/dashboard' + }; + return routes[role]; + } +} diff --git a/src/app/services/email.service.ts b/src/app/services/email.service.ts new file mode 100644 index 0000000..01938eb --- /dev/null +++ b/src/app/services/email.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@angular/core'; +import { Observable, of, delay } from 'rxjs'; + +export interface EmailOptions { + to: string; + subject: string; + body: string; + isHTML?: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class EmailService { + // In production, this would integrate with a real email service (SendGrid, AWS SES, etc.) + + sendEmail(options: EmailOptions): Observable<{ success: boolean; message: string }> { + console.log('=== EMAIL SENT ==='); + console.log(`To: ${options.to}`); + console.log(`Subject: ${options.subject}`); + console.log(`Body:\n${options.body}`); + console.log('=================='); + + // Simulate email sending delay + return of({ + success: true, + message: 'Email sent successfully' + }).pipe(delay(500)); + } + + sendOTPEmail(email: string, otp: string): Observable<{ success: boolean; message: string }> { + const body = ` +

BioStream - Your OTP Code

+

Hello,

+

Your One-Time Password (OTP) for logging into BioStream is:

+

${otp}

+

This code will expire in 5 minutes.

+

If you didn't request this code, please ignore this email.

+
+

Best regards,
BioStream Team

+ `; + + return this.sendEmail({ + to: email, + subject: 'BioStream - Your OTP Code', + body, + isHTML: true + }); + } + + sendVerificationEmail(email: string, verificationLink: string): Observable<{ success: boolean; message: string }> { + const body = ` +

BioStream - Email Verification

+

Hello,

+

Thank you for registering with BioStream!

+

Please click the link below to verify your email address:

+

Verify Email

+

Or copy and paste this link into your browser:

+

${verificationLink}

+

This link will expire in 24 hours.

+

If you didn't create an account, please ignore this email.

+
+

Best regards,
BioStream Team

+ `; + + return this.sendEmail({ + to: email, + subject: 'BioStream - Verify Your Email', + body, + isHTML: true + }); + } + + sendWelcomeEmail(email: string, firstName: string, role: string): Observable<{ success: boolean; message: string }> { + const body = ` +

Welcome to BioStream!

+

Hello ${firstName},

+

Your email has been verified successfully!

+

You are now registered as a ${role}.

+

You can now log in to your account and start using BioStream.

+

Login Now

+
+

Best regards,
BioStream Team

+ `; + + return this.sendEmail({ + to: email, + subject: 'Welcome to BioStream!', + body, + isHTML: true + }); + } + + sendPasswordResetEmail(email: string, resetLink: string): Observable<{ success: boolean; message: string }> { + const body = ` +

BioStream - Password Reset

+

Hello,

+

We received a request to reset your password.

+

Click the link below to reset your password:

+

Reset Password

+

Or copy and paste this link into your browser:

+

${resetLink}

+

This link will expire in 1 hour.

+

If you didn't request a password reset, please ignore this email.

+
+

Best regards,
BioStream Team

+ `; + + return this.sendEmail({ + to: email, + subject: 'BioStream - Password Reset', + body, + isHTML: true + }); + } +} diff --git a/src/styles.css b/src/styles.css index 90d4ee0..cdaa2ae 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1 +1,14 @@ /* You can add global styles to this file, and also import other style files */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: #f5f5f5; + color: #333; + line-height: 1.6; +}