Saturday, August 16, 2025
0 comments

Master Angular 20 Authentication & Guards: A Beginner’s Guide to Route Protection, Login/Logout, and Restricting Admin Routes

 




Angular Authentication BasicsAuthentication in Angular involves verifying user identity and managing their session state. For a beginner-friendly approach, we’ll use local storage to store a user’s authentication state, though production apps typically use JWT tokens or OAuth.Key Concepts
  • Authentication State: Track whether a user is logged in and their role (e.g., admin).
  • Session Storage: Use local storage to persist login state across page refreshes.
  • Services: Manage authentication logic in a dedicated service (e.g., AuthService).
  • Route Protection: Use guards to restrict access to routes based on authentication state.
Simple Authentication Flow
  1. User logs in via a login form (e.g., username/password).
  2. Validate credentials (e.g., against a service or API).
  3. Store authentication state in local storage.
  4. Use route guards to protect restricted routes.
  5. Allow logout to clear the state.

Route Guards (CanActivate, CanDeactivate)Angular’s route guards control navigation by allowing or denying access to routes based on conditions.CanActivate
  • Determines if a route can be accessed.
  • Used to restrict routes to authenticated users or specific roles.
  • Returns true (allow navigation) or false (deny navigation, redirect elsewhere).
Example:
typescript
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(private router: Router) {}

  canActivate(): boolean {
    const isAuthenticated = !!localStorage.getItem('authToken');
    if (!isAuthenticated) {
      this.router.navigate(['/login']);
      return false;
    }
    return true;
  }
}
CanDeactivate
  • Determines if a user can leave a route (e.g., to prevent unsaved changes).
  • Useful for forms or components with unsaved data.
  • Returns true (allow navigation) or false (block navigation, e.g., show a confirmation dialog).
Example:
typescript
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { RegisterComponent } from '../components/register/register.component';

@Injectable({
  providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<RegisterComponent> {
  canDeactivate(component: RegisterComponent): boolean {
    if (component.registerForm.dirty && !component.registerForm.submitted) {
      return confirm('You have unsaved changes. Are you sure you want to leave?');
    }
    return true;
  }
}

Simple Login/Logout System with Local StorageWe’ll create an AuthService to handle login, logout, and authentication state, storing the logged-in user in local storage. The system will:
  • Validate admin credentials (from UserService).
  • Store the authenticated user’s state.
  • Provide login/logout functionality.
  • Update the UI (e.g., HeaderComponent) to reflect authentication state.
Example: AuthService
typescript
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/user.model';
import { StorageService } from './storage.service';
import { UserService } from './user.service';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  currentUser$ = this.currentUserSubject.asObservable();

  constructor(
    private storageService: StorageService,
    private userService: UserService
  ) {
    const storedUser = this.storageService.getLocalStorage('currentUser');
    if (storedUser) {
      this.currentUserSubject.next(storedUser);
    }
  }

  login(username: string, password: string): boolean {
    const isValid = this.userService.validateAdmin(username, password);
    if (isValid) {
      const user: User = {
        id: 0,
        userName: username,
        emailId: `${username}@example.com`,
        role: 'admin'
      };
      this.currentUserSubject.next(user);
      this.storageService.setLocalStorage('currentUser', user);
      return true;
    }
    return false;
  }

  logout() {
    this.currentUserSubject.next(null);
    this.storageService.clearLocalStorage('currentUser');
  }

  isAuthenticated(): boolean {
    return !!this.currentUserSubject.getValue();
  }

  isAdmin(): boolean {
    const user = this.currentUserSubject.getValue();
    return user?.role === 'admin';
  }
}
Explanation:
  • Uses BehaviorSubject to manage the current user state reactively.
  • Integrates with StorageService to persist the user in local storage.
  • login validates credentials via UserService and sets the user state.
  • logout clears the user state and storage.
  • isAuthenticated and isAdmin check the authentication state and role.

Restricting Admin RouteWe’ll create an AuthGuard to restrict the /admin route to authenticated admin users, redirecting unauthenticated users to the login page.Example: AuthGuard
typescript
// src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(): boolean {
    if (this.authService.isAuthenticated() && this.authService.isAdmin()) {
      return true;
    }
    this.router.navigate(['/login']);
    return false;
  }
}
Explanation:
  • Checks if the user is authenticated and has the admin role.
  • Redirects to /login if the conditions are not met.

