JavaScript + Angular-compatible version of loan amortization calculator that you can integrate into an Angular component or service

 

JavaScript Version of Loan Amortization

1. Loan Calculator Function (Pure JS/TS)

export function calculateLoanSchedule( principal: number, annualRate: number, years: number, extraPayment: number = 0 ) { const monthlyRate = annualRate / 12 / 100; const months = years * 12; const monthlyPayment = (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -months)); const schedule = []; let balance = principal; let totalInterest = 0; let month = 1; while (balance > 0 && month <= months) { const interest = balance * monthlyRate; let principalPayment = monthlyPayment - interest + extraPayment; if (principalPayment > balance) { principalPayment = balance; } balance -= principalPayment; totalInterest += interest; schedule.push({ month, payment: parseFloat((monthlyPayment + extraPayment).toFixed(2)), principal: parseFloat(principalPayment.toFixed(2)), interest: parseFloat(interest.toFixed(2)), balance: parseFloat(balance.toFixed(2)) }); month++; } const totalPaid = schedule.reduce((sum, p) => sum + p.payment, 0); return { schedule, totalPaid: parseFloat(totalPaid.toFixed(2)), totalInterest: parseFloat(totalInterest.toFixed(2)), totalMonths: schedule.length }; }

2. Example Usage in Angular Component

import { Component, OnInit } from '@angular/core'; import { calculateLoanSchedule } from './loan-calculator'; @Component({ selector: 'app-loan-schedule', templateUrl: './loan-schedule.component.html', }) export class LoanScheduleComponent implements OnInit { loanData: any; schedule: any[] = []; ngOnInit(): void { const principal = 300000; const annualRate = 6.0; const years = 30; const extraPayment = 200; this.loanData = calculateLoanSchedule(principal, annualRate, years, extraPayment); this.schedule = this.loanData.schedule; console.log('Loan Summary:', this.loanData); } }

3. Optional Template Snippet

<table> <thead> <tr> <th>Month</th> <th>Payment</th> <th>Principal</th> <th>Interest</th> <th>Balance</th> </tr> </thead> <tbody> <tr *ngFor="let row of schedule"> <td>{{ row.month }}</td> <td>{{ row.payment | currency }}</td> <td>{{ row.principal | currency }}</td> <td>{{ row.interest | currency }}</td> <td>{{ row.balance | currency }}</td> </tr> </tbody> </table>

1. Create the Service

Run this command to generate a service:

ng generate service services/loan-calculator

Or manually create:

src/app/services/loan-calculator.service.ts

import { Injectable } from '@angular/core'; export interface LoanScheduleEntry { month: number; payment: number; principal: number; interest: number; balance: number; } export interface LoanSummary { schedule: LoanScheduleEntry[]; totalPaid: number; totalInterest: number; totalMonths: number; payoffYears: number; payoffMonths: number; } @Injectable({ providedIn: 'root', }) export class LoanCalculatorService { constructor() {} calculateLoanSchedule( principal: number, annualRate: number, years: number, extraPayment: number = 0 ): LoanSummary { const monthlyRate = annualRate / 12 / 100; const totalMonths = years * 12; const monthlyPayment = (principal * monthlyRate) / (1 - Math.pow(1 + monthlyRate, -totalMonths)); const schedule: LoanScheduleEntry[] = []; let balance = principal; let totalInterest = 0; let month = 1; while (balance > 0 && month <= totalMonths) { const interest = balance * monthlyRate; let principalPayment = monthlyPayment - interest + extraPayment; if (principalPayment > balance) { principalPayment = balance; } balance -= principalPayment; totalInterest += interest; schedule.push({ month, payment: parseFloat((monthlyPayment + extraPayment).toFixed(2)), principal: parseFloat(principalPayment.toFixed(2)), interest: parseFloat(interest.toFixed(2)), balance: parseFloat(balance.toFixed(2)), }); month++; } const totalPaid = schedule.reduce((sum, entry) => sum + entry.payment, 0); const monthsTaken = schedule.length; return { schedule, totalPaid: parseFloat(totalPaid.toFixed(2)), totalInterest: parseFloat(totalInterest.toFixed(2)), totalMonths: monthsTaken, payoffYears: Math.floor(monthsTaken / 12), payoffMonths: monthsTaken % 12, }; } }

