diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml deleted file mode 100644 index 73f825a..0000000 --- a/.github/workflows/build-check.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build Check - -on: - push: - branches-ignore: - - "dependabot/**" - pull_request: - branches: - - main - - v5.* - schedule: - # build runs every weekday at 4:15AM UTC - - cron: '15 4 * * *' - -env: - FORCE_COLOR: 2 - NODE: 24 - -jobs: - build: - strategy: - matrix: - platform: [ubuntu-latest, macOS-latest] - node-version: [24.x] - runs-on: ${{ matrix.platform }} - steps: - - name: Clone repository - uses: actions/checkout@v7 - - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: ${{ env.node-version }} - - - name: Install npm dependencies - run: npm ci - - - name: Install Playwright Browsers - run: npx playwright install --with-deps --only-shell chromium - - - name: Run build - run: npm run build diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 163fd32..0000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,27 +0,0 @@ -# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. -# -# You can adjust the behavior by modifying this file. -# For more information, see: -# https://github.com/actions/stale -name: Mark stale issues and pull requests - -on: - schedule: - - cron: '34 23 * * 0' - -jobs: - stale: - - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - - steps: - - uses: actions/stale@v5 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions' - stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions' - stale-issue-label: 'no-issue-activity' - stale-pr-label: 'no-pr-activity' diff --git a/.gitignore b/.gitignore index c0d1f23..0fc8eb6 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ testem.log # System files .DS_Store Thumbs.db + +# Environment files +src/environments/environment.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index de1d46d..8e6b558 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -6,12 +6,14 @@ import { delay, filter, map, tap } from 'rxjs/operators'; import { ColorModeService } from '@coreui/angular'; import { IconSetService } from '@coreui/icons-angular'; +import { freeSet, brandSet, flagSet } from '@coreui/icons'; // Import all icon sets import { iconSubset } from './icons/icon-subset'; @Component({ selector: 'app-root', template: '', - imports: [RouterOutlet] + imports: [RouterOutlet], + providers: [IconSetService] // Add IconSetService as a provider }) export class AppComponent implements OnInit { title = 'CoreUI Angular Admin Template'; @@ -26,14 +28,21 @@ export class AppComponent implements OnInit { constructor() { this.#titleService.setTitle(this.title); - // iconSet singleton - this.#iconSetService.icons = { ...iconSubset }; + + // Register ALL icon sets globally + // This makes ALL icons available everywhere in the app + this.#iconSetService.icons = { + ...iconSubset, // Your custom icons + ...freeSet, // cil icons (including cilPhone, cilUser, cilLockLocked) + ...brandSet, // cib icons (Google, Facebook, Twitter, etc.) + ...flagSet // cif icons (Country flags) + }; + this.#colorModeService.localStorageItemName.set('coreui-free-angular-admin-template-theme-default'); this.#colorModeService.eventName.set('ColorSchemeChange'); } ngOnInit(): void { - this.#router.events.pipe( takeUntilDestroyed(this.#destroyRef) ).subscribe((evt) => { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 8196731..7062b4a 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -8,8 +8,10 @@ import { withRouterConfig, withViewTransitions } from '@angular/router'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { IconSetService } from '@coreui/icons-angular'; import { routes } from './app.routes'; +import { authInterceptor } from './interceptors/auth.interceptor'; export const appConfig: ApplicationConfig = { providers: [ @@ -26,7 +28,11 @@ export const appConfig: ApplicationConfig = { withHashLocation() ), IconSetService, - provideAnimationsAsync() + provideAnimationsAsync(), + + // HTTP Client with functional interceptor + provideHttpClient( + withInterceptors([authInterceptor]) + ) ] }; - diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 9bb43fe..99d10f9 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,4 +1,6 @@ import { Routes } from '@angular/router'; +import { AuthGuard } from './guards/auth.guard'; +import { RoleGuard } from './guards/role.guard'; export const routes: Routes = [ { @@ -12,6 +14,7 @@ export const routes: Routes = [ data: { title: 'Home' }, + canActivate: [AuthGuard], // Protect all child routes children: [ { path: 'dashboard', @@ -55,6 +58,20 @@ export const routes: Routes = [ } ] }, + { + path: 'login', + loadComponent: () => import('./views/pages/login/login.component').then(m => m.LoginComponent), + data: { + title: 'Login Page' + } + }, + { + path: 'register', + loadComponent: () => import('./views/pages/register/register.component').then(m => m.RegisterComponent), + data: { + title: 'Register Page' + } + }, { path: '404', loadComponent: () => import('./views/pages/page404/page404.component').then(m => m.Page404Component), @@ -69,19 +86,37 @@ export const routes: Routes = [ title: 'Page 500' } }, + // ============================== + // ADMIN & SUPER ADMIN ROUTES (Commented - Will be implemented later) + // ============================== + + // Protected Admin routes with role guard + // Uncomment when admin component is created + /* { - path: 'login', - loadComponent: () => import('./views/pages/login/login.component').then(m => m.LoginComponent), + path: 'admin', + loadComponent: () => import('./views/admin/admin.component').then(m => m.AdminComponent), + canActivate: [AuthGuard, RoleGuard], data: { - title: 'Login Page' + roles: ['Admin', 'SuperAdmin'], + title: 'Admin' } }, + */ + + // Protected SuperAdmin routes with role guard + // Uncomment when super-admin component is created + /* { - path: 'register', - loadComponent: () => import('./views/pages/register/register.component').then(m => m.RegisterComponent), + path: 'super-admin', + loadComponent: () => import('./views/super-admin/super-admin.component').then(m => m.SuperAdminComponent), + canActivate: [AuthGuard, RoleGuard], data: { - title: 'Register Page' + roles: ['SuperAdmin'], + title: 'Super Admin' } }, + */ + { path: '**', redirectTo: 'dashboard' } ]; diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts new file mode 100644 index 0000000..5904ca9 --- /dev/null +++ b/src/app/guards/auth.guard.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { map, take } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { + return this.authService.isAuthenticated$.pipe( + take(1), + map(isAuthenticated => { + if (isAuthenticated) { + return true; + } + + // Store the attempted URL for redirecting after login + return this.router.createUrlTree(['/login'], { + queryParams: { returnUrl: state.url } + }); + }) + ); + } +} diff --git a/src/app/guards/role.guard.ts b/src/app/guards/role.guard.ts new file mode 100644 index 0000000..a3b491e --- /dev/null +++ b/src/app/guards/role.guard.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { + CanActivate, + ActivatedRouteSnapshot, + RouterStateSnapshot, + Router, + UrlTree +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, take } from 'rxjs/operators'; +import { AuthService } from '../services/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class RoleGuard implements CanActivate { + constructor( + private authService: AuthService, + private router: Router + ) {} + + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable | Promise | boolean | UrlTree { + const requiredRoles = route.data['roles'] as string[]; + + if (!requiredRoles || requiredRoles.length === 0) { + return true; + } + + return this.authService.currentUser$.pipe( + take(1), + map(user => { + if (!user) { + return this.router.createUrlTree(['/login'], { + queryParams: { returnUrl: state.url } + }); + } + + const hasRequiredRole = requiredRoles.some(role => user.roles.includes(role)); + + if (hasRequiredRole) { + return true; + } + + // User doesn't have required role - redirect to unauthorized page or home + return this.router.createUrlTree(['/unauthorized']); + }) + ); + } +} diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..4404112 --- /dev/null +++ b/src/app/interceptors/auth.interceptor.ts @@ -0,0 +1,44 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { catchError } from 'rxjs/operators'; +import { throwError } from 'rxjs'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const authService = inject(AuthService); + const router = inject(Router); + + // Get the auth token from the service + const token = authService.getToken(); + + // Clone the request and add the authorization header + let authReq = req; + if (token) { + authReq = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + + // Handle the request and catch any errors + return next(authReq).pipe( + catchError((error) => { + // If we get a 401 Unauthorized, redirect to login + if (error.status === 401) { + authService.logout(); + router.navigate(['/login'], { + queryParams: { returnUrl: router.url } + }); + } + + // Handle 403 Forbidden + if (error.status === 403) { + console.error('You do not have permission to perform this action.'); + } + + return throwError(() => error); + }) + ); +}; diff --git a/src/app/models/auth.models.ts b/src/app/models/auth.models.ts new file mode 100644 index 0000000..1c8f77d --- /dev/null +++ b/src/app/models/auth.models.ts @@ -0,0 +1,75 @@ +// Request Models +export interface LoginRequest { + userName: string; + password: string; +} + +export interface RegisterRequest { + email: string; + userName: string; + fullName: string; + password: string; + phoneNumber?: string; +} + +export interface ResetPasswordRequest { + email: string; + token: string; + password: string; +} + +export interface ChangePasswordRequest { + currentPassword: string; + newPassword: string; + confirmNewPassword: string; +} + +export interface AssignRoleRequest { + userId: string; + role: string; +} + +// Response Models +export interface ServiceResponse { + success: boolean; + message: string; + data?: T; + errors?: string[]; +} + +export interface LoginResponse { + token: string; + user: UserDto; + returnUrl?: string; +} + +export interface UserDto { + id: string; + email: string; + userName: string; + fullName: string; + phoneNumber: string; + emailConfirmed: boolean; + roles: string[]; + createdAt: Date; + updatedAt?: Date; + deletedAt?: Date; +} + +export interface PaginatedResponse { + items: T[]; + totalCount: number; + pageNumber: number; + pageSize: number; + totalPages: number; + hasPrevious: boolean; + hasNext: boolean; +} + +export interface PaginationParameters { + pageNumber: number; + pageSize: number; + searchTerm?: string; + sortBy?: string; + sortDescending?: boolean; +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts new file mode 100644 index 0000000..c1d4729 --- /dev/null +++ b/src/app/services/auth.service.ts @@ -0,0 +1,361 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, BehaviorSubject, throwError } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; +import { Router } from '@angular/router'; +import { environment } from '../../environments/environment'; + +import { + LoginRequest, + RegisterRequest, + ResetPasswordRequest, + ChangePasswordRequest, + ServiceResponse, + LoginResponse, + UserDto +} from '../models/auth.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private apiUrl = environment.apiUrl; + private tokenKey = environment.auth.tokenKey; + private userKey = environment.auth.userKey; + + private currentUserSubject = new BehaviorSubject(null); + public currentUser$ = this.currentUserSubject.asObservable(); + + private isAuthenticatedSubject = new BehaviorSubject(false); + public isAuthenticated$ = this.isAuthenticatedSubject.asObservable(); + + constructor( + private http: HttpClient, + private router: Router + ) { + this.loadStoredUser(); + } + + // ============================== + // Authentication Methods + // ============================== + + /** + * Login user with username and password + */ + login(credentials: LoginRequest): Observable> { + return this.http.post>( + `${this.apiUrl}/User/login`, + credentials + ).pipe( + tap(response => { + if (response.success && response.data) { + // Store the token + localStorage.setItem(this.tokenKey, response.data.token); + + // Store the user data if available + if (response.data.user) { + this.currentUserSubject.next(response.data.user); + localStorage.setItem(this.userKey, JSON.stringify(response.data.user)); + console.log('User data stored from login response:', response.data.user); + } + + this.isAuthenticatedSubject.next(true); + } + }), + catchError(this.handleError) + ); + } + + /** + * Register a new user + */ + register(userData: RegisterRequest): Observable> { + return this.http.post>( + `${this.apiUrl}/User/register`, + userData + ).pipe( + catchError(this.handleError) + ); + } + + /** + * Logout user + */ + logout(): void { + localStorage.removeItem(this.tokenKey); + localStorage.removeItem(this.userKey); + this.currentUserSubject.next(null); + this.isAuthenticatedSubject.next(false); + this.router.navigate(['/login']); + } + + /** + * Confirm email with token + */ + confirmEmail(email: string, token: string): Observable> { + return this.http.get>( + `${this.apiUrl}/User/confirm-email`, + { + params: { + email: email, + encodedToken: token + } + } + ).pipe( + catchError(this.handleError) + ); + } + + /** + * Resend email confirmation + */ + resendConfirmation(email: string): Observable> { + return this.http.post>( + `${this.apiUrl}/User/resend-confirmation`, + email + ).pipe( + catchError(this.handleError) + ); + } + + /** + * Request password reset + */ + forgotPassword(email: string): Observable> { + return this.http.post>( + `${this.apiUrl}/User/forgot-password`, + email + ).pipe( + catchError(this.handleError) + ); + } + + /** + * Reset password with token + */ + resetPassword(resetData: ResetPasswordRequest): Observable> { + return this.http.post>( + `${this.apiUrl}/User/reset-password`, + resetData + ).pipe( + catchError(this.handleError) + ); + } + + /** + * Change password (authenticated user) + */ + changePassword(userId: string, changeData: ChangePasswordRequest): Observable> { + return this.http.put>( + `${this.apiUrl}/User/${userId}/change-password`, + changeData + ).pipe( + catchError(this.handleError) + ); + } + + // ============================== + // User Management Methods + // ============================== + + /** + * Get current user profile + */ + getCurrentUser(): Observable> { + const userId = this.getUserId(); + if (!userId) { + return throwError(() => new Error('User not authenticated')); + } + + return this.http.get>( + `${this.apiUrl}/User/${userId}` + ).pipe( + tap(response => { + if (response.success && response.data) { + this.currentUserSubject.next(response.data); + localStorage.setItem(this.userKey, JSON.stringify(response.data)); + } + }), + catchError(this.handleError) + ); + } + + /** + * Get user by ID + */ + getUserById(userId: string): Observable> { + return this.http.get>( + `${this.apiUrl}/User/${userId}` + ).pipe( + catchError(this.handleError) + ); + } + + // ============================== + // External Login Methods + // ============================== + + /** + * Initiate external login with provider + */ + externalLogin(provider: string, returnUrl: string = '/'): void { + const redirectUrl = `${this.apiUrl}/ExternalLogin/external-login`; + window.location.href = `${redirectUrl}?provider=${provider}&returnUrl=${returnUrl}`; + } + + /** + * Handle external login callback + */ + handleExternalLoginCallback(): Observable> { + const urlParams = new URLSearchParams(window.location.search); + const returnUrl = urlParams.get('returnUrl') || '/'; + const remoteError = urlParams.get('remoteError'); + + if (remoteError) { + return throwError(() => new Error(`External login error: ${remoteError}`)); + } + + return this.http.get>( + `${this.apiUrl}/ExternalLogin/external-login-callback`, + { + params: { + returnUrl: returnUrl + } + } + ).pipe( + tap(response => { + if (response.success && response.data) { + localStorage.setItem(this.tokenKey, response.data.token); + + if (response.data.user) { + this.currentUserSubject.next(response.data.user); + localStorage.setItem(this.userKey, JSON.stringify(response.data.user)); + } + + this.isAuthenticatedSubject.next(true); + this.router.navigateByUrl(returnUrl); + } + }), + catchError(this.handleError) + ); + } + + // ============================== + // Token and User Management + // ============================== + + /** + * Get stored JWT token + */ + getToken(): string | null { + return localStorage.getItem(this.tokenKey); + } + + /** + * Get stored user data + */ + getUser(): UserDto | null { + const userData = localStorage.getItem(this.userKey); + if (userData) { + try { + return JSON.parse(userData); + } catch { + return null; + } + } + return null; + } + + /** + * Get current user ID from stored user + */ + getUserId(): string | null { + const user = this.getUser(); + return user ? user.id : null; + } + + /** + * Check if user is authenticated + */ + isAuthenticated(): boolean { + return !!this.getToken() && !!this.getUser(); + } + + /** + * Check if user has specific role + */ + hasRole(role: string): boolean { + const user = this.getUser(); + return user ? user.roles.includes(role) : false; + } + + /** + * Check if user is Admin + */ + isAdmin(): boolean { + return this.hasRole('Admin'); + } + + /** + * Check if user is SuperAdmin + */ + isSuperAdmin(): boolean { + return this.hasRole('SuperAdmin'); + } + + // ============================== + // Private Helper Methods + // ============================== + + /** + * Load stored user on app initialization + */ + private loadStoredUser(): void { + const user = this.getUser(); + const token = this.getToken(); + + if (user && token) { + this.currentUserSubject.next(user); + this.isAuthenticatedSubject.next(true); + console.log('User restored from storage:', user); + } else { + this.currentUserSubject.next(null); + this.isAuthenticatedSubject.next(false); + } + } + + private handleError(error: any): Observable { + let errorMessage = 'An error occurred'; + + if (error.error instanceof ErrorEvent) { + errorMessage = error.error.message; + } else { + if (error.error && error.error.message) { + errorMessage = error.error.message; + } else if (error.status) { + switch (error.status) { + case 400: + errorMessage = 'Bad request. Please check your input.'; + break; + case 401: + errorMessage = 'Unauthorized. Please login again.'; + break; + case 403: + errorMessage = 'You do not have permission to perform this action.'; + break; + case 404: + errorMessage = 'Resource not found.'; + break; + case 500: + errorMessage = 'Server error. Please try again later.'; + break; + default: + errorMessage = `Error ${error.status}: ${error.statusText}`; + } + } + } + + return throwError(() => new Error(errorMessage)); + } +} diff --git a/src/app/views/pages/login/login.component.html b/src/app/views/pages/login/login.component.html index a1cf9b4..41622c4 100644 --- a/src/app/views/pages/login/login.component.html +++ b/src/app/views/pages/login/login.component.html @@ -5,34 +5,73 @@ -
+