Hands-On Project: Restrict /admin Access to Authenticated AdminsLet’s enhance your angular-routing-demo project by:
  1. Creating an AuthService for login/logout functionality.
  2. Implementing an AuthGuard to restrict /admin access.
  3. Updating the LoginComponent to use the AuthService.
  4. Adding logout functionality to the HeaderComponent.
  5. Optionally applying a CanDeactivate guard to the RegisterComponent for unsaved changes.
  6. Maintaining integration with freeapi.miniprojectideas.com, StateService, and Angular Material from Modules 9 and 10.
Step 1: Set Up the ProjectEnsure the project is set up (from Module 10):
bash
ng new angular-routing-demo --style=scss
cd angular-routing-demo
ng add @angular/material
ng g component components/user
ng g component components/admin
ng g component components/user-details
ng g component components/not-found
ng g component components/admin-dashboard
ng g component components/admin-settings
ng g component components/register
ng g component components/login
ng g component components/user-info
ng g component components/header
ng g component components/footer
ng g pipe pipes/filter-by-role
ng g service services/user
ng g service services/storage
ng g service services/state
ng g service services/auth
ng g guard guards/auth
ng g guard guards/can-deactivate
  • Select the Indigo/Pink theme during ng add @angular/material.
  • Choose CanActivate for AuthGuard and CanDeactivate for CanDeactivateGuard when prompted.
Step 2: Update Global Styles
scss
// src/styles.scss
@import '@angular/material/theming';
@include mat-core();

$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);

:root {
  --primary-color: #3f51b5;
  --accent-color: #ff4081;
  --background-color: #f5f5f5;
  --text-color: #333;
}

body {
  margin: 0;
  font-family: Roboto, 'Helvetica Neue', sans-serif;
  background-color: var(--background-color);
  color: var(--text-color);
}

.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}
Step 3: Create AuthService
typescript
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/user.model';
import { StorageService } from './storage.service';
import { UserService } from './user.service';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  currentUser$ = this.currentUserSubject.asObservable();

  constructor(
    private storageService: StorageService,
    private userService: UserService
  ) {
    const storedUser = this.storageService.getLocalStorage('currentUser');
    if (storedUser) {
      this.currentUserSubject.next(storedUser);
    }
  }

  login(username: string, password: string): boolean {
    const isValid = this.userService.validateAdmin(username, password);
    if (isValid) {
      const user: User = {
        id: 0,
        userName: username,
        emailId: `${username}@example.com`,
        role: 'admin'
      };
      this.currentUserSubject.next(user);
      this.storageService.setLocalStorage('currentUser', user);
      return true;
    }
    return false;
  }

  logout() {
    this.currentUserSubject.next(null);
    this.storageService.clearLocalStorage('currentUser');
  }

  isAuthenticated(): boolean {
    return !!this.currentUserSubject.getValue();
  }

  isAdmin(): boolean {
    const user = this.currentUserSubject.getValue();
    return user?.role === 'admin';
  }
}
Step 4: Create AuthGuard
typescript
// src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(): boolean {
    if (this.authService.isAuthenticated() && this.authService.isAdmin()) {
      return true;
    }
    this.router.navigate(['/login']);
    return false;
  }
}
Step 5: Create CanDeactivateGuard
typescript
// src/app/guards/can-deactivate.guard.ts
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { RegisterComponent } from '../components/register/register.component';