✅ 2. Usage in a Component

src/app/components/loan.component.ts

import { Component, OnInit } from '@angular/core'; import { LoanCalculatorService, LoanSummary } from 'src/app/services/loan-calculator.service'; @Component({ selector: 'app-loan', templateUrl: './loan.component.html', }) export class LoanComponent implements OnInit { loanSummary!: LoanSummary; constructor(private loanCalculator: LoanCalculatorService) {} ngOnInit(): void { const principal = 300000; const annualRate = 6.0; const years = 30; const extraPayment = 200; this.loanSummary = this.loanCalculator.calculateLoanSchedule( principal, annualRate, years, extraPayment ); console.log(this.loanSummary); } }

✅ 3. Template Example (loan.component.html)

<h2>Loan Summary</h2> <p>Total Paid: {{ loanSummary.totalPaid | currency }}</p> <p>Total Interest: {{ loanSummary.totalInterest | currency }}</p> <p>Loan Paid Off In: {{ loanSummary.payoffYears }} years and {{ loanSummary.payoffMonths }} months</p> <table> <thead> <tr> <th>Month</th> <th>Payment</th> <th>Principal</th> <th>Interest</th> <th>Balance</th> </tr> </thead> <tbody> <tr *ngFor="let row of loanSummary.schedule"> <td>{{ row.month }}</td> <td>{{ row.payment | currency }}</td> <td>{{ row.principal | currency }}</td> <td>{{ row.interest | currency }}</td> <td>{{ row.balance | currency }}</td> </tr> </tbody> </table>

complete SQL Server guide with queries from start to end covering how to create tables, add/drop columns, insert/update/delete data, and manage advanced features like constraints, indexes, and stored procedures. This is your go-to reference for working with SQL Server

.

🧱 1. Create Table

sql

CREATE TABLE Employee (
    EmpId INT PRIMARY KEY,
    FirstName VARCHAR(50),
    LastName VARCHAR(50),
    DOB DATE,
    Salary DECIMAL(18,2)
);

2. Add Columns

sql
ALTER TABLE Employee
ADD Department VARCHAR(50),
    JoiningDate DATE;

🧹 3. Drop Columns

sql
ALTER TABLE Employee
DROP COLUMN JoiningDate

4. Modify Column Data Type

sql
ALTER TABLE Employee
ALTER COLUMN Salary FLOAT;

📥 5. Insert Data

sql
INSERT INTO Employee (EmpId, FirstName, LastName, DOB, Salary, Department)
VALUES (101, 'John', 'Doe', '1990-05-15', 75000, 'HR');

🧾 6. Update Data

sql
UPDATE Employee
SET Salary = 80000
WHERE EmpId = 101;

❌ 7. Delete Data

sql
DELETE FROM Employee
WHERE EmpId = 101;

🔍 8. Select Data

sql
SELECT * FROM Employee;
SELECT FirstName, Salary FROM Employee WHERE Department = 'HR';

🔐 9. Add Constraints

Unique Constraint:

sql
ALTER TABLE Employee
ADD CONSTRAINT UQ_Employee_Email UNIQUE (Email);

Default Value:

sql
ALTER TABLE Employee
ADD CONSTRAINT DF_Employee_Salary DEFAULT 50000 FOR Salary;

🧱 10. Add Index

sql
CREATE INDEX IX_Employee_Department ON Employee(Department);


11. Create Stored Procedure

sql
CREATE PROCEDURE InsertEmployee
    @EmpId INT,
    @FirstName VARCHAR(50),
    @LastName VARCHAR(50),
    @DOB DATE,
    @Salary DECIMAL(18,2),
    @Department VARCHAR(50)
AS
BEGIN
    INSERT INTO Employee (EmpId, FirstName, LastName, DOB, Salary, Department)
    VALUES (@EmpId, @FirstName, @LastName, @DOB, @Salary, @Department);
END;

🧪 12. Execute Stored Procedure

sql
EXEC InsertEmployee 
    @EmpId = 102, 
    @FirstName = 'Jane', 
    @LastName = 'Smith', 
    @DOB = '1985-03-22', 
    @Salary = 85000, 
    @Department = 'Finance';