Login

Sign In to your account

+ + + + {{ errorMessage }} + + + - + +
+ Username is required +
+ + +
+ Password is required + Password must be at least 6 characters +
+ - - @@ -45,10 +84,16 @@

Login

Sign up

- Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. + Create your account and start using our platform today. + Join our community and access all features.

-
diff --git a/src/app/views/pages/login/login.component.ts b/src/app/views/pages/login/login.component.ts index e9ab25b..4301de0 100644 --- a/src/app/views/pages/login/login.component.ts +++ b/src/app/views/pages/login/login.component.ts @@ -1,4 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { Router, RouterLink, ActivatedRoute } from '@angular/router'; import { IconDirective } from '@coreui/icons-angular'; import { ButtonDirective, @@ -11,12 +13,94 @@ import { FormDirective, InputGroupComponent, InputGroupTextDirective, - RowComponent + RowComponent, + AlertComponent } from '@coreui/angular'; +import { AuthService } from '../../../services/auth.service'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-login', templateUrl: './login.component.html', - imports: [ContainerComponent, RowComponent, ColComponent, CardGroupComponent, CardComponent, CardBodyComponent, FormDirective, InputGroupComponent, InputGroupTextDirective, IconDirective, FormControlDirective, ButtonDirective] + imports: [ + CommonModule, + ReactiveFormsModule, + ContainerComponent, + RowComponent, + ColComponent, + CardGroupComponent, + CardComponent, + CardBodyComponent, + FormDirective, + InputGroupComponent, + InputGroupTextDirective, + IconDirective, + FormControlDirective, + ButtonDirective, + RouterLink, + AlertComponent + ] }) -export class LoginComponent {} +export class LoginComponent implements OnInit { + loginForm: FormGroup; + isLoading = false; + errorMessage: string | null = null; + returnUrl: string = '/dashboard'; + + constructor( + private fb: FormBuilder, + private authService: AuthService, + private router: Router, + private route: ActivatedRoute + ) { + this.loginForm = this.fb.group({ + userName: ['', [Validators.required]], + password: ['', [Validators.required, Validators.minLength(6)]] + }); + } + + ngOnInit(): void { + // If user is already logged in, redirect to dashboard + if (this.authService.isAuthenticated()) { + this.router.navigate(['/dashboard']); + return; + } + + // Get return URL from query params + this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/dashboard'; + } + + onSubmit(): void { + if (this.loginForm.invalid) { + Object.keys(this.loginForm.controls).forEach(key => { + this.loginForm.get(key)?.markAsTouched(); + }); + return; + } + + this.isLoading = true; + this.errorMessage = null; + + this.authService.login(this.loginForm.value).subscribe({ + next: (response) => { + this.isLoading = false; + if (response.success) { + // User data is already stored in AuthService + // Just redirect to the return URL + this.router.navigateByUrl(this.returnUrl); + } else { + this.errorMessage = response.message || 'Login failed. Please try again.'; + } + }, + error: (error) => { + this.isLoading = false; + this.errorMessage = error.message || 'An error occurred during login.'; + console.error('Login error:', error); + } + }); + } + + // Convenience getters for form controls + get userName() { return this.loginForm.get('userName'); } + get password() { return this.loginForm.get('password'); } +} diff --git a/src/app/views/pages/register/register.component.html b/src/app/views/pages/register/register.component.html index 68bffd1..534a892 100644 --- a/src/app/views/pages/register/register.component.html +++ b/src/app/views/pages/register/register.component.html @@ -4,33 +4,159 @@ - +

Register

Create your account

+ + + + {{ successMessage }} + + + + + {{ errorMessage }} + + + - + +
+ Full name is required + Full name must be at least 2 characters +
+ + + + + + + + +
+ Username is required + Username must be at least 3 characters +
+ + @ - + +
+ Email is required + Please enter a valid email address +
+ + + + + + + + + + - + +
+ Password is required + Password must be at least 6 characters +
+ + - + +
+ Please confirm your password + Passwords do not match +
+
- + +
+ +
+

+ Already have an account? + Login +

diff --git a/src/app/views/pages/register/register.component.ts b/src/app/views/pages/register/register.component.ts index c19b544..7cb3321 100644 --- a/src/app/views/pages/register/register.component.ts +++ b/src/app/views/pages/register/register.component.ts @@ -1,4 +1,6 @@ import { Component } from '@angular/core'; +import { FormBuilder, FormGroup, Validators, ReactiveFormsModule, AbstractControl, ValidationErrors } from '@angular/forms'; +import { Router, RouterLink } from '@angular/router'; import { IconDirective } from '@coreui/icons-angular'; import { ButtonDirective, @@ -10,12 +12,113 @@ import { FormDirective, InputGroupComponent, InputGroupTextDirective, - RowComponent + RowComponent, + AlertComponent } from '@coreui/angular'; +import { AuthService } from '../../../services/auth.service'; +import { CommonModule } from '@angular/common'; @Component({ selector: 'app-register', templateUrl: './register.component.html', - imports: [ContainerComponent, RowComponent, ColComponent, CardComponent, CardBodyComponent, FormDirective, InputGroupComponent, InputGroupTextDirective, IconDirective, FormControlDirective, ButtonDirective] + imports: [ + CommonModule, + ReactiveFormsModule, + ContainerComponent, + RowComponent, + ColComponent, + CardComponent, + CardBodyComponent, + FormDirective, + InputGroupComponent, + InputGroupTextDirective, + IconDirective, + FormControlDirective, + ButtonDirective, + RouterLink, + AlertComponent + ] }) -export class RegisterComponent {} +export class RegisterComponent { + registerForm: FormGroup; + isLoading = false; + errorMessage: string | null = null; + successMessage: string | null = null; + + constructor( + private fb: FormBuilder, + private authService: AuthService, + private router: Router + ) { + this.registerForm = this.fb.group({ + fullName: ['', [Validators.required, Validators.minLength(2)]], + userName: ['', [Validators.required, Validators.minLength(3)]], + email: ['', [Validators.required, Validators.email]], + phoneNumber: ['', []], + password: ['', [Validators.required, Validators.minLength(6)]], + confirmPassword: ['', [Validators.required]] + }, { + validators: this.passwordMatchValidator + }); + } + + // Custom validator to check if passwords match + passwordMatchValidator(control: AbstractControl): ValidationErrors | null { + const password = control.get('password'); + const confirmPassword = control.get('confirmPassword'); + + if (!password || !confirmPassword) { + return null; + } + + return password.value === confirmPassword.value ? null : { passwordMismatch: true }; + } + + onSubmit(): void { + if (this.registerForm.invalid) { + Object.keys(this.registerForm.controls).forEach(key => { + this.registerForm.get(key)?.markAsTouched(); + }); + return; + } + + this.isLoading = true; + this.errorMessage = null; + this.successMessage = null; + + // Remove confirmPassword from the request payload + const { confirmPassword, ...registrationData } = this.registerForm.value; + + this.authService.register(registrationData).subscribe({ + next: (response) => { + this.isLoading = false; + if (response.success) { + this.successMessage = 'Registration successful! Please check your email to confirm your account.'; + // Redirect to login after 3 seconds + setTimeout(() => { + this.router.navigate(['/login']); + }, 3000); + } else { + this.errorMessage = response.message || 'Registration failed. Please try again.'; + // Show specific error messages from server + if (response.errors && response.errors.length > 0) { + this.errorMessage = response.errors.join(', '); + } + } + }, + error: (error) => { + this.isLoading = false; + this.errorMessage = error.message || 'An error occurred during registration.'; + console.error('Registration error:', error); + } + }); + } + + // Convenience getters for form controls + get fullName() { return this.registerForm.get('fullName'); } + get userName() { return this.registerForm.get('userName'); } + get email() { return this.registerForm.get('email'); } + get phoneNumber() { return this.registerForm.get('phoneNumber'); } + get password() { return this.registerForm.get('password'); } + get confirmPassword() { return this.registerForm.get('confirmPassword'); } +} diff --git a/src/app/views/pages/routes.ts b/src/app/views/pages/routes.ts index 410f516..e3fcf6e 100644 --- a/src/app/views/pages/routes.ts +++ b/src/app/views/pages/routes.ts @@ -28,5 +28,38 @@ export const routes: Routes = [ data: { title: 'Register Page' } - } + }, + // ============================== + // FORGOT & RESET PASSWORD ROUTES (Commented - Will be implemented later) + // ============================== + // Uncomment when forgot-password component is created + /* + { + path: 'forgot-password', + loadComponent: () => import('./forgot-password/forgot-password.component').then(m => m.ForgotPasswordComponent), + data: { + title: 'Forgot Password' + } + }, + { + path: 'reset-password', + loadComponent: () => import('./reset-password/reset-password.component').then(m => m.ResetPasswordComponent), + data: { + title: 'Reset Password' + } + }, + */ + // ============================== + // EXTERNAL LOGIN CALLBACK ROUTE (Commented - Will be implemented later) + // ============================== + // Uncomment when external-login-callback component is created + /* + { + path: 'external-login-callback', + loadComponent: () => import('./external-login-callback/external-login-callback.component').then(m => m.ExternalLoginCallbackComponent), + data: { + title: 'External Login Callback' + } + }, + */ ];