@Injectable({
  providedIn: 'root'
})
export class CanDeactivateGuard implements CanDeactivate<RegisterComponent> {
  canDeactivate(component: RegisterComponent): boolean {
    if (component.registerForm.dirty && !component.registerForm.submitted) {
      return confirm('You have unsaved changes. Are you sure you want to leave?');
    }
    return true;
  }
}
Step 6: Update User Model
typescript
// src/app/models/user.model.ts
export interface User {
  id: number;
  userName: string;
  emailId: string;
  role: string;
}
Step 7: Update StateService
typescript
// src/app/services/state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/user.model';
import { StorageService } from './storage.service';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  private selectedUserSubject = new BehaviorSubject<User | null>(null);
  selectedUser$ = this.selectedUserSubject.asObservable();

  constructor(private storageService: StorageService) {
    const storedUser = this.storageService.getLocalStorage('selectedUser');
    if (storedUser) {
      this.selectedUserSubject.next(storedUser);
    }
  }

  setSelectedUser(user: User | null) {
    this.selectedUserSubject.next(user);
    if (user) {
      this.storageService.setLocalStorage('selectedUser', user);
    } else {
      this.storageService.clearLocalStorage('selectedUser');
    }
  }

  getCurrentUser() {
    return this.selectedUserSubject.getValue();
  }
}
Step 8: Update StorageService
typescript
// src/app/services/storage.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  setLocalStorage(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  getLocalStorage(key: string) {
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }

  clearLocalStorage(key: string) {
    localStorage.removeItem(key);
  }
}
Step 9: Update UserService
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://freeapi.miniprojectideas.com/api/User/GetAllUsers';
  private localUsers: User[] = [
    { id: 1, userName: 'Alice', emailId: 'alice@example.com', role: 'admin' },
    { id: 2, userName: 'Bob', emailId: 'bob@example.com', role: 'user' },
    { id: 3, userName: 'Charlie', emailId: 'charlie@example.com', role: 'admin' },
    { id: 4, userName: 'David', emailId: 'david@example.com', role: 'user' }
  ];
  private admins = [
    { username: 'admin1', password: 'admin123' },
    { username: 'admin2', password: 'admin456' }
  ];

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl).pipe(
      catchError(error => {
        console.error('Error fetching users from API:', error);
        return of(this.localUsers);
      })
    );
  }

  getUserById(id: number): Observable<User | undefined> {
    return this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
      catchError(error => {
        console.error('Error fetching user by ID:', error);
        const localUser = this.localUsers.find(user => user.id === id);
        return of(localUser);
      })
    );
  }

  addUser(user: User): Observable<any> {
    return this.http.post(this.apiUrl, user).pipe(
      catchError(error => {
        console.error('Error adding user:', error);
        this.localUsers.push(user);
        return of(user);
      })
    );
  }

  validateAdmin(username: string, password: string): boolean {
    return this.admins.some(admin => admin.username === username && admin.password === password);
  }
}
Step 10: Update app.routes.tsApply the AuthGuard to the /admin route and CanDeactivateGuard to the /register route.
typescript
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { UserComponent } from './components/user/user.component';
import { AdminComponent } from './components/admin/admin.component';
import { UserDetailsComponent } from './components/user-details/user-details.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { AdminDashboardComponent } from './components/admin-dashboard/admin-dashboard.component';
import { AdminSettingsComponent } from './components/admin-settings/admin-settings.component';
import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';
import { AuthGuard } from './guards/auth.guard';
import { CanDeactivateGuard } from './guards/can-deactivate.guard';

export const routes: Routes = [
  {
    path: 'user',
    component: UserComponent,
    children: [
      { path: ':id', component: UserDetailsComponent }
    ]
  },
  { path: 'register', component: RegisterComponent, canDeactivate: [CanDeactivateGuard] },
  { path: 'login', component: LoginComponent },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      { path: 'dashboard', component: AdminDashboardComponent },
      { path: 'settings', component: AdminSettingsComponent },
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' }
    ]
  },
  { path: '', redirectTo: '/user', pathMatch: 'full' },
  { path: '**', component: NotFoundComponent }
];
Step 11: Update main.ts
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideAnimations()
  ]
}).catch(err => console.error(err));
Step 12: Update AppComponent
typescript
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  standalone: true,
  imports: [RouterOutlet, HeaderComponent, FooterComponent]
})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log('Navigation started:', event.url);
      } else if (event instanceof NavigationEnd) {
        console.log('Navigation ended:', event.url);
      }
    });
  }
}
typescript
// src/app/app.component.html
<app-header></app-header>
<main class="container">
  <router-outlet></router-outlet>
</main>
<app-footer></app-footer>
scss
// src/app/app.component.scss
main.container {
  min-height: calc(100vh - 128px); /* Adjust for header/footer height */
}
Step 13: Update HeaderComponentAdd logout functionality and display the logged-in user.
typescript
// src/app/components/header/header.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive, Router } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { AuthService } from '../../services/auth.service';
import { User } from '../../models/user.model';

