Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2df56c7
wip refacto components to be more signal-friendly
vmillet-dev Mar 6, 2026
8330d59
fix signal form error in template
vmillet-dev Mar 14, 2026
3f94319
wip generic input form for signal form
vmillet-dev Mar 15, 2026
22770c1
wip add some docs over forminput
vmillet-dev Mar 15, 2026
2d66839
wip add in header dropdown to change language
vmillet-dev Mar 15, 2026
100b685
wip fix registerform to display all errors when submitting an empty form
vmillet-dev Mar 15, 2026
6dea465
wip refacto form submitting by using a directive
vmillet-dev Mar 15, 2026
36ebb88
add structured async errors with i18n support and update auth forms
vmillet-dev Mar 15, 2026
5dca6b5
fix TU
vmillet-dev Mar 15, 2026
cf540bc
do not throw error when user not found when trying to reset password
vmillet-dev Mar 15, 2026
2b8589d
Remove unused environment files and update password‑reset flow, navig…
vmillet-dev Mar 15, 2026
46dbd99
Add vitest coverage config, data‑cy selectors, and full test suite fo…
vmillet-dev Mar 15, 2026
f8b2cd9
Refactor tests and add missing spec files across core and auth featur…
vmillet-dev Mar 15, 2026
b2d9565
fix e2e tests
vmillet-dev Mar 19, 2026
64ecd42
fix all tests
vmillet-dev Mar 19, 2026
2c2905f
fix all tests
vmillet-dev Mar 19, 2026
f8d84f4
fix all tests
vmillet-dev Mar 19, 2026
d4af060
remove assert on i18n text
vmillet-dev Mar 21, 2026
ead6f35
(feat) add many backend unit tests (#99)
vmillet-dev Mar 11, 2026
ec71b0e
Feat/update readme (#101)
vmillet-dev Mar 11, 2026
c7ef5bf
fix e2e tests
vmillet-dev Mar 21, 2026
f7e011d
remove console log
vmillet-dev Mar 21, 2026
60106df
final touch
vmillet-dev Mar 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 1 addition & 19 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,26 +115,8 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6

- name: Build docker image app
working-directory: e2e
run: docker compose -f ../devops/compose-e2e.yaml up -d --wait app-e2e

- name: Cypress run
working-directory: e2e
run: |
npm i
npm run cypress:ci

- name: Upload Cypress screenshots on failure
uses: actions/upload-artifact@v6
if: failure()
with:
name: cypress-screenshots
path: e2e/cypress/screenshots

- name: Cleanup on failure
if: failure()
run: |
echo "=== Debug Info on Failure ==="
docker ps -a
docker logs devops-app-e2e-1
npm run cypress:run:docker
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,61 @@
# ✈️ Tripr

> **fast SaaS Starter** combining the power of **Spring Boot 3 (Kotlin)** and the reactivity of **Angular 21 (Signals)**.
> **Modern SaaS Starter** combining the power of **Spring Boot 4 (Kotlin)** and the reactivity of **Angular 21 (Signals)**.

[![Build Status](https://img.shields.io/badge/build-passing-brightgreen.svg)](https://github.com/example/tripr/actions)
[![Kotlin](https://img.shields.io/badge/Kotlin-2.3.10-blue.svg)](https://kotlinlang.org/)
[![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.dev/)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

Tripr is a production-ready template designed for scalability, security, and a smooth developer experience. It integrates a strict hexagonal architecture on the backend and a reactive zoneless approach on the frontend.
Tripr is a production-ready template designed for scalability, security, and a smooth developer experience. It features a **contract-first approach** using OpenAPI to bridge the gap between a strict Hexagonal backend and a reactive Zoneless frontend.

### 🏗️ Architecture Overview

```mermaid
graph LR
User((User)) <--> Front[Angular App <br/><i>Signals & Zoneless</i>]
Front <--> Back[Spring Boot API <br/><i>Hexagonal Architecture</i>]
Back <--> DB[(PostgreSQL)]
Back <--> SMTP[SMTP Service]

subgraph Angular
A1[Components]
A2[Services]
end

subgraph OpenAPI-Generator
O1[openapi.yaml]
O2[TS Client]
end

subgraph Spring-Boot
subgraph adapters-in
B1[REST Controllers]
end
subgraph domain
B2[Ports In]
B3[Services / Use Cases]
B4[Ports Out]
end
subgraph adapters-out
B5[Persistence JPA]
B6[Notification Email]
end
subgraph application
B7[Main / Config]
end

B1 --> B2 --> B3 --> B4
B4 --> B5
B4 --> B6
end

A2 --> O1 --> O2 --> B1
```

### 🛠️ Tech Stack

| Component | Key Technologies |
|:-------------|:-------------------------------------------------------------------------------------|
| **Backend** | Kotlin, Spring Boot 4.+, Spring Security (JWT), Liquibase, MapStruct, Testcontainers |
| **Frontend** | Angular 21, Vite, Vitest, Bootstrap 5, Transloco (i18n), Signals |
| **Frontend** | Angular 21, Vite, Vitest, Bootstrap 5, Transloco (i18n), Signals, Zoneless |
| **Bridge** | **OpenAPI Generator** (Automatic Model & API Synchronization) |
| **DevOps** | Docker, GitHub Actions, Ansible |

---
Expand All @@ -49,6 +80,12 @@ cd backend && ./gradlew bootRun
cd frontend && npm install && npm run dev
```

### 🛠️ Dev Links

- 🌐 **Frontend** → http://localhost:4200
- 📄 **Swagger UI** → http://localhost:8080/api/swagger-ui
- 📬 **Mailpit** → http://localhost:8026

### 📁 Monorepo Structure

```text
Expand All @@ -63,4 +100,4 @@ cd frontend && npm install && npm run dev
---

- 📖 [**Backend Documentation**](backend/README.md) — *Architecture, API & Tests*
- 📖 [**Frontend Documentation**](frontend/README.md) — *Signals, Standalone & Vite*
- 📖 [**Frontend Documentation**](frontend/README.md) — *Modern Angular, Tooling & Vite*
1 change: 0 additions & 1 deletion api-spec/src/main/openapi/auth.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,6 @@ components:
type: string
description: Username or email for password reset
example: "john.doe@example.com"
minLength: 3
maxLength: 100

PasswordResetDto:
Expand Down
16 changes: 8 additions & 8 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 🏗️ Tripr API — Backend

Robust API built with **Spring Boot 3.4** and **Kotlin**, using a strict **Hexagonal Architecture** to ensure domain isolation and testability.
Robust API built with **Spring Boot 4** and **Kotlin**, using a strict **Hexagonal Architecture** to ensure domain isolation and testability.

🔗 **Swagger UI** (dev mode): [http://localhost:8080/api/swagger-ui](http://localhost:8080/api/swagger-ui)
The project follows a **Contract-First** approach using **OpenAPI**. The server-side code is generated from the OpenAPI spec, ensuring a perfect sync between documentation and implementation.

---

Expand All @@ -13,14 +13,14 @@ The project follows the **Ports & Adapters** pattern to decouple business logic
- **`domain/`**: **The Core**.
- `model/`: Domain entities and value objects.
- `port/`: Inbound (In) and Outbound (Out) interfaces.
- `service/`: Business orchestration implementing *In-Ports* by using *Out-Ports*.
- `service/`: Business orchestration. Services implement *In-Ports* and use *Out-Ports* to execute business logic.
- **`infrastructure/`**: **The Adapters**.
- Concrete port implementations (JPA/PostgreSQL Persistence, JWT Security, SMTP Email).
- REST Controllers and external configurations.
- Concrete implementations of *Out-Ports* (Persistence with JPA/PostgreSQL, Security with JWT, Email with SMTP).
- Inbound adapters such as REST Controllers.
- **`application/`**: **The Shell**.
- Application bootstrap (Main class).
- Global configuration (Spring, Security, Liquibase).
- Integration tests and architectural compliance (**ArchUnit**).
- Entry point of the application (Main class).
- Global configurations (Spring, Security, Docker Compose).
- Integration tests and architectural validation (**ArchUnit**).

---

Expand Down
4 changes: 3 additions & 1 deletion backend/application/src/main/resources/application-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ spring:
jwt:
secret: verySecretKeyThatShouldBeAtLeast32CharactersLong
password-reset:
base-url: localhost:8080
base-url: localhost:8081
server:
port: 8081
62 changes: 38 additions & 24 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ plugins {
alias(libs.plugins.kotlin.noarg) apply false
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.kotlin.kapt) apply false
jacoco
alias(libs.plugins.kover)
}

subprojects {
apply(plugin = rootProject.libs.plugins.kotlin.jvm.get().pluginId)
apply(plugin = "jacoco")
apply(plugin = rootProject.libs.plugins.kover.get().pluginId)

kotlin {
compilerOptions {
Expand All @@ -28,7 +28,7 @@ subprojects {

tasks.withType<Test> {
useJUnitPlatform()
finalizedBy("jacocoTestReport")
finalizedBy("koverHtmlReport", "koverXmlReport")
}

tasks.withType<BootRun> {
Expand All @@ -39,29 +39,43 @@ subprojects {
enabled = false
}

jacoco {
toolVersion = rootProject.libs.versions.jacoco.get()
}

afterEvaluate {
tasks.named<JacocoReport>("jacocoTestReport") {
dependsOn(tasks.test)

reports {
xml.required.set(true)
}

classDirectories.setFrom(
fileTree("build/classes/kotlin/main") {
exclude(
"**/config/**",
"**/dto/**",
"**/enums/**",
"**/exception/**",
"**/annotation/**"
kover {
reports {
filters {
excludes {
classes(
"**.config.**",
"**.dto.**",
"**.enums.**",
"**.exception.**",
"**.annotation.**",
"**.entity.**",
"**.model.**",
"**.*DefaultImpls",
"**.*Api",
"**.*Delegate",
"**.*ExceptionHandler",
"**.*ApiUtil",
"**.*Exception",
$$"**.*$Companion"
)
}
)
}
total {
xml {
onCheck = true
}
html {
onCheck = true
}
}
verify {
rule {
bound {
minValue = 80
}
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ class PasswordResetService(
* Request a password reset for a user
*/
override fun requestPasswordReset(username: String) {
val user: User = userPersistence.findByUsername(username)
?: throw UserNotFoundException("Password reset request failed - user not found with username: $username")
val user: User = userPersistence.findByUsername(username) ?: return // For security reason, we must not throw any error if the user doesn't exist

// Delete any existing tokens for this user
tokenPersistence.deletePasswordResetTokenByUser(user)
Expand Down
Loading