A production-deployed, full-stack expense management platform built with Angular 21 and ASP.NET Core (.NET 10) following Clean Architecture principles. Handles the complete expense lifecycle — from employee submission to manager approval — with real-time role enforcement, JWT security, file uploads, and a live analytics dashboard.
- Live Demo
- Project Highlights
- Features
- Tech Stack
- Architecture
- Role Permissions
- API Reference
- Getting Started
- Deployment
- CI/CD
- Author
| Service | URL |
|---|---|
| Frontend (Angular SPA) | https://zealous-coast-0c7b24903.7.azurestaticapps.net |
| Backend REST API | https://expensemgmt-api.azurewebsites.net |
| Interactive Swagger Docs | https://expensemgmt-api.azurewebsites.net/swagger |
Backend runs on Azure App Service (Linux, F1) connected to Azure SQL Database. Frontend is served via Azure Static Web Apps with global CDN. Both services auto-deploy via GitHub Actions on every push to
master.
These are the technically interesting decisions made in this project — not just CRUD features:
-
Real-time role invalidation without a blocklist — Rather than maintaining a token blocklist, the backend hooks into
OnTokenValidatedJWT events and queries the database on every authenticated request. The moment an admin demotes a user, their next API call returns401and the Angular interceptor silently logs them out. No polling, no expiry delay. -
Clean Architecture with zero cross-layer leakage — The
Domainlayer has no NuGet dependencies whatsoever.Applicationdefines all interfaces;Infrastructureimplements them. Controllers never touch EF Core directly. -
Signals-first Angular — The entire frontend uses Angular's Signals API (
signal(),computed(),effect()) withOnPushchange detection. NoBehaviorSubjectwrappers on mutable state. -
Production secrets management — Connection strings and JWT keys live exclusively in Azure App Service Application Settings.
appsettings.Production.jsonis gitignored. The committedappsettings.jsoncontains only empty placeholders. -
Atomic receipt management — Uploading a new receipt replaces the old file on disk and updates the record in a single operation. Deleting an expense also deletes its receipt file, preventing orphaned storage.
- JWT Bearer tokens with refresh token rotation — short-lived access tokens, long-lived encrypted refresh tokens
- Passwords BCrypt-hashed before storage — never stored in plain text
- Real-time role enforcement via
OnTokenValidated— role/status changes take effect on the next request, not at token expiry - Refresh token cleared on role change to block silent re-authentication
- Rate limiting: 100 requests per minute per IP (fixed window)
- CORS restricted to the Angular origin only
- Input validation via Data Annotations on every DTO
- 6 KPI cards: All-Time Total, This Month, This Year, Approved, Pending, Rejected — all currency-formatted
- Monthly bar chart — spending trend over the last 12 months (Chart.js)
- Category doughnut chart — spending breakdown by category
- Budget utilization bars — per-category actual spend vs monthly budget, red-highlighted when over budget
- Recent activity feed — last 5 expenses with status badges, amounts, submitter names, and categories
- Employees submit expenses; Admins & Managers view and manage all
- Approval workflow:
Pending → ApprovedorPending → Rejectedwith an optional rejection reason recorded - Receipt upload (Admin/Manager): JPG, JPEG, PNG, PDF — max 5 MB — old file replaced on re-upload, deleted on expense removal
- Edit expenses in
Pendingstatus with inline receipt update - Admin-only hard delete
- Paginated list with full-text search (title, description, date range) and filter by category and status
- Color-coded status badges (green / amber / red)
- Create, edit, activate, deactivate, soft-delete, and hard-delete categories
- Set monthly and/or yearly budgets per category — both are optional but at least one required when a budget is provided
- Paginated list with search and active/inactive filter
- View all users with role, status, and registration date
- Inline role change via color-coded dropdown (Admin = indigo, Manager = amber, Employee = teal) — triggers immediate JWT invalidation for the affected user
- Activate / deactivate accounts (inactive users cannot log in)
- Delete users — with safeguards: cannot delete yourself or another Admin
- Paginated list with search and filter by role and status
| Technology | Version | Purpose |
|---|---|---|
| Angular | 21 | SPA framework — standalone components, Signals, OnPush |
| Angular Material (MDC) | 21 | UI component library |
| TypeScript | 5 | Strongly typed language |
| Reactive Forms | — | Form handling, validators, cross-field validation |
| Chart.js + ng2-charts | 4 / 10 | Dashboard charts (bar, doughnut) |
| RxJS | 7.8 | Async streams, forkJoin, switchMap, takeUntilDestroyed |
| HTTP Interceptor | — | Auto-attaches JWT; global 401 → logout redirect |
| Technology | Version | Purpose |
|---|---|---|
| ASP.NET Core | .NET 10 | RESTful Web API |
| Entity Framework Core | 9 | ORM — Code First, fluent configuration, migrations |
| SQL Server / Azure SQL | — | Relational data store |
| JWT Bearer Authentication | — | Stateless auth with custom OnTokenValidated hook |
| BCrypt.Net | — | Password hashing |
| Swagger / OpenAPI | — | Interactive API documentation |
| Tool | Purpose |
|---|---|
| Azure App Service (F1 Linux) | Backend hosting |
| Azure SQL Database (Basic) | Production database |
| Azure Static Web Apps | Frontend hosting + global CDN |
| GitHub Actions | CI/CD — separate workflows for frontend and backend |
| Azure Service Principal | Scoped RBAC credentials for automated deployment |
Expense-Management-System/
├── backend/
│ └── ExpenseManagement.API/
│ ├── ExpenseManagement.Domain/ # Entities, Enums — zero external dependencies
│ ├── ExpenseManagement.Application/ # Interfaces, DTOs, service contracts
│ ├── ExpenseManagement.Infrastructure/ # EF Core, Services, Repositories, Migrations
│ └── ExpenseManagement.API/ # Controllers, Middleware, Program.cs
│
└── frontend/
└── expense-management-ui/src/app/
├── core/ # Auth service, HTTP interceptor, guards, models
├── features/ # Dashboard, Expenses, Categories, Users (all lazy-loaded routes)
└── shared/ # Shared components and Angular Material imports
API → Application → Domain
↑
Infrastructure
The Domain layer is the innermost ring — it has no NuGet references and no knowledge of the database or HTTP. The Application layer defines interfaces (IExpenseService, IAuthService, etc.) and DTOs. Infrastructure provides the concrete implementations. The API layer wires everything together through dependency injection in Program.cs.
| Decision | Rationale |
|---|---|
| Clean Architecture layers | Business rules never depend on delivery mechanisms (HTTP) or data access (EF Core) |
| 7 named authorization policies | AdminOnly, AdminOrManager, ManagerOnly, All, etc. — applied per endpoint, not per role check scattered in code |
Signals + OnPush |
Fine-grained reactivity; no zone.js triggers on unrelated state changes |
OnTokenValidated instead of blocklist |
No shared blocklist state needed; enforcement is immediate and stateless |
fileReplacements for environments |
Single environment.ts in source; Angular CLI swaps in environment.prod.ts at build time — no runtime config injection needed |
| Feature | Employee | Manager | Admin |
|---|---|---|---|
| Submit expense | ✅ | ✅ | ✅ |
| View own expenses | ✅ | ✅ | ✅ |
| View all expenses | ❌ | ✅ | ✅ |
| Approve / Reject expense | ❌ | ✅ | ✅ |
| Edit expense (Pending only) | ❌ | ✅ | ✅ |
| Upload / replace receipt | ❌ | ✅ | ✅ |
| Delete expense | ❌ | ❌ | ✅ |
| View dashboard | ❌ | ✅ | ✅ |
| Manage categories | ❌ | ❌ | ✅ |
| Manage users | ❌ | ❌ | ✅ |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/auth/register |
Public | Register a new user |
POST |
/api/auth/login |
Public | Login — returns JWT + refresh token |
POST |
/api/auth/refresh-token |
Public | Rotate refresh token |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/expense |
All roles | Paginated list (search + filters) |
POST |
/api/expense |
All roles | Submit a new expense |
PUT |
/api/expense/{id} |
Admin / Manager | Edit expense |
POST |
/api/expense/{id}/approve |
Admin / Manager | Approve or reject with reason |
POST |
/api/expense/{id}/receipt |
Admin / Manager | Upload / replace receipt file |
DELETE |
/api/expense/{id} |
Admin | Hard delete expense + receipt |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/category |
All roles | Paginated category list |
POST |
/api/category |
Admin | Create category with optional budget |
PUT |
/api/category/{id} |
Admin | Update category |
PATCH |
/api/category/{id}/deactivate |
Admin | Soft deactivate |
DELETE |
/api/category/{id} |
Admin | Hard delete |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/dashboard/summary |
Admin / Manager | 6 KPI totals |
GET |
/api/dashboard/monthly-expenses |
Admin / Manager | Monthly spend array |
GET |
/api/dashboard/category-expenses |
Admin / Manager | Per-category spend + budget |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/api/user |
Admin | Paginated user list |
PUT |
/api/user/{id}/role |
Admin | Change user role |
PUT |
/api/user/{id}/status |
Admin | Toggle active / inactive |
DELETE |
/api/user/{id} |
Admin | Delete user |
Full interactive documentation with request/response schemas available at the Swagger UI.
- .NET 10 SDK
- Node.js 20+ and npm
- SQL Server (local instance or SQL Server Express)
- EF Core CLI tools:
dotnet tool install --global dotnet-ef
git clone https://github.com/Brisco15/Expense-Management-System.git
cd Expense-Management-Systemcd backend/ExpenseManagement.API/ExpenseManagement.APIConfigure appsettings.json with your local values:
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=ExpenseManagementDB;Trusted_Connection=True;TrustServerCertificate=True;"
},
"Jwt": {
"Key": "your-secret-key-at-least-32-characters-long",
"Issuer": "ExpenseManagementAPI",
"Audience": "ExpenseManagementClient",
"ExpirationMinutes": "60"
}
}Apply migrations and run:
dotnet ef database update
dotnet runAPI: https://localhost:5297 — Swagger: https://localhost:5297/swagger
cd frontend/expense-management-ui
npm install --legacy-peer-deps
ng serveApp: http://localhost:4200
The frontend points to
http://localhost:5297/apiin development mode. To change this, editsrc/environment.ts.
The full application is deployed to Microsoft Azure.
| Resource | Azure Service | Tier | Region |
|---|---|---|---|
| Backend REST API | App Service | F1 Free, Linux | West Europe |
| Production database | Azure SQL Database | Basic | West Europe |
| Frontend SPA | Azure Static Web Apps | Free | West Europe |
Sensitive values are never committed to source control:
appsettings.Production.jsonis listed in.gitignore- Connection string, JWT key, issuer, and audience are stored as Azure App Service Application Settings
- Azure overrides
appsettingsvalues at runtime via environment variable injection - The committed
appsettings.jsoncontains only empty placeholder strings
Backend:
dotnet publish -c Release -o ./publish
# Create zip with forward-slash paths (required for Linux App Service)
az webapp deploy --resource-group ExpenseManagementRG --name expensemgmt-api \
--src-path publish.zip --type zipFrontend:
npm run build -- --configuration production
swa deploy ./dist/expense-management-ui/browser \
--deployment-token <token>Two independent GitHub Actions workflows handle automated deployment. Each triggers only when its relevant part of the codebase changes.
Triggers on push to master with changes under frontend/:
Setup Node.js 20
→ npm install --legacy-peer-deps
→ ng build --configuration production
→ Azure/static-web-apps-deploy@v1 (pre-built output)
Triggers on push to master with changes under backend/:
Setup .NET 10 SDK
→ dotnet publish -c Release
→ zip output
→ azure/login@v2 (service principal)
→ az webapp deploy
| Secret | Workflow | Description |
|---|---|---|
AZURE_STATIC_WEB_APPS_API_TOKEN |
Frontend | SWA deployment token |
AZURE_CREDENTIALS |
Backend | Service principal JSON (az ad sp create-for-rbac --json-auth) |
Brisco15 — Full-Stack Developer
Built from scratch as a portfolio project demonstrating end-to-end production skills: Clean Architecture, JWT security with real-time enforcement, Angular Signals, RESTful API design, EF Core Code First, role-based access control, file management, Azure cloud deployment, and automated CI/CD pipelines.