@Component({
  selector: 'app-header',
  template: `
    <mat-toolbar color="primary">
      <span>Angular Routing Demo</span>
      <nav>
        <a mat-button routerLink="/user" routerLinkActive="active">User Dashboard</a>
        <a mat-button routerLink="/register" routerLinkActive="active">Register</a>
        <a mat-button *ngIf="!(currentUser$ | async)" routerLink="/login" routerLinkActive="active">Login</a>
        <span *ngIf="currentUser$ | async as user">Welcome, {{ user.userName }}</span>
        <button mat-button *ngIf="currentUser$ | async" (click)="logout()">Logout</button>
        <a mat-button routerLink="/admin" routerLinkActive="active">Admin Panel</a>
      </nav>
    </mat-toolbar>
  `,
  styleUrls: ['./header.component.scss'],
  standalone: true,
  imports: [RouterLink, RouterLinkActive, MatToolbarModule, MatButtonModule]
})
export class HeaderComponent {
  currentUser$ = this.authService.currentUser$;

  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  logout() {
    this.authService.logout();
    this.router.navigate(['/user']);
  }
}
scss
// src/app/components/header/header.component.scss
mat-toolbar {
  display: flex;
  justify-content: space-between;
  padding: 0 20px;

  nav {
    display: flex;
    align-items: center;
    a, button, span {
      margin-left: 10px;
    }
    a.active {
      font-weight: bold;
      border-bottom: 2px solid white;
    }
  }
}
Step 14: Update FooterComponent
typescript
// src/app/components/footer/footer.component.ts
import { Component } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';

@Component({
  selector: 'app-footer',
  template: `
    <mat-toolbar color="primary">
      <span>&copy; 2025 Angular Routing Demo</span>
    </mat-toolbar>
  `,
  styleUrls: ['./footer.component.scss'],
  standalone: true,
  imports: [MatToolbarModule]
})
export class FooterComponent {}
scss
// src/app/components/footer/footer.component.scss
mat-toolbar {
  justify-content: center;
  padding: 10px 0;
}
Step 15: Update AdminComponent
typescript
// src/app/components/admin/admin.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterOutlet, RouterLink, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { FilterByRolePipe } from '../../pipes/filter-by-role.pipe';
import { UserService } from '../../services/user.service';
import { StateService } from '../../services/state.service';
import { User } from '../../models/user.model';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';

@Component({
  selector: 'app-admin',
  templateUrl: './admin.component.html',
  styleUrls: ['./admin.component.scss'],
  standalone: true,
  imports: [
    FormsModule,
    RouterOutlet,
    RouterLink,
    FilterByRolePipe,
    MatCardModule,
    MatButtonModule,
    MatSelectModule,
    MatInputModule,
    MatFormFieldModule
  ]
})
export class AdminComponent implements OnInit, OnDestroy {
  users$: Observable<User[]>;
  filterRole = 'admin';
  isHighlighted = false;
  isLoading = false;
  error: string | null = null;
  selectedUser$ = this.stateService.selectedUser$;

  constructor(
    private userService: UserService,
    private stateService: StateService,
    private router: Router
  ) {
    this.users$ = of([]);
  }

  ngOnInit() {
    this.isLoading = true;
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error = 'Failed to load users: ' + err.message;
        this.isLoading = false;
        return of([]);
      }),
      finalize(() => this.isLoading = false)
    );
  }

  ngOnDestroy() {
    console.log('AdminComponent: Destroyed');
  }

  toggleHighlight() {
    this.isHighlighted = !this.isHighlighted;
  }

  setFilterRole(role: string) {
    this.filterRole = role;
  }

  logSearch(value: string) {
    console.log('Search:', value);
  }

  selectUser(user: User) {
    this.stateService.setSelectedUser(user);
    this.router.navigate(['/user']);
  }

  clearSelectedUser() {
    this.stateService.setSelectedUser(null);
  }
}
typescript
// src/app/components/admin/admin.component.html
<mat-card class="container">
  <mat-card-header>
    <mat-card-title>Admin Panel</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <nav>
      <a mat-button routerLink="dashboard" routerLinkActive="active">Dashboard</a>
      <a mat-button routerLink="settings" routerLinkActive="active">Settings</a>
    </nav>
    <router-outlet></router-outlet>
    <h3>Admin Users List</h3>
    <div *ngIf="error" class="error">{{ error }}</div>
    <div *ngIf="isLoading" class="loading">Loading users...</div>
    <div *ngIf="users$ | async as users; else loading">
      <mat-form-field appearance="fill">
        <mat-label>Filter by Role</mat-label>
        <mat-select [(ngModel)]="filterRole" (ngModelChange)="setFilterRole($event)">
          <mat-option value="all">All</mat-option>
          <mat-option value="admin">Admin</mat-option>
          <mat-option value="user">User</mat-option>
        </mat-select>
      </mat-form-field>
      <button mat-raised-button color="primary" (click)="toggleHighlight()">Toggle Highlight</button>
      <mat-card *ngFor="let user of users | filterByRole:filterRole" [ngClass]="{'highlight': isHighlighted && user.role === 'admin'}">
        <mat-card-content>
          {{ user.userName | uppercase }} ({{ user.role }}) - {{ user.emailId }}
          <button mat-raised-button color="accent" (click)="selectUser(user)">Select</button>
        </mat-card-content>
      </mat-card>
      <div *ngIf="selectedUser$ | async as selectedUser">
        <p>Selected: {{ selectedUser.userName }}</p>
        <button mat-raised-button color="warn" (click)="clearSelectedUser()">Clear Selection</button>
      </div>
      <mat-form-field appearance="fill">
        <mat-label>Search users</mat-label>
        <input matInput #searchInput>
        <button mat-button (click)="logSearch(searchInput.value)">Search</button>
      </mat-form-field>
    </div>
    <ng-template #loading>Loading users...</ng-template>
  </mat-card-content>
