profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
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