🧾 13. Create Table-Valued Parameter (TVP)

Create Type:

sql
CREATE TYPE EmployeeType AS TABLE (
    EmpId INT,
    FirstName VARCHAR(50),
    LastName VARCHAR(50),
    DOB DATE,
    Salary DECIMAL(18,2),
    Department VARCHAR(50)
);

Create Procedure:

sql
CREATE PROCEDURE InsertEmployeesBulk
    @Employees EmployeeType READONLY
AS
BEGIN
    INSERT INTO Employee
    SELECT * FROM @Employees;
END;

🔄 14. Drop Table or Type

sql
DROP TABLE Employee;
DROP TYPE EmployeeType;

🧰 15. Other Useful Features

Rename Column:

sql
EXEC sp_rename 'Employee.FirstName', 'GivenName', 'COLUMN';

Rename Table:

sql
EXEC sp_rename 'Employee', 'Staff';

View Table Structure:

sql
EXEC sp_help 'Employee';

Check Constraints:

sql
SELECT * FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE
WHERE TABLE_NAME = 'Employee'

VARCHAR(n) vs NVARCHAR(n) vs VARCHAR(MAX) vs NVARCHAR(MAX)

 let's break down the differences between NVARCHAR(MAX) and VARCHAR, and also compare other related types like NVARCHAR(n), VARCHAR(n), and when to use each.


🔍 1. VARCHAR vs NVARCHAR: Core Difference

FeatureVARCHARNVARCHAR
Character setNon-Unicode (uses code page)Unicode (UTF-16)
Bytes per character1 byte (usually)2 bytes per character
Language supportLimited to specific localesSupports all languages (e.g. Hindi, Chinese, Arabic)
Storage sizeSmaller (if only English/ASCII)Larger, but universal
Use caseEnglish or single language dataMulti-language or unknown language data

📏 2. VARCHAR(n) vs NVARCHAR(n) vs VARCHAR(MAX) vs NVARCHAR(MAX)

Data TypeMax LengthUnicodeStorage
VARCHAR(n)Up to 8,000 characters❌ NoUp to 8,000 bytes
NVARCHAR(n)Up to 4,000 characters✅ YesUp to 8,000 bytes (2 bytes/char)
VARCHAR(MAX)Up to 2^31-1 chars (~2 GB)❌ NoLOB (Large Object) storage
NVARCHAR(MAX)Up to 2^30 chars (~2 GB)✅ YesLOB storage

📌 Notes:

  • Use MAX only when you expect text > 8,000 characters.

  • NVARCHAR(MAX) allows for large multilingual documents, e.g. descriptions, logs, or chat histories.

  • VARCHAR(MAX) is better if you're storing long ASCII or English text only.


🔄 3. Performance Differences

OperationVARCHAR(n) / NVARCHAR(n)VARCHAR(MAX) / NVARCHAR(MAX)
In-memory operationsFastSlower (may spill to disk)
Indexable?✅ Yes (full indexing)⚠️ Limited (not fully indexable)
Can be used in temp tables?✅ Yes✅ Yes (but slower)
Can be used in computed columns / filters?✅ Yes⚠️ Not always

Important: Avoid MAX unless you really need it. It can cause:

  • Query plan degradation

  • Table scans

  • Limited indexing

  • Extra I/O overhead


🧪 4. Examples

-- VARCHAR vs NVARCHAR (basic) DECLARE @v1 VARCHAR(50) = 'Hello'; -- ASCII only DECLARE @v2 NVARCHAR(50) = N'नमस्ते'; -- Unicode required (N prefix) -- Incorrect: this will not store Unicode properly DECLARE @wrong VARCHAR(50) = N'你好'; -- Will become ???
-- Use MAX when needed CREATE TABLE Articles ( ArticleID INT, Title NVARCHAR(255), Content NVARCHAR(MAX) -- Long content, possibly multi-language );

✅ When to Use Each?

ScenarioRecommended Type
English names, emailsVARCHAR(255)
International names, messagesNVARCHAR(255)
Very long text (e.g. documents, chat logs)NVARCHAR(MAX)
JSON/XML (long) in EnglishVARCHAR(MAX)
JSON/XML with multilingual textNVARCHAR(MAX)