</mat-card>
scss
// src/app/components/admin/admin.component.scss
.container {
  margin: 20px;
}

mat-card {
  margin: 10px 0;
  padding: 10px;
}

.highlight {
  background-color: #e0f7fa;
}

nav {
  margin-bottom: 20px;
  a {
    margin-right: 10px;
    &.active {
      font-weight: bold;
      border-bottom: 2px solid var(--primary-color);
    }
  }
}

.loading {
  text-align: center;
  color: var(--primary-color);
}

.error {
  text-align: center;
  color: red;
}

button {
  margin-left: 10px;
}
Step 16: Update UserComponent
typescript
// src/app/components/user/user.component.ts
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { StateService } from '../../services/state.service';
import { UserInfoComponent } from '../user-info/user-info.component';
import { User } from '../../models/user.model';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatListModule } from '@angular/material/list';

@Component({
  selector: 'app-user',
  templateUrl: './user.component.html',
  styleUrls: ['./user.component.scss'],
  standalone: true,
  imports: [
    FormsModule,
    RouterLink,
    UserInfoComponent,
    MatCardModule,
    MatButtonModule,
    MatInputModule,
    MatFormFieldModule,
    MatListModule
  ]
})
export class UserComponent implements OnInit {
  searchQuery = '';
  users$: Observable<User[]>;
  isLoading = false;
  error: string | null = null;
  selectedUser$ = this.stateService.selectedUser$;

  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private userService: UserService,
    private stateService: StateService
  ) {
    this.users$ = of([]);
  }

  ngOnInit() {
    this.isLoading = true;
    this.users$ = this.userService.getUsers().pipe(
      catchError(err => {
        this.error = 'Failed to load users: ' + err.message;
        this.isLoading = false;
        return of([]);
      }),
      finalize(() => this.isLoading = false)
    );
    this.route.queryParamMap.subscribe(params => {
      this.searchQuery = params.get('search') || '';
    });
  }

  updateQuery() {
    this.router.navigate(['/user'], { queryParams: { search: this.searchQuery } });
  }

  clearSelectedUser() {
    this.stateService.setSelectedUser(null);
  }
}
typescript
// src/app/components/user/user.component.html
<mat-card class="container">
  <mat-card-header>
    <mat-card-title>User Dashboard</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <div *ngIf="error" class="error">{{ error }}</div>
    <div *ngIf="isLoading" class="loading">Loading users...</div>
    <mat-form-field appearance="fill">
      <mat-label>Search users</mat-label>
      <input matInput [(ngModel)]="searchQuery" (ngModelChange)="updateQuery()">
    </mat-form-field>
    <p>Search Query: {{ searchQuery }}</p>
    <app-user-info [user]="selectedUser$ | async" (clearUser)="clearSelectedUser()"></app-user-info>
    <router-outlet></router-outlet>
    <mat-list *ngIf="users$ | async as users; else loading">
      <mat-list-item *ngFor="let user of users">
        <a mat-button routerLink="/user/{{ user.id }}">{{ user.userName }} ({{ user.role }}) - {{ user.emailId }}</a>
      </mat-list-item>
    </mat-list>
    <ng-template #loading>Loading users...</ng-template>
  </mat-card-content>
</mat-card>
scss
// src/app/components/user/user.component.scss
.container {
  margin: 20px;
}

mat-list {
  margin-top: 20px;
}

mat-list-item {
  margin: 5px 0;
  a {
    text-decoration: none;
    color: var(--primary-color);
    &:hover {
      text-decoration: underline;
    }
  }
}

.loading {
  text-align: center;
  color: var(--primary-color);
}

