README.md
18,929 bytes
| 1 | # SplitApp — Trip Expense Management |
|---|---|
| 2 | |
| 3 | URL: https://travel.rasmusj.com/ |
| 4 | Front: https://travel.rasmusj.com/ |
| 5 | SplitApp is an ASP.NET Core 10.0 web application for managing group trips and splitting expenses. Users create trips, invite friends, track costs with flexible splitting, manage budgets, run polls, maintain a wishlist, and settle debts via an optimized algorithm. |
| 6 | |
| 7 | Built as a **Personal Project — Phase 2** for the TalTech "Web Applications with C#" course (Phase 1 + full Clean/Onion architecture compliance with mandatory Repositories, UoW, Services, BLL DTOs, and Mappers). |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Architecture at a glance |
| 12 | |
| 13 | **Clean / Onion Architecture with strict 3-tier DTOs.** Dependencies point inward toward `App.Domain`. Interfaces live in the Domain layer (`App.Domain/Contracts/`), and `App.DAL.EF` is a plugin that implements them. `App.BLL` (application services) depends only on Domain abstractions and exposes **BLL DTOs** at its boundary — controllers never see Domain entities. `App.DTO` (Public DTOs) sits at the outer edge and maps from BLL DTOs to versioned API contracts. |
| 14 | |
| 15 | ``` |
| 16 | WebApp (MVC + API + Admin) |
| 17 | │ uses only App.BLL services + App.DTO public mappers |
| 18 | │ ── controllers see ONLY BLL DTOs and Public DTOs, NEVER Domain entities |
| 19 | ▼ |
| 20 | App.DTO (v1 Public DTOs + Mappers/ — BLL DTO ↔ Public DTO) |
| 21 | │ references App.BLL (so it can map BllDto → public DTO) |
| 22 | ▼ |
| 23 | App.BLL (Services: Trip, Expense, Settlement, Invitation, Poll, BudgetCategory, |
| 24 | │ Wishlist, SplitPreset, Identity, + 12 Admin services) |
| 25 | │ (DTO/ — TripBllDto, ExpenseBllDto, AppUserBllDto, …) |
| 26 | │ (Mappers/ — Domain ↔ BLL DTO factory mappers) |
| 27 | │ depends on App.Domain contracts only |
| 28 | ▼ |
| 29 | App.Domain — POCO entities + Contracts/ (IAppUnitOfWork + 12 repository interfaces) |
| 30 | ▲ ▲ |
| 31 | │ implements │ |
| 32 | │ │ |
| 33 | App.DAL.EF (AppDbContext, AppUnitOfWork, Repositories, Migrations) |
| 34 | Plugin — sits OUTSIDE Domain; Program.cs wires it via AddDalServices() |
| 35 | ``` |
| 36 | |
| 37 | **Key Phase 2 properties:** |
| 38 | |
| 39 | - **Three DTO tiers** as required by the BLL lecture: |
| 40 | 1. **Domain entity** (`App.Domain/Trip.cs`) — POCO, EF-friendly, owns business validation |
| 41 | 2. **BLL DTO** (`App.BLL/DTO/TripBllDto.cs`) — internal application boundary |
| 42 | 3. **Public DTO** (`App.DTO/v1/TripDto.cs`) — versioned external API contract |
| 43 | - **Two mapping layers** at each boundary: |
| 44 | - `App.BLL/Mappers/*BllDtoFactory.cs` — Domain ↔ BLL DTO (factory pattern as recommended by lecture) |
| 45 | - `App.DTO/Mappers/*Mapper.cs` — BLL DTO ↔ Public DTO |
| 46 | - `App.BLL.csproj` does **not** reference `App.DAL.EF` — dependency inversion via Domain contracts |
| 47 | - `App.BLL.csproj` does **not** reference `App.DTO` — public DTO mapping is App.DTO's responsibility (App.DTO references App.BLL, not the other way around) |
| 48 | - `WebApp.csproj` references `App.DAL.EF` **only** for `Program.cs` composition-root wiring (`builder.Services.AddDalServices(...)`); no controller uses DAL or DbContext directly |
| 49 | - Every repository interface (`IAppUnitOfWork`, `ITripRepository`, `IRefreshTokenRepository`, `IUserRepository`, …) is defined in `App.Domain/Contracts/` and implemented in `App.DAL.EF/` |
| 50 | |
| 51 | See [explanation.md](explanation.md) for a full architectural walkthrough, flow examples, and defense cheat sheet. |
| 52 | |
| 53 | --- |
| 54 | |
| 55 | ## Phase 2 changes — what was added on top of Phase 1 |
| 56 | |
| 57 | ### Onion strictness — 2 critical violations fixed |
| 58 | - **`AccountController` no longer injects `AppDbContext`** — replaced direct EF calls with new `App.BLL.Services.Identity.IIdentityService` (Register, Login, RefreshToken, Logout) backed by `IRefreshTokenRepository` and `IUserRepository` in the UoW. |
| 59 | - **`AdminStatsService` no longer depends on `Microsoft.AspNetCore.*`** — `UserManager` replaced with `IUserRepository`; `IStringLocalizer` removed (service now emits `MessageKey + MessageArgs`, controller resolves localization at projection time). |
| 60 | - The only `Microsoft.AspNetCore.Identity` import remaining in `App.BLL` is in `IdentityService` (auth abstraction by design — `AppUser` already inherits `IdentityUser` in Domain). |
| 61 | |
| 62 | ### BLL DTO + Factory Mapper layer (Lecture: *"Controllers should never see domain entities"*) |
| 63 | |
| 64 | Added across all 8 client services and 12 admin services: |
| 65 | |
| 66 | | App.BLL/DTO/ (15 BLL DTOs) | App.BLL/Mappers/ (9 Factories) | |
| 67 | |---|---| |
| 68 | | TripBllDto, TripParticipantBllDto, AppUserBllDto | TripBllDtoFactory | |
| 69 | | ExpenseBllDto, ExpenseSplitBllDto | ExpenseBllDtoFactory | |
| 70 | | SettlementPlanBllDto, SettlementPaymentBllDto | SettlementBllDtoFactory | |
| 71 | | TripInvitationBllDto | InvitationBllDtoFactory | |
| 72 | | TripPollBllDto, TripPollOptionBllDto | PollBllDtoFactory | |
| 73 | | TripWishlistItemBllDto | WishlistBllDtoFactory | |
| 74 | | BudgetCategoryBllDto | BudgetCategoryBllDtoFactory | |
| 75 | | CurrencyBllDto | CurrencyBllDtoFactory | |
| 76 | | SplitPresetBllDto, SplitPresetMemberBllDto | SplitPresetBllDtoFactory | |
| 77 | | BalanceBllDto | (settlement helper) | |
| 78 | |
| 79 | Every service interface now uses BLL DTOs at its public surface (`Task<TripBllDto> CreateTripAsync(TripBllDto dto, Guid userId)`). All ~30 controllers (API + MVC + Admin) and Razor views were updated to use BLL DTO types instead of Domain entities. |
| 80 | |
| 81 | ### Full Admin UX completed |
| 82 | |
| 83 | New views and controller actions added in `WebApp/Areas/Admin/`: |
| 84 | |
| 85 | | Area | Added | |
| 86 | |---|---| |
| 87 | | `SplitPresets` | `Create.cshtml` + `Create` action + `AdminSplitPresetFormViewModel` + `CreateAsync` service method | |
| 88 | | `Invitations` | `Create.cshtml`, `Edit.cshtml` + actions + `AdminInvitationFormViewModel` + `CreateAsync`/`UpdateAsync` service methods | |
| 89 | | `SettlementPayments` | `Create.cshtml`, `Edit.cshtml` + actions + `AdminSettlementPaymentFormViewModel` + `CreateAsync`/`UpdateAsync` service methods | |
| 90 | | `Users` | `Details.cshtml`, `Edit.cshtml`, `Delete.cshtml` + actions + `AdminUserDetailsViewModel` + `AdminUserEditViewModel` | |
| 91 | |
| 92 | All 13 admin controllers now have full CRUD coverage with **0 ViewBag/ViewData usage** — strict ViewModel-only views as required. |
| 93 | |
| 94 | --- |
| 95 | |
| 96 | ## Feature overview |
| 97 | |
| 98 | - **Trips** — create, manage, and archive group trips with Organizer / Participant roles |
| 99 | - **Expenses** — four split methods: `EqualAll`, `EqualSubset`, `ExactAmounts`, `Percentages` |
| 100 | - **Split presets** — reusable splitting templates |
| 101 | - **Budgets** — per-trip categories with real-time progress tracking |
| 102 | - **Invitations** — token-based invite links (Pending → Accepted / Declined / Expired / Revoked) |
| 103 | - **Settlement** — real-time balance tracking; trip lifecycle is `Active → Finalizing → Settled`: organizer clicks **Finalize Trip** to lock expenses and generate an optimized settlement plan (greedy algorithm minimizing payment count), trip enters `Finalizing`; two-sided confirmation flow (debtor marks paid → creditor confirms) — trip auto-advances to `Settled` only once every payment has been confirmed by its recipient. While in `Finalizing`, the organizer can still **Reopen** the trip (blocked once any payment is confirmed). |
| 104 | - **Wishlist** — places, activities, restaurants with voting and priority |
| 105 | - **Polls** — group decision-making with single/multi-vote support |
| 106 | - **Multi-currency** — EUR, USD, GBP, SEK, NOK (hardcoded rates) |
| 107 | - **Localization** — English + Estonian (UI via `.resx`; dynamic system data via `LangStr` JSON in DB) |
| 108 | - **Auth** — JWT Bearer for API (+ refresh token rotation), Cookie auth for MVC, role-based authorization (system roles + trip roles), IDOR protection |
| 109 | |
| 110 | --- |
| 111 | |
| 112 | ## Tech Stack |
| 113 | |
| 114 | - **Runtime:** ASP.NET Core 10.0 (MVC + REST API) |
| 115 | - **Database:** PostgreSQL 16 via Npgsql EF Core provider |
| 116 | - **Identity:** ASP.NET Identity with JWT Bearer + refresh token rotation, wrapped in `IIdentityService` |
| 117 | - **API docs:** Swagger / OpenAPI (with versioning and JWT auth integrated) |
| 118 | - **Deployment:** Docker + docker-compose, GitLab CI auto-deploy on `main` |
| 119 | |
| 120 | --- |
| 121 | |
| 122 | ## Phase 2 assignment requirements — mapping |
| 123 | |
| 124 | | Requirement | Status | Where to see it | |
| 125 | |---|---|---| |
| 126 | | **CLEAN/ONION architecture** | ✅ | Inverted dependencies, Domain-owned interfaces, BLL DTO + Factory pattern | |
| 127 | | Domain design: min 10 meaningful entities | ✅ 16 entities | `App.Domain/` | |
| 128 | | REST API: controllers + versioning + public DTOs | ✅ | `WebApp/ApiControllers/`, `/api/v1/`, `App.DTO/v1/` | |
| 129 | | Swagger | ✅ | `/swagger`, `ConfigureSwaggerOptions.cs` | |
| 130 | | Auth (JWT + refresh tokens) | ✅ | `IIdentityService` in BLL, `AccountController` thin wrapper | |
| 131 | | Client UX (MVC) | ✅ | `WebApp/Controllers/*Controller.cs` (uses BLL DTOs) | |
| 132 | | Admin UX (MVC, Area, ViewModels, no ViewBag/ViewData) | ✅ Full CRUD on all 13 controllers | `WebApp/Areas/Admin/`, `AdminViewModels.cs` | |
| 133 | | **Full Admin UX** | ✅ | All entities have Index / Details / Create / Edit / Delete (where meaningful) | |
| 134 | | UI translations (i18n, .resx) | ✅ EN + ET | `App.Resources/` | |
| 135 | | DB translations (LangStr) | ✅ | `Currency.Name`, `BudgetCategory.Name` use `LangStr` | |
| 136 | | IDOR protection | ✅ | `_uow.TripParticipants.IsParticipantAsync()` / `IsOrganizerAsync()` checks centralized in BLL services | |
| 137 | | **Repositories, UoW, Services, BLL, Mappers — mandatory** | ✅ All present | `App.Domain/Contracts/`, `App.DAL.EF/Repositories/`, `App.BLL/Services/`, `App.BLL/Mappers/`, `App.DTO/Mappers/` | |
| 138 | | CI/CD deploy (app + DB) | ✅ | `.gitlab-ci.yml`, `Dockerfile`, `docker-compose.yml` | |
| 139 | | Test coverage | ⏳ deferred to next iteration | — | |
| 140 | |
| 141 | --- |
| 142 | |
| 143 | ## Getting Started |
| 144 | |
| 145 | ### Prerequisites |
| 146 | |
| 147 | - .NET 10.0 SDK |
| 148 | - PostgreSQL 16 (or Docker) |
| 149 | |
| 150 | ### Run with Docker (recommended) |
| 151 | |
| 152 | ```bash |
| 153 | docker compose up --build |
| 154 | ``` |
| 155 | |
| 156 | The app listens on **http://localhost:84** (host port 84 → container port 8080). Migrations and seed data are applied automatically on startup. PostgreSQL data persists in a named volume (`pgdata`). |
| 157 | |
| 158 | For a clean reset (drop DB volume + reseed): |
| 159 | |
| 160 | ```bash |
| 161 | docker compose down -v && docker compose up --build |
| 162 | ``` |
| 163 | |
| 164 | ### Run locally (without Docker) |
| 165 | |
| 166 | ```bash |
| 167 | cd SplitApp |
| 168 | dotnet restore |
| 169 | dotnet ef database update --project App.DAL.EF --startup-project WebApp |
| 170 | dotnet run --project WebApp |
| 171 | ``` |
| 172 | |
| 173 | On first launch, seed data creates: default users, roles, currencies, and 4 example trips with expenses, polls, wishlist items. |
| 174 | |
| 175 | ### Default seed users |
| 176 | |
| 177 | The demo accounts are `user@`, `alice@`, `bob@`, `charlie@` and `diana@taltech.ee`, |
| 178 | all with the password `Kala.12345`. That is in the source on purpose: this is a |
| 179 | demo, the data is invented, and anyone reading the code is meant to be able to |
| 180 | sign in and look around. |
| 181 | |
| 182 | The administrator is not seeded at all unless `SEED_ADMIN_PASSWORD` is set, and |
| 183 | there is no default. See `DEPLOY.md`. |
| 184 | |
| 185 | --- |
| 186 | |
| 187 | ## REST API |
| 188 | |
| 189 | Versioned under `/api/v1/`. All protected endpoints require a JWT Bearer token. |
| 190 | |
| 191 | | Controller | Endpoints | Auth | |
| 192 | |---|---|---| |
| 193 | | `AccountController` (Identity) | register, login, refreshtoken, logout | partial (login/register public) — backed by `IIdentityService` | |
| 194 | | `TripsController` | trip CRUD, participant info | JWT + participant/organizer check | |
| 195 | | `ExpensesController` | expense CRUD with splits | JWT + participant check | |
| 196 | | `BudgetCategoriesController` | per-trip budget categories | JWT + participant check | |
| 197 | | `InvitationsController` | create, info, accept, decline, revoke | JWT + organizer check | |
| 198 | | `WishlistController` | wishlist CRUD, voting, completion | JWT + participant check | |
| 199 | | `PollsController` | poll CRUD, voting, closing | JWT + participant check | |
| 200 | | `SettlementsController` | balances, calculation, mark-paid, confirm | JWT + participant check | |
| 201 | | `SplitPresetsController` | split preset CRUD | JWT + organizer check | |
| 202 | | `CurrenciesController` | currency reference data | JWT | |
| 203 | |
| 204 | Swagger UI exposes the Bearer-auth flow — log in, paste the JWT, and all protected endpoints become callable from the browser. |
| 205 | |
| 206 | --- |
| 207 | |
| 208 | ## MVC Client UX |
| 209 | |
| 210 | Standard MVC controllers — functional, focused on proving the domain logic works through the BLL DTO layer: |
| 211 | |
| 212 | - **Home** (public), **Trips** (CRUD + details), **Expenses** (CRUD + 4 split methods), **Budget** (categories + progress), **Members** (invite links), **Settlement** (balances + payments), **PollsClient**, **WishlistClient** |
| 213 | |
| 214 | All views use BLL DTO types (`@model App.BLL.DTO.TripBllDto`) — no Domain entity leaks into Razor. |
| 215 | |
| 216 | --- |
| 217 | |
| 218 | ## Admin Panel |
| 219 | |
| 220 | Admin-only area at `/Admin`, protected by `[Authorize(Roles = "admin")]`. Designed, not pure scaffold: |
| 221 | |
| 222 | - **Custom sidebar layout** (`Areas/Admin/Views/Shared/_Layout.cshtml`) with Bootstrap Icons |
| 223 | - **Admin.css** — dedicated styling (sidebar, metric cards, status badges, timeline feed, empty states) |
| 224 | - **Dashboard** with custom statistics: Top Active Trips, Biggest Expenses, User activity (7d/30d), Top Active Users, chronological Activity Feed |
| 225 | - **13 admin controllers** with **full CRUD** coverage (Trips, Expenses, BudgetCategories, Currencies, Polls, Wishlist, Invitations, SettlementPlans, SettlementPayments, SplitPresets, TripParticipants, Users, Dashboard) |
| 226 | - **Strict ViewModels** — every view uses typed ViewModel inheriting `AdminPageViewModel`; **0 `ViewData`/`ViewBag` usage** |
| 227 | - **Generic wrappers** — `AdminDetailsViewModel<T>` and `AdminDeleteViewModel<T>` parameterized over BLL DTO types — keep domain entities out of views |
| 228 | - **User management** — Details / Edit (FirstName + LastName) / EditRoles / Delete actions |
| 229 | |
| 230 | --- |
| 231 | |
| 232 | ## Authorization Model |
| 233 | |
| 234 | - **System roles** (ASP.NET Identity): `admin`, `user` — enforced via `[Authorize(Roles = "admin")]` |
| 235 | - **Trip roles** (domain): `Organizer`, `Participant` — enforced via `TripParticipantRepository.IsOrganizerAsync()` / `IsParticipantAsync()` |
| 236 | - **IDOR protection** — every trip-scoped operation verifies the caller is a participant; check is centralized in BLL services (each query/mutation method takes `Guid userId` and validates it internally), so controllers cannot accidentally bypass the check |
| 237 | - **Trip lifecycle enforcement** — expenses cannot be created/edited/deleted while trip is outside `Active` (i.e. `Finalizing`, `Settled`, or `Archived`); settlement plan actions (Mark Paid / Confirm Receipt) are available during `Finalizing` and `Settled`; payer-only Mark Paid and payee-only Confirm buttons are enforced both in the BLL guards (`MarkPaidGuardedAsync` / `ConfirmPaymentGuardedAsync`) and in the views (button hidden for other users) |
| 238 | - **Creator-based access** — wishlist items editable/deletable only by creator; expenses editable/deletable by creator or trip organizer |
| 239 | |
| 240 | --- |
| 241 | |
| 242 | ## Project Structure |
| 243 | |
| 244 | ``` |
| 245 | SplitApp/ |
| 246 | ├── Base.Contracts/ Generic interfaces (IBaseEntity, IBaseRepository, IUnitOfWork) |
| 247 | ├── Base.Domain/ BaseEntity, LangStr |
| 248 | ├── Base.Helpers/ JWT generation/validation helpers |
| 249 | ├── App.Domain/ 16 domain entities + 8 enums |
| 250 | │ └── Contracts/ IAppUnitOfWork + 12 repository interfaces |
| 251 | │ (ITripRepository, IRefreshTokenRepository, IUserRepository, …) |
| 252 | ├── App.DAL.EF/ EF Core DbContext, UnitOfWork + repository implementations, |
| 253 | │ migrations, ServiceCollectionExtensions.AddDalServices() |
| 254 | ├── App.BLL/ Application services — depends only on App.Domain |
| 255 | │ ├── DTO/ BLL DTOs — internal application boundary (TripBllDto, ExpenseBllDto, |
| 256 | │ │ AppUserBllDto, … 15 DTOs) |
| 257 | │ ├── Mappers/ Domain ↔ BLL DTO factory mappers (9 factory classes) |
| 258 | │ └── Services/ |
| 259 | │ ├── Identity/ IIdentityService + IdentityService (Register/Login/Refresh/Logout) |
| 260 | │ ├── *.cs Core services (Trip, Expense, Settlement, Invitation, Poll, |
| 261 | │ │ BudgetCategory, Wishlist, SplitPreset) |
| 262 | │ └── Admin/ 12 admin services + AdminStatsService + AdminDashboardData |
| 263 | ├── App.DTO/ Public API DTOs (versioned) + Mappers/ |
| 264 | │ (BLL DTO ↔ Public DTO — TripMapper, ExpenseMapper, … 9 mappers) |
| 265 | ├── App.Resources/ .resx localization files (EN + ET) |
| 266 | ├── WebApp/ MVC views, API controllers, admin area, Program.cs composition root |
| 267 | │ ├── ApiControllers/ REST API (use BLL DTO + Public DTO mappers) |
| 268 | │ ├── Controllers/ MVC client (use BLL DTO + ViewModels) |
| 269 | │ ├── Areas/Admin/ Admin area (Controllers, Views, Models) |
| 270 | │ └── Models/ MVC ViewModels |
| 271 | ├── Dockerfile |
| 272 | ├── docker-compose.yml |
| 273 | └── SplitApp.sln |
| 274 | ``` |
| 275 | |
| 276 | **Dependency graph (Phase 2):** |
| 277 | |
| 278 | - `Base.Contracts` — no deps |
| 279 | - `Base.Domain` → `Base.Contracts` |
| 280 | - `Base.Helpers` — JWT (System.IdentityModel.Tokens.Jwt) |
| 281 | - `App.Domain` → `Base.Domain`, `Base.Contracts`, `App.Resources` (for Display attributes) |
| 282 | - `App.DAL.EF` → `App.Domain`, `Base.Contracts` (implements Domain contracts) |
| 283 | - `App.BLL` → `App.Domain`, `Base.Helpers` (**does NOT reference App.DAL.EF or App.DTO** — Clean inversion) |
| 284 | - `App.DTO` → `App.Domain`, `App.BLL` (Public DTO layer maps from BLL DTOs) |
| 285 | - `WebApp` → `App.BLL`, `App.DTO`, `App.Resources`, `App.DAL.EF` (DAL ref only for `Program.cs` `AddDalServices(...)`; no controller uses DAL) |
| 286 | |
| 287 | --- |
| 288 | |
| 289 | ## Defense cheat sheet (Phase 2 architecture questions) |
| 290 | |
| 291 | | Question | Answer / file | |
| 292 | |---|---| |
| 293 | | Where do interfaces live? | `App.Domain/Contracts/` — Domain owns the interfaces (Onion) | |
| 294 | | Why doesn't BLL reference DAL? | Dependency inversion via Domain contracts; see `App.BLL.csproj` | |
| 295 | | Show me the 3 DTO tiers | Domain `Trip` → BLL `TripBllDto` → Public `TripDto` | |
| 296 | | How do you map between layers? | Factory pattern: `App.BLL.Mappers.TripBllDtoFactory` (Domain↔BLL DTO) and `App.DTO.Mappers.TripMapper` (BLL DTO↔Public DTO) | |
| 297 | | How does a controller talk to the database? | Controller → BLL service interface → `IAppUnitOfWork` → `IRepository<T>` → `DbContext` (4-layer indirection, all abstractions) | |
| 298 | | Why no DbContext in controllers? | `IIdentityService` is the example — Identity flow moved entirely to BLL | |
| 299 | | Why no ViewBag/ViewData? | Every view has a typed `ViewModel`; verified by grep across `WebApp/Views/` and `WebApp/Areas/Admin/Views/` | |
| 300 | | How do you protect against IDOR? | Every trip-scoped service method validates `userId` against `TripParticipants.IsParticipantAsync` / `IsOrganizerAsync` before returning data | |
| 301 | | What's in the BLL DTO that's not in Domain entity? | Computed flat fields like `UserFullName`, `TripName`, `VoteCount`, `SpentAmount` — view-friendly, framework-agnostic | |
| 302 | | What's in the Public DTO that's not in BLL DTO? | String-based enums (versionable), flat denormalized fields (`DefaultCurrencyCode` instead of nested), no nav collections in list views | |
| 303 | |
| 304 | --- |
| 305 | |
| 306 | ## License |
| 307 | |
| 308 | Course project — TalTech "Web Applications with C#". |
| 309 | |
| 310 | See [explanation.md](explanation.md) for architectural decisions, the settlement algorithm, and layer-by-layer walkthrough. |
| 311 | |