❗️ Caution: Literal String Prefix N

If you're inserting Unicode strings, always use the N prefix:

-- Correct INSERT INTO Users (Name) VALUES (N'José'); -- NVARCHAR -- Wrong INSERT INTO Users (Name) VALUES ('José'); -- VARCHAR: may cause encoding issue

Start With a Tutorial-Based OS

 These tutorials walk you through building an OS from scratch:

realistic, high-quality Angular service example from scratch, based on common real-world needs.

 

🎯 Use Case: UserService

Goal: Create a service that:

  • Fetches user profile from an API

  • Updates the user’s profile

  • Calculates the user's age

  • Caches the user data in a BehaviorSubject for shared use

  • Handles errors cleanly

  • Is testable and easy to maintain


📦 1. Define the User Model

// models/user.model.ts export interface User { id: string; name: string; email: string; dateOfBirth: string; // ISO date string phone?: string; }

🛠️ 2. Create the UserService

// services/user.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, map, of, throwError } from 'rxjs'; import { User } from '../models/user.model'; @Injectable({ providedIn: 'root' }) export class UserService { private userSubject = new BehaviorSubject<User | null>(null); public user$ = this.userSubject.asObservable(); private readonly API_URL = '/api/users'; constructor(private http: HttpClient) {} /** * Loads user from backend and updates local state */ loadUser(userId: string): Observable<User> { return this.http.get<User>(`${this.API_URL}/${userId}`).pipe( map(user => { this.userSubject.next(user); return user; }), catchError(error => { console.error('Error loading user', error); return throwError(() => new Error('User load failed')); }) ); } /** * Returns current user snapshot (not observable) */ getCurrentUser(): User | null { return this.userSubject.value; } /** * Updates user and refreshes local cache */ updateUser(user: Partial<User>): Observable<User> { const currentUser = this.getCurrentUser(); if (!currentUser) return throwError(() => new Error('No user loaded')); return this.http.put<User>(`${this.API_URL}/${currentUser.id}`, user).pipe( map(updatedUser => { this.userSubject.next(updatedUser); return updatedUser; }), catchError(error => { console.error('Error updating user', error); return throwError(() => new Error('User update failed')); }) ); } /** * Calculates age from dateOfBirth */ calculateAge(dateOfBirth: string): number { const dob = new Date(dateOfBirth); const today = new Date(); let age = today.getFullYear() - dob.getFullYear(); const m = today.getMonth() - dob.getMonth(); if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) { age--; } return age; } }

📘 3. How a Component Would Use It

// profile.component.ts export class ProfileComponent implements OnInit { user$ = this.userService.user$; age: number | null = null; constructor(private userService: UserService) {} ngOnInit() { this.userService.loadUser('12345').subscribe(user => { this.age = this.userService.calculateAge(user.dateOfBirth); }); } updatePhoneNumber(newPhone: string) { this.userService.updateUser({ phone: newPhone }).subscribe(updated => { console.log('Updated:', updated); }); } }

✅ Why This Is High-Level and Clean

FeatureBenefit
BehaviorSubject cachingState is shareable and reactive across components
✅ Separation of logicAPI, business logic (age calc), and UI are well-separated
✅ Clear, meaningful methodsEasy to understand and self-documenting
✅ Centralized error handlingErrors managed at the service layer, reducing repetition
✅ TestablePure functions (calculateAge) and DI-based logic are easy to unit test

🧪 4. Test Example (for calculateAge)