.error {
  text-align: center;
  color: red;
}
Step 17: Update UserInfoComponent
typescript
// src/app/components/user-info/user-info.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { User } from '../../models/user.model';
import { StateService } from '../../services/state.service';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-user-info',
  template: `
    <mat-card *ngIf="user">
      <mat-card-content>
        <p>Selected User: {{ user.userName }} ({{ user.role }})</p>
        <button mat-raised-button color="warn" (click)="clearSelection()">Clear Selection</button>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./user-info.component.scss'],
  standalone: true,
  imports: [MatCardModule, MatButtonModule]
})
export class UserInfoComponent {
  @Input() user: User | null = null;
  @Output() clearUser = new EventEmitter<void>();

  constructor(private stateService: StateService) {}

  clearSelection() {
    this.stateService.setSelectedUser(null);
    this.clearUser.emit();
  }
}
scss
// src/app/components/user-info/user-info.component.scss
mat-card {
  margin: 10px 0;
  padding: 10px;
}
Step 18: Update UserDetailsComponent
typescript
// src/app/components/user-details/user-details.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-user-details',
  templateUrl: './user-details.component.html',
  styleUrls: ['./user-details.component.scss'],
  standalone: true,
  imports: [RouterLink, MatCardModule, MatButtonModule]
})
export class UserDetailsComponent implements OnInit {
  user$: Observable<User | undefined>;
  isLoading = false;
  error: string | null = null;

  constructor(
    private route: ActivatedRoute,
    private userService: UserService
  ) {
    this.user$ = of(undefined);
  }

  ngOnInit() {
    this.isLoading = true;
    this.route.paramMap.subscribe(params => {
      const id = Number(params.get('id'));
      this.user$ = this.userService.getUserById(id).pipe(
        catchError(err => {
          this.error = 'Failed to load user: ' + err.message;
          this.isLoading = false;
          return of(undefined);
        }),
        finalize(() => this.isLoading = false)
      );
    });
  }
}
typescript
// src/app/components/user-details/user-details.component.html
<mat-card class="container">
  <mat-card-header>
    <mat-card-title>User Details</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <div *ngIf="error" class="error">{{ error }}</div>
    <div *ngIf="isLoading" class="loading">Loading user...</div>
    <div *ngIf="user$ | async as user; else noUser">
      <p>Name: {{ user.userName }}</p>
      <p>Email: {{ user.emailId }}</p>
      <p>Role: {{ user.role | uppercase }}</p>
    </div>
    <ng-template #noUser><p>User not found</p></ng-template>
    <a mat-button routerLink="/user">Back to User Dashboard</a>
  </mat-card-content>
</mat-card>
scss
// src/app/components/user-details/user-details.component.scss
.container {
  margin: 20px;
}

.loading {
  text-align: center;
  color: var(--primary-color);
}

.error {
  text-align: center;
  color: red;
}
Step 19: Update RegisterComponent
typescript
// src/app/components/register/register.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';
import { noSpacesValidator } from '../../validators/no-spaces.validator';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.scss'],
  standalone: true,
  imports: [
    ReactiveFormsModule,
    RouterLink,
    MatCardModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule,
    MatSelectModule
  ]
})
export class RegisterComponent {
  registerForm = new FormGroup({
    userName: new FormControl('', [Validators.required, Validators.minLength(3), noSpacesValidator()]),
    emailId: new FormControl('', [Validators.required, Validators.email]),
    role: new FormControl('user', [Validators.required])
  });
  isLoading = false;
  error: string | null = null;

  constructor(private userService: UserService, private router: Router) {}

  onSubmit() {
    if (this.registerForm.valid) {
      this.isLoading = true;
      const formValue = this.registerForm.value;
      const newUser = {
        id: Math.max(...this.localUsers.map(u => u.id), 0) + 1,
        userName: formValue.userName || '',
        emailId: formValue.emailId || '',
        role: formValue.role || 'user'
      };
      this.userService.addUser(newUser).subscribe({
        next: () => {
          this.registerForm.reset();
          this.registerForm.markAsPristine();
          this.router.navigate(['/user']);
        },
        error: err => {
          this.error = 'Failed to register user: ' + err.message;
          this.isLoading = false;
        },
        complete: () => {
          this.isLoading = false;
        }
      });
    }
  }

  private get localUsers(): User[] {
    let users: User[] = [];
    this.userService.getUsers().subscribe({
      next: (data) => users = data,
      error: (err) => console.error('Error fetching users:', err)
    });
    return users;
  }
}
typescript
// src/app/components/register/register.component.html
<mat-card class="container">
  <mat-card-header>
    <mat-card-title>User Registration</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <div *ngIf="error" class="error">{{ error }}</div>
    <div *ngIf="isLoading" class="loading">Registering user...</div>
    <form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
      <mat-form-field appearance="fill">
        <mat-label>Username</mat-label>
        <input matInput formControlName="userName">
        <mat-error *ngIf="registerForm.get('userName')?.hasError('required') && registerForm.get('userName')?.touched">
          Username is required
        </mat-error>
        <mat-error *ngIf="registerForm.get('userName')?.hasError('minlength') && registerForm.get('userName')?.touched">
          Username must be at least 3 characters
        </mat-error>
        <mat-error *ngIf="registerForm.get('userName')?.hasError('noSpaces') && registerForm.get('userName')?.touched">
          Username cannot contain spaces
        </mat-error>
      </mat-form-field>
      <mat-form-field appearance="fill">
        <mat-label>Email</mat-label>
        <input matInput formControlName="emailId">
        <mat-error *ngIf="registerForm.get('emailId')?.hasError('required') && registerForm.get('emailId')?.touched">
          Email is required
        </mat-error>
        <mat-error *ngIf="registerForm.get('emailId')?.hasError('email') && registerForm.get('emailId')?.touched">
          Invalid email format
        </mat-error>
      </mat-form-field>
      <mat-form-field appearance="fill">
        <mat-label>Role</mat-label>
        <mat-select formControlName="role">
          <mat-option value="user">User</mat-option>
          <mat-option value="admin">Admin</mat-option>
        </mat-select>
      </mat-form-field>
      <button mat-raised-button color="primary" type="submit" [disabled]="registerForm.invalid || isLoading">Register</button>
    </form>
    <a mat-button routerLink="/user">Back to User Dashboard</a>
  </mat-card-content>
</mat-card>
scss
// src/app/components/register/register.component.scss
.container {
  margin: 20px;
}

form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

mat-form-field {
  width: 100%;
  max-width: 400px;
}

.error {
  color: red;
  font-size: 0.9em;
}

.loading {
  text-align: center;
  color: var(--primary-color);
}
Step 20: Update LoginComponent
typescript
// src/app/components/login/login.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { AuthService } from '../../services/auth.service';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss'],
  standalone: true,
  imports: [
    FormsModule,
    RouterLink,
    MatCardModule,
    MatButtonModule,
    MatFormFieldModule,
    MatInputModule
  ]
})
export class LoginComponent {
  username = '';
  password = '';
  errorMessage = '';

  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  onSubmit() {
    if (this.authService.login(this.username, this.password)) {
      this.router.navigate(['/admin']);
    } else {
      this.errorMessage = 'Invalid username or password';
    }
  }
}
typescript
// src/app/components/login/login.component.html
<mat-card class="container">
  <mat-card-header>
    <mat-card-title>Admin Login</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <form (ngSubmit)="onSubmit()">
      <mat-form-field appearance="fill">
        <mat-label>Username</mat-label>
        <input matInput [(ngModel)]="username" name="username" required>
        <mat-error *ngIf="form.username?.errors?.['required'] && form.username?.touched">
          Username is required
        </mat-error>
      </mat-form-field>
      <mat-form-field appearance="fill">
        <mat-label>Password</mat-label>
        <input matInput [(ngModel)]="password" name="password" type="password" required>
        <mat-error *ngIf="form.password?.errors?.['required'] && form.password?.touched">
          Password is required
        </mat-error>
      </mat-form-field>
      <div *ngIf="errorMessage" class="error">{{ errorMessage }}</div>
      <button mat-raised-button color="primary" type="submit" [disabled]="!form.valid">Login</button>
    </form>
    <a mat-button routerLink="/user">Back to User Dashboard</a>
  </mat-card-content>
</mat-card>
scss
// src/app/components/login/login.component.scss
.container {
  margin: 20px;
}

form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

mat-form-field {
  width: 100%;
  max-width: 400px;
}

.error {
  color: red;
  font-size: 0.9em;
}
Step 21: Update Child Components
typescript
// src/app/components/admin-dashboard/admin-dashboard.component.ts
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';

@Component({
  selector: 'app-admin-dashboard',
  template: `
    <mat-card class="container">
      <mat-card-header>
        <mat-card-title>Admin Dashboard</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <p>Welcome to the Admin Dashboard!</p>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./admin-dashboard.component.scss'],
  standalone: true,
  imports: [MatCardModule]
})
export class AdminDashboardComponent {}
scss
// src/app/components/admin-dashboard/admin-dashboard.component.scss
.container {
  margin: 20px;
}
typescript
// src/app/components/admin-settings/admin-settings.component.ts
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';

