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
Route Guards (CanActivate, CanDeactivate)Angular’s route guards control navigation by allowing or denying access to routes based on conditions.CanActivateCanDeactivate
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:Explanation:
Restricting Admin RouteWe’ll create an AuthGuard to restrict the /admin route to authenticated admin users, redirecting unauthenticated users to the login page.Example: AuthGuardExplanation:
Hands-On Project: Restrict /admin Access to Authenticated AdminsLet’s enhance your angular-routing-demo project by:Step 3: Create AuthServiceStep 4: Create AuthGuardStep 5: Create CanDeactivateGuardStep 6: Update User ModelStep 7: Update StateServiceStep 8: Update StorageServiceStep 9: Update UserServiceStep 10: Update app.routes.tsApply the AuthGuard to the /admin route and CanDeactivateGuard to the /register route.Step 11: Update main.tsStep 12: Update AppComponentStep 13: Update HeaderComponentAdd logout functionality and display the logged-in user.Step 14: Update FooterComponentStep 15: Update AdminComponentStep 16: Update UserComponentStep 17: Update UserInfoComponentStep 18: Update UserDetailsComponentStep 19: Update RegisterComponentStep 20: Update LoginComponentStep 21: Update Child ComponentsStep 22: Update NotFoundComponentStep 23: Update FilterByRolePipeStep 24: Update no-spaces.validator.tsStep 25: Test the Application
Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: Incorrect import in main.ts.Fix:Steps:
Best Practices for Authentication & Guards
- 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.
- User logs in via a login form (e.g., username/password).
- Validate credentials (e.g., against a service or API).
- Store authentication state in local storage.
- Use route guards to protect restricted routes.
- 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).
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;
}
}
- 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).
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.
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';
}
}
- 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;
}
}
- 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:
- Creating an AuthService for login/logout functionality.
- Implementing an AuthGuard to restrict /admin access.
- Updating the LoginComponent to use the AuthService.
- Adding logout functionality to the HeaderComponent.
- Optionally applying a CanDeactivate guard to the RegisterComponent for unsaved changes.
- Maintaining integration with freeapi.miniprojectideas.com, StateService, and Angular Material from Modules 9 and 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.
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;
}
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';
}
}
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;
}
}
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;
}
}
typescript
// src/app/models/user.model.ts
export interface User {
id: number;
userName: string;
emailId: string;
role: string;
}
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();
}
}
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);
}
}
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);
}
}
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 }
];
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));
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 */
}
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;
}
}
}
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>© 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;
}
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;
}
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;
}
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;
}
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;
}
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);
}
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;
}
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;
}
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;
}
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);
}
}
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;
};
}
- Run ng serve and visit http://localhost:4200.
- 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.
- Login (/login):
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));
- Ensure AppComponent is exported in app.component.ts.
- Verify file paths.
- Run ng serve.
- Ensure AuthGuard is applied to the /admin route in app.routes.ts.
- Verify AuthService returns correct authentication state.
- Check isAdmin() logic in AuthService.
- 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:
Post a Comment