it('should calculate correct age', () => { const service = new UserService({} as any); // mock HttpClient const age = service.calculateAge('2000-01-01'); const thisYear = new Date().getFullYear(); expect(age).toBe(thisYear - 2000); });

🔚 Final Thoughts

This service is:

  • Modular

  • Testable

  • Scalable

  • Follows Angular and SOLID best practices 



Advanced UserService with Role-Based Access and Token Auth

We'll build on the original UserService to include:

✅ Features:

  1. JWT token integration (e.g., get token from AuthService)

  2. Role-based logic (isAdmin(), hasRole())

  3. Guard-ready helper methods

  4. Secure user update with token header


📁 Project Structure Update

src/ ├── models/ │ └── user.model.ts ├── services/ │ ├── auth.service.ts ✅ New │ └── user.service.ts

👤 Updated User Model with Roles

// models/user.model.ts export interface User { id: string; name: string; email: string; dateOfBirth: string; roles: string[]; // ['user', 'admin', ...] token?: string; // Optional JWT token }

🔐 AuthService – Manages JWT Token

// services/auth.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly TOKEN_KEY = 'auth_token'; setToken(token: string) { localStorage.setItem(this.TOKEN_KEY, token); } getToken(): string | null { return localStorage.getItem(this.TOKEN_KEY); } clearToken() { localStorage.removeItem(this.TOKEN_KEY); } isAuthenticated(): boolean { return !!this.getToken(); } }

🚀 Updated Advanced UserService

// services/user.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, map, throwError } from 'rxjs'; import { User } from '../models/user.model'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root' }) export class UserService { private userSubject = new BehaviorSubject<User | null>(null); user$ = this.userSubject.asObservable(); private readonly API_URL = '/api/users'; constructor(private http: HttpClient, private auth: AuthService) {} /** * Load user and set token */ loadUser(userId: string): Observable<User> { const headers = this.getAuthHeaders(); return this.http.get<User>(`${this.API_URL}/${userId}`, { headers }).pipe( map(user => { this.userSubject.next(user); if (user.token) this.auth.setToken(user.token); // Store token if returned return user; }), catchError(error => { console.error('Error loading user', error); return throwError(() => new Error('Failed to load user')); }) ); } getCurrentUser(): User | null { return this.userSubject.value; } updateUser(data: Partial<User>): Observable<User> { const currentUser = this.getCurrentUser(); if (!currentUser) return throwError(() => new Error('No user loaded')); return this.http.put<User>(`${this.API_URL}/${currentUser.id}`, data, { headers: this.getAuthHeaders() }).pipe( map(user => { this.userSubject.next(user); return user; }), catchError(error => { console.error('Error updating user', error); return throwError(() => new Error('Update failed')); }) ); } calculateAge(dob: string): number { const birth = new Date(dob); const today = new Date(); let age = today.getFullYear() - birth.getFullYear(); const m = today.getMonth() - birth.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) { age--; } return age; } /** * Checks if current user has a specific role */ hasRole(role: string): boolean { const user = this.getCurrentUser(); return !!user?.roles.includes(role); } /** * Shortcut for admin check */ isAdmin(): boolean { return this.hasRole('admin'); } /** * Prepares auth headers using token */ private getAuthHeaders(): HttpHeaders { const token = this.auth.getToken(); return new HttpHeaders({ 'Authorization': `Bearer ${token ?? ''}` }); } }

🔒 Bonus: Can Be Used in Guards or Route Conditions

// auth.guard.ts canActivate(): boolean { return this.userService.isAdmin(); // Only admins allowed }

🧪 Optional Unit Test Example (hasRole)

it('should return true if user has role', () => { const service = new UserService({} as any, { getToken: () => 'token123' } as any); (service as any).userSubject.next({ id: '1', name: 'Test', email: '', dateOfBirth: '', roles: ['admin', 'user'] }); expect(service.hasRole('admin')).toBeTrue(); expect(service.hasRole('editor')).toBeFalse(); });

✅ Summary: What You Now Have

FeatureBenefit
✅ JWT integrationSecure authenticated requests
✅ Role-based logicCentralized access control helpers
BehaviorSubject stateReactive and shared user state
✅ Error handlingSafer and more maintainable
✅ Easy to test and reuseServices are self-contained, pure, and injectable

⏭️ Want to Go Further?

Here are some next steps if you want:

  1. ✅ Add refresh token and token expiration logic

  2. ✅ Protect routes using CanActivate guards with role checks

  3. ✅ Store state in NgRx or SignalStore (for larger apps)

  4. ✅ Add interceptors to automatically inject the token into all HTTP calls

  5. ✅ Implement logout flow


Let me know if you want help with any of those next steps — I can create examples too.


Great — let’s continue by adding the next advanced features one by one. Since you said yes, here’s what we’ll build next:


🔄 Next Advanced Features:

  1. Token Auto-Injection using HTTP Interceptor

  2. Role-Based Route Protection using AuthGuard with CanActivate

  3. Logout and Session Expiry Handling

  4. ✅ (Optional) Token Refresh Flow (JWT refresh tokens)

Let’s tackle each part cleanly and step-by-step.


🔐 1. HTTP Interceptor – Auto-Add JWT to All Requests

✅ What It Does:

  • Automatically attaches Authorization: Bearer <token> header to every HTTP request

  • Centralizes token logic instead of adding headers manually in each service


📦 Create AuthInterceptor

// interceptors/auth.interceptor.ts import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AuthService } from '../services/auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private auth: AuthService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const token = this.auth.getToken(); if (token) { const cloned = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); return next.handle(cloned); } return next.handle(req); } }

🔗 Register Interceptor in App Module

// app.module.ts import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './interceptors/auth.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule {}

✅ Now all services (e.g., UserService) no longer need to manually set headers.


🛡️ 2. AuthGuard – Protect Routes by Role

✅ Goal:

Allow/deny access based on user role (like 'admin', 'editor', etc.)


📦 Create AuthGuard

// guards/auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { UserService } from '../services/user.service'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { constructor(private userService: UserService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { const requiredRole = route.data['role'] as string; if (!this.userService.getCurrentUser()) { this.router.navigate(['/login']); return false; } if (requiredRole && !this.userService.hasRole(requiredRole)) { this.router.navigate(['/unauthorized']); return false; } return true; } }

🧭 Usage in Routes

// app-routing.module.ts const routes: Routes = [ { path: 'admin', component: AdminPanelComponent, canActivate: [AuthGuard], data: { role: 'admin' } // only admin can access }, { path: 'profile', component: ProfileComponent, canActivate: [AuthGuard] } ];

🚪 3. Logout and Session Expiry Handling

✅ AuthService: Add logout logic

logout() { this.clearToken(); // Optionally clear other data like BehaviorSubjects }

✅ UserService: Add logout and cleanup

logout() { this.auth.clearToken(); this.userSubject.next(null); }

⛔ Handle Token Expiry (Basic Way)

Inside AuthInterceptor, detect expired token (e.g., 401 error) and redirect:

import { catchError } from 'rxjs/operators'; import { of, throwError } from 'rxjs'; intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const token = this.auth.getToken(); const cloned = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req; return next.handle(cloned).pipe( catchError(err => { if (err.status === 401) { this.auth.clearToken(); this.router.navigate(['/login']); } return throwError(() => err); }) ); }

🔁 4. (Optional) Refresh Token Flow

If your backend returns:

  • an access token (short-lived)

  • and a refresh token (longer-lived),

Then you can:

  • store the refresh token securely (e.g., HttpOnly cookie)

  • and use a token refresh endpoint (/auth/refresh) when access token expires

Let me know if you want me to implement that too — it’s more advanced and uses token queuing logic to prevent race conditions.


✅ Summary of What You Now Have

FeatureDescription
✅ Auth InterceptorAdds token to all HTTP requests automatically
✅ Auth GuardProtects routes based on login + roles
✅ Role-based accessEasy to add .hasRole('admin') logic
✅ Logout and cleanupCentralized logout + session cleanup
✅ Token Expiry handlingDetect 401s, auto-redirect to login

⏭️ Want to Add Next?

Here are options you can choose:

  1. 🔄 Implement refresh token flow

  2. 🧪 Write unit tests for the guards/interceptor

  3. 💡 Show Signal-based global state instead of BehaviorSubject

  4. 🧰 Build a TokenService to manage both access + refresh cleanly

Let me know what you'd like next — or if you'd prefer a GitHub project template version of all this.


Perfect — since you've said yes, let's now implement a full Refresh Token Flow (step 4), which is a more advanced and realistic solution for production-level Angular apps that use JWT authentication.


🔄 What Is the Refresh Token Flow?

In modern JWT-based authentication:

  • 🔐 Access Token: short-lived (e.g. 5–15 mins)

  • ♻️ Refresh Token: long-lived (e.g. 1–7 days), used to get a new access token when it expires

✅ Goal:

Automatically refresh the access token when it expires, without logging the user out or requiring a manual login.


🧱 What We'll Build:

  1. TokenService: Stores and manages access/refresh tokens

  2. ✅ Updated AuthService: Handles login, logout, and token refresh

  3. AuthInterceptor:

    • Adds access token to all requests

    • Detects 401 errors

    • Refreshes token and retries request

  4. ✅ Backend expected endpoints:

    • POST /auth/login

    • POST /auth/refresh with refresh token

    • POST /auth/logout


1. 🧰 TokenService

// services/token.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class TokenService { private ACCESS = 'access_token'; private REFRESH = 'refresh_token'; setTokens(accessToken: string, refreshToken: string) { localStorage.setItem(this.ACCESS, accessToken); localStorage.setItem(this.REFRESH, refreshToken); } getAccessToken(): string | null { return localStorage.getItem(this.ACCESS); } getRefreshToken(): string | null { return localStorage.getItem(this.REFRESH); } clearTokens() { localStorage.removeItem(this.ACCESS); localStorage.removeItem(this.REFRESH); } }

2. 🔐 AuthService

// services/auth.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { TokenService } from './token.service'; import { Observable, tap } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthService { private readonly API = '/api/auth'; constructor(private http: HttpClient, private tokenService: TokenService) {} login(email: string, password: string): Observable<any> { return this.http.post<any>(`${this.API}/login`, { email, password }).pipe( tap(tokens => { this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); }) ); } logout(): Observable<any> { return this.http.post(`${this.API}/logout`, {}).pipe( tap(() => this.tokenService.clearTokens()) ); } refreshToken(): Observable<any> { const refreshToken = this.tokenService.getRefreshToken(); return this.http.post<any>(`${this.API}/refresh`, { refreshToken }).pipe( tap(tokens => { this.tokenService.setTokens(tokens.accessToken, tokens.refreshToken); }) ); } isLoggedIn(): boolean { return !!this.tokenService.getAccessToken(); } }

3. 🚀 AuthInterceptor with Token Refresh Logic

// interceptors/auth.interceptor.ts import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, catchError, filter, switchMap, take, throwError, BehaviorSubject } from 'rxjs'; import { AuthService } from '../services/auth.service'; import { TokenService } from '../services/token.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { private isRefreshing = false; private refreshTokenSubject = new BehaviorSubject<string | null>(null); constructor(private auth: AuthService, private tokenService: TokenService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const token = this.tokenService.getAccessToken(); const cloned = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req; return next.handle(cloned).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401 && !cloned.url.endsWith('/auth/refresh')) { return this.handle401Error(cloned, next); } return throwError(() => error); }) ); } private handle401Error(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (!this.isRefreshing) { this.isRefreshing = true; this.refreshTokenSubject.next(null); return this.auth.refreshToken().pipe( switchMap((tokens) => { this.isRefreshing = false; this.refreshTokenSubject.next(tokens.accessToken); return next.handle(this.addToken(req, tokens.accessToken)); }), catchError((err) => { this.isRefreshing = false; this.tokenService.clearTokens(); return throwError(() => err); }) ); } else { return this.refreshTokenSubject.pipe( filter(token => token != null), take(1), switchMap(token => next.handle(this.addToken(req, token!))) ); } } private addToken(req: HttpRequest<any>, token: string): HttpRequest<any> { return req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }); } }

4. ✅ Register Interceptor in AppModule

// app.module.ts import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './interceptors/auth.interceptor'; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class AppModule {}

💡 What Backend Should Support

Your API should expose:

EndpointPurpose
POST /auth/loginAuthenticates user, returns tokens
POST /auth/refreshAccepts refresh token, returns new access token
POST /auth/logoutInvalidates refresh token (optional)

✅ Final Result

  • 💡 Access tokens are auto-added to all requests

  • ♻️ Expired tokens are auto-refreshed behind the scenes

  • 🛡️ User stays logged in until refresh token expires

  • 🧠 No need to handle token logic in components or services


K

JavaScript + Angular-compatible version of loan amortization calculator that you can integrate into an Angular component or service

  JavaScript Version of Loan Amortization 1. Loan Calculator Function (Pure JS/TS) export function calculateLoanSchedule ( principal:...

Best for you