@Component({
  selector: 'app-admin-settings',
  template: `
    <mat-card class="container">
      <mat-card-header>
        <mat-card-title>Admin Settings</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <p>Configure settings here.</p>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./admin-settings.component.scss'],
  standalone: true,
  imports: [MatCardModule]
})
export class AdminSettingsComponent {}
scss
// src/app/components/admin-settings/admin-settings.component.scss
.container {
  margin: 20px;
}
Step 22: Update NotFoundComponent
typescript
// src/app/components/not-found/not-found.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';

@Component({
  selector: 'app-not-found',
  template: `
    <mat-card class="container">
      <mat-card-header>
        <mat-card-title>404 - Page Not Found</mat-card-title>
      </mat-card-header>
      <mat-card-content>
        <a mat-button routerLink="/user">Go back to User Dashboard</a>
      </mat-card-content>
    </mat-card>
  `,
  styleUrls: ['./not-found.component.scss'],
  standalone: true,
  imports: [RouterLink, MatCardModule, MatButtonModule]
})
export class NotFoundComponent {}
scss
// src/app/components/not-found/not-found.component.scss
.container {
  margin: 20px;
  text-align: center;
}
Step 23: Update FilterByRolePipe
typescript
// src/app/pipes/filter-by-role.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { User } from '../models/user.model';

@Pipe({
  name: 'filterByRole',
  standalone: true
})
export class FilterByRolePipe implements PipeTransform {
  transform(users: User[], role: string): User[] {
    if (!users || role === 'all') return users;
    return users.filter(user => user.role === role);
  }
}
Step 24: Update no-spaces.validator.ts
typescript
// src/app/validators/no-spaces.validator.ts
import { AbstractControl, ValidatorFn } from '@angular/forms';

export function noSpacesValidator(): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    const hasSpaces = control.value?.includes(' ');
    return hasSpaces ? { noSpaces: true } : null;
  };
}
Step 25: Test the Application
  1. Run ng serve and visit http://localhost:4200.
  2. Test features:
    • Login (/login):
      • Log in with admin1/admin123. Verify redirection to /admin.
      • Try invalid credentials (e.g., wrong/wrong). Confirm error message.
    • Admin Route (/admin):
      • Attempt to access /admin without logging in. Confirm redirection to /login.
      • Log in as admin and access /admin. Verify access to dashboard and settings.
    • Logout (Header):
      • Log in, then click “Logout” in the header. Confirm redirection to /user and cleared user state (check local storage).
    • Register (/register):
      • Enter form data, then try navigating away without submitting. Confirm the CanDeactivate prompt.
      • Submit a valid form and verify the new user appears in the user list.
    • User Dashboard (/user):
      • Select a user and verify state management.
    • User Details (/user/:id):
      • View user details.
    • 404 Page (/invalid):
      • Confirm the Not Found page appears.

Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: Incorrect import in main.ts.Fix:
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, provideHttpClient } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideAnimations } from '@angular/platform-browser/animations';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideAnimations()
  ]
}).catch(err => console.error(err));
Steps:
  1. Ensure AppComponent is exported in app.component.ts.
  2. Verify file paths.
  3. Run ng serve.
Guard Not Restricting AccessCause: Incorrect guard implementation or route configuration.Fix:
  • Ensure AuthGuard is applied to the /admin route in app.routes.ts.
  • Verify AuthService returns correct authentication state.
  • Check isAdmin() logic in AuthService.
Local Storage Not PersistingCause: Incorrect serialization or clearing of local storage.Fix:
  • Ensure JSON.stringify and JSON.parse are used in StorageService.
  • Verify setLocalStorage and clearLocalStorage are called correctly.

Best Practices for Authentication & Guards
  • Centralize Auth Logic: Use AuthService for all authentication tasks.
  • Secure Storage: For production, use secure methods (e.g., JWT with HttpOnly cookies) instead of local storage.
  • Granular Guards: Create separate guards for different roles or conditions.
  • Clean Logout: Clear all relevant state and storage on logout.
  • User Feedback: Show loading states and error messages for login failures.
  • Accessibility: Ensure login forms are accessible with ARIA labels.

0 comments:

Featured Post

Master Angular 20 Basics: A Complete Beginner’s Guide with Examples and Best Practices

Welcome to the complete Angular 20 learning roadmap ! This series takes you step by step from basics to intermediate concepts , with hands...

Subscribe

 
Toggle Footer
Top