merge(main): scaffolding du serveur Dart headless hexagonal (ticket #47)

Fusionne feature/server-47-scaffolding — dart analyze clean, dart
test 1/1 vert, serveur démarré et GET /health validé (200
{"status":"ok"}). Premier ticket du chantier serveur (#47 à #53).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 00:04:44 +02:00
35 changed files with 996 additions and 19 deletions

View File

@ -13,3 +13,4 @@
- [gametime-ux-exercise-default-targets](gametime-ux-exercise-default-targets.md) — memory note gametime-ux-exercise-default-targets
- [gametime-ux-series-counter](gametime-ux-series-counter.md) — memory note gametime-ux-series-counter
- [gametime-ux-exercise-media-viewer](gametime-ux-exercise-media-viewer.md) — memory note gametime-ux-exercise-media-viewer
- [gametime-server-architecture-sync-sharing](gametime-server-architecture-sync-sharing.md) — memory note gametime-server-architecture-sync-sharing

View File

@ -0,0 +1,200 @@
---
name: gametime-server-architecture-sync-sharing
description: memory note gametime-server-architecture-sync-sharing
metadata:
type: project
---
# GameTime — Architecture serveur headless, sync et partage
Décision serveur pour le ticket #46.
## Stack retenue
- Langage/runtime : Dart serveur.
- Framework HTTP : `shelf` + `shelf_router`.
- Base serveur : PostgreSQL.
- Containerisation : Docker + `docker-compose.yaml` dans `server/`.
Raison : cohérence forte avec le client Flutter/Dart et les contrats métier existants, faible surface framework, testabilité correcte, packaging Docker simple avec image officielle Dart, PostgreSQL robuste pour comptes, tokens, ownership, sync incrémentale et partage ciblé.
Alternatives écartées pour la v1 :
- FastAPI/Python : excellent écosystème, mais introduit un second langage et des DTO à dupliquer.
- Node/NestJS : robuste mais plus lourd et moins cohérent avec l'existant.
- Serverpod : intéressant en Dart, mais plus structurant/opinionated que nécessaire pour un serveur API headless simple.
## Architecture hexagonale serveur
Sous-répertoire dédié : `server/`.
Couches attendues :
- `domain/` : entités serveur et invariants purs.
- `application/` : use cases, ports repositories/services, DTO API indépendants de Shelf/PostgreSQL.
- `infrastructure/postgres/` : adapters repositories PostgreSQL, migrations.
- `infrastructure/security/` : hash password, token signing/verification, clock/id providers.
- `api/` : routes Shelf, middleware auth, mapping request/response.
- `bin/server.dart` : composition root.
Le domaine serveur ne dépend pas de Shelf, Docker ou PostgreSQL.
## Entités serveur
- `UserAccount` : id serveur, email/login unique, passwordHash, displayName?, createdAt, updatedAt, disabledAt?.
- `AuthSession` / refresh token : id, userId, tokenHash, issuedAt, expiresAt, revokedAt?, userAgent?, deviceLabel?.
- `SyncedResource` : ressource possédée par un utilisateur pour Exercise, Program, WorkoutTemplate, WorkoutHistory, MediaAsset metadata.
- `Share` : partage ciblé vers un ou plusieurs comptes, jamais public ouvert.
- `ShareRecipient` : user destinataire + statut `pending|accepted|declined|revoked`.
## Modèle sync serveur
Une table générique `synced_resources` est acceptable pour la v1 afin d'éviter de dupliquer tout le schéma Drift côté serveur. Champs recommandés :
- `server_id` UUID primary key.
- `owner_user_id` FK users.
- `resource_type` enum text : `exercise|program|workoutTemplate|workoutHistory|mediaAsset`.
- `client_id` text : id local stable du client.
- `payload_json` jsonb : snapshot de la ressource côté client.
- `schema_version` int.
- `client_updated_at` timestamptz.
- `server_updated_at` timestamptz.
- `deleted_at` timestamptz nullable.
- `origin_device_id` text nullable.
Contraintes/index :
- unique `(owner_user_id, resource_type, client_id)`.
- index `(owner_user_id, resource_type, server_updated_at)`.
- index `(owner_user_id, server_updated_at)` pour pull global.
Les médias binaires ne sont pas couverts en profondeur par #46 : stocker d'abord les métadonnées et prévoir le port `MediaObjectStore` pour ajout futur.
## Protocole sync LWW v1
Stratégie : last-write-wins simple basé sur `clientUpdatedAt`. En cas d'égalité, tie-breaker stable côté serveur (`serverUpdatedAt`, puis `serverId` si nécessaire). Pas de résolution interactive en v1.
Endpoints recommandés :
### `POST /sync/push`
Requête :
```json
{
"deviceId": "...",
"items": [
{
"resourceType": "exercise",
"clientId": "...",
"schemaVersion": 3,
"clientUpdatedAt": "2026-07-18T10:00:00Z",
"deletedAt": null,
"payload": {}
}
]
}
```
Réponse :
```json
{
"serverCursor": "...",
"results": [
{
"resourceType": "exercise",
"clientId": "...",
"serverId": "...",
"status": "accepted|ignoredOlder|conflictLwwApplied|error",
"serverUpdatedAt": "2026-07-18T10:00:01Z"
}
]
}
```
### `GET /sync/pull?since=<cursor>`
Retourne toutes les ressources de l'utilisateur modifiées après le curseur serveur, soft deletes inclus.
Réponse :
```json
{
"serverCursor": "...",
"items": [
{
"resourceType": "workoutTemplate",
"clientId": "...",
"serverId": "...",
"schemaVersion": 3,
"clientUpdatedAt": "...",
"serverUpdatedAt": "...",
"deletedAt": null,
"payload": {}
}
]
}
```
### `POST /sync/exchange` optionnel
Combine push puis pull pour simplifier le futur client Flutter.
## Partage ciblé
Le partage n'est pas un lien public. Un utilisateur authentifié envoie un snapshot de `program` ou `workoutTemplate` à des destinataires identifiés.
Endpoints :
- `POST /shares` : créer un partage vers un ou plusieurs comptes.
- `GET /shares/inbox` : lister les partages reçus.
- `POST /shares/{id}/accept` : importer/copier la ressource dans l'espace du destinataire.
- `POST /shares/{id}/decline`.
- `POST /shares/{id}/revoke` pour l'émetteur.
À l'acceptation, créer une nouvelle ressource syncable détenue par le destinataire avec nouveaux IDs côté serveur et payload importable côté client. Ne jamais modifier la ressource source de l'émetteur.
## Structure `server/`
Structure cible :
```text
server/
pubspec.yaml
README.md
Dockerfile
docker-compose.yaml
.env.example
bin/
server.dart
lib/
domain/
application/
infrastructure/
postgres/
security/
config/
api/
migrations/
scripts/
push-gitea-image.sh
test/
```
`docker-compose.yaml` doit laisser libres via variables :
- bind host/IP de la machine Docker.
- port API exposé.
- URL publique HTTPS derrière reverse proxy.
- origine/IP reverse proxy autorisée si contrôle réseau ajouté.
- paramètres PostgreSQL (`POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, volume).
- secrets auth/JWT.
Le script `scripts/push-gitea-image.sh` ne doit contenir aucune URL/identifiant en dur. Il accepte registry/image/tag/user/token via variables d'environnement ou arguments.
## Tickets créés
- #47 `[Server] Scaffolding serveur Dart headless hexagonal`.
- #48 `[Server] Auth comptes utilisateurs et tokens API`, dépend de #47.
- #49 `[Server] Schéma PostgreSQL sync-ready GameTime`, dépend de #47.
- #50 `[Server] API de synchronisation incrémentale LWW`, dépend de #48 et #49.
- #51 `[Server] Partage ciblé de programmes et séances entre comptes`, dépend de #48 et #49.
- #52 `[Server] Packaging Docker Compose et push Gitea Registry`, dépend de #47.
- #53 `[Server] Tests API, contrats OpenAPI et vérification d'intégration`, dépend de #50, #51 et #52.
Ordre recommandé : #47, puis #48 et #49 en parallèle, puis #50 et #51, puis #52, puis #53.

View File

@ -1,6 +1,6 @@
---
issueRef: "#44"
version: 4
updatedBy: {"kind":"user"}
updatedAt: 1784379295672
version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784406980453
---

View File

@ -2,15 +2,15 @@
id: "c63e88a9-5868-4bd0-a71b-ebc4e7e6552a"
number: 44
title: "[Bug] Je veux pouvoir mettre un score par défaut à 0."
status: "open"
status: "qa"
priority: "medium"
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"}
updatedBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784379259225
updatedAt: 1784379295672
version: 4
updatedAt: 1784406980453
version: 5
---
Je veux pouvoir renseigner un score par défaut à 0 (je ne parle pas du nombre de répétition qui doit lui rester comme actuellement).

View File

@ -1,6 +1,6 @@
---
issueRef: "#45"
version: 4
updatedBy: {"kind":"user"}
updatedAt: 1784379490098
version: 5
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784406980638
---

View File

@ -2,15 +2,15 @@
id: "2f075be4-bcb8-4fdd-89fc-2f897e97bde4"
number: 45
title: "[Bug] Affichage de répétition en séance à 0"
status: "open"
status: "qa"
priority: "medium"
sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: []
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
createdBy: {"kind":"user"}
updatedBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784379414910
updatedAt: 1784379490098
version: 4
updatedAt: 1784406980638
version: 5
---
Quand je suis en séance sur un exercice qui demande un nombre de répétition, l'affichage m'affiche 0 alors que le nombre de répétition est set à une valeur > 0 dans l'exercice. Il devrait être à la même valeur

View File

@ -0,0 +1,6 @@
---
issueRef: "#47"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784411985578
---

View File

@ -0,0 +1,16 @@
---
id: "f3283335-8f96-40aa-88a4-bcc58a530ad2"
number: 47
title: "[Server] Scaffolding serveur Dart headless hexagonal"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784411985578
updatedAt: 1784411985578
version: 1
---
Créer le sous-répertoire `server/` à la racine avec une application Dart headless basée sur `shelf`/`shelf_router`, structure hexagonale (`domain/`, `application/`, `infrastructure/`, `api/`), configuration env, healthcheck et tests de base. Inclure `Dockerfile`, `.env.example`, `docker-compose.yaml` paramétrable et un README d'exploitation minimal. Le serveur doit écouter derrière reverse proxy sans HTTPS interne obligatoire.

View File

@ -0,0 +1,6 @@
---
issueRef: "#48"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784411990980
---

View File

@ -0,0 +1,16 @@
---
id: "6df36ec4-94cd-438b-a2ee-10c022f6848a"
number: 48
title: "[Server] Auth comptes utilisateurs et tokens API"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"},{"target":"#47","kind":"dependsOn"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784411990980
updatedAt: 1784411990980
version: 1
---
Implémenter le domaine serveur `UserAccount` et `AuthSession` : création de compte, connexion, hash de mot de passe Argon2id ou bcrypt, émission de tokens d'accès + refresh tokens, middleware d'auth API, révocation/logout. Stockage PostgreSQL. Endpoints attendus : `POST /auth/register`, `POST /auth/login`, `POST /auth/refresh`, `POST /auth/logout`, `GET /me`. Aucun écran client dans ce ticket.

View File

@ -0,0 +1,6 @@
---
issueRef: "#49"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784411997184
---

View File

@ -0,0 +1,16 @@
---
id: "c73b39be-061c-44b4-b5c3-f482878fe58d"
number: 49
title: "[Server] Schéma PostgreSQL sync-ready GameTime"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"},{"target":"#47","kind":"dependsOn"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784411997184
updatedAt: 1784411997184
version: 1
---
Créer le modèle serveur PostgreSQL pour les ressources synchronisables : Exercise, Program, WorkoutTemplate, WorkoutHistory, MediaAsset metadata, plus tables d'ownership par utilisateur. Chaque ressource doit stocker `ownerUserId`, `clientId`, `serverId`, `payloadJson`, `clientUpdatedAt`, `serverUpdatedAt`, `deletedAt`, `schemaVersion`, `originDeviceId`. Prévoir contraintes d'unicité `(owner_user_id, resource_type, client_id)`, index pull incrémental `(owner_user_id, resource_type, server_updated_at)`, migrations et tests repository.

View File

@ -0,0 +1,6 @@
---
issueRef: "#50"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784412001974
---

View File

@ -0,0 +1,16 @@
---
id: "983aa02b-7577-4fde-b77a-12355df0efdb"
number: 50
title: "[Server] API de synchronisation incrémentale LWW"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"},{"target":"#48","kind":"dependsOn"},{"target":"#49","kind":"dependsOn"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784412001974
updatedAt: 1784412001974
version: 1
---
Implémenter les ports/use cases et endpoints de sync incrémentale authentifiée. Endpoints : `POST /sync/push`, `GET /sync/pull?since=...`, optionnel `POST /sync/exchange`. Stratégie v1 last-write-wins basée sur `clientUpdatedAt` puis tie-breaker `serverUpdatedAt`/`serverId`. Supporter upsert, soft delete, réponses par item (`accepted`, `ignoredOlder`, `conflictLwwApplied`, `error`) et cursor serveur monotone. Ne pas intégrer le client Flutter dans ce ticket.

View File

@ -0,0 +1,6 @@
---
issueRef: "#51"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784412007538
---

View File

@ -0,0 +1,16 @@
---
id: "850ce975-99c3-4ee4-8d39-759ecc55139c"
number: 51
title: "[Server] Partage ciblé de programmes et séances entre comptes"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"},{"target":"#48","kind":"dependsOn"},{"target":"#49","kind":"dependsOn"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784412007538
updatedAt: 1784412007538
version: 1
---
Implémenter le domaine de partage ciblé : `Share`, destinataires par compte utilisateur, statut `pending|accepted|declined|revoked`, payload snapshot autonome de Program ou WorkoutTemplate. Endpoints : `POST /shares`, `GET /shares/inbox`, `POST /shares/{id}/accept`, `POST /shares/{id}/decline`, `POST /shares/{id}/revoke`. À l'acceptation, créer une copie importable dans l'espace du destinataire, sans lien public ouvert.

View File

@ -0,0 +1,6 @@
---
issueRef: "#52"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784412011625
---

View File

@ -0,0 +1,16 @@
---
id: "28ebc668-c209-4e6a-b123-252a38a3c784"
number: 52
title: "[Server] Packaging Docker Compose et push Gitea Registry"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"},{"target":"#47","kind":"dependsOn"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784412011625
updatedAt: 1784412011625
version: 1
---
Finaliser l'exploitation container : `server/docker-compose.yaml` paramétrable via `.env` pour host bind, port API interne/externe, origine reverse proxy, URL publique HTTPS, config PostgreSQL, secrets, volumes persistants. Ajouter `server/scripts/push-gitea-image.sh` acceptant registry, image, tag, username/token via variables d'environnement ou arguments, sans valeur en dur. Documenter le flux build/push/run.

View File

@ -0,0 +1,6 @@
---
issueRef: "#53"
version: 1
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedAt: 1784412015509
---

View File

@ -0,0 +1,16 @@
---
id: "d5fce252-f3c4-499f-ad0a-d3c81ea13305"
number: 53
title: "[Server] Tests API, contrats OpenAPI et vérification d'intégration"
status: "open"
priority: "low"
sprint: null
links: [{"target":"#46","kind":"relatesTo"},{"target":"#50","kind":"dependsOn"},{"target":"#51","kind":"dependsOn"},{"target":"#52","kind":"dependsOn"}]
agentRefs: []
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
createdAt: 1784412015509
updatedAt: 1784412015509
version: 1
---
Ajouter la couverture de validation serveur : tests unitaires domain/application, tests repository PostgreSQL, tests API auth/sync/share, fixtures de payload GameTime, vérification Docker Compose locale. Produire un contrat OpenAPI ou équivalent documentant auth, sync et partage pour les futurs tickets d'intégration Flutter.

View File

@ -1,3 +1,3 @@
{
"nextNumber": 47
"nextNumber": 54
}

View File

@ -501,25 +501,25 @@
"issueRef": "#44",
"path": "44",
"title": "[Bug] Je veux pouvoir mettre un score par défaut à 0.",
"status": "open",
"status": "qa",
"priority": "medium",
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
"assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae"
],
"updatedAt": 1784379295672
"updatedAt": 1784406980453
},
{
"issueRef": "#45",
"path": "45",
"title": "[Bug] Affichage de répétition en séance à 0",
"status": "open",
"status": "qa",
"priority": "medium",
"sprint": "abc4f969-b169-45f7-988c-daeeab762201",
"assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae"
],
"updatedAt": 1784379490098
"updatedAt": 1784406980638
},
{
"issueRef": "#46",
@ -532,6 +532,76 @@
"57695b92-24d0-4876-837c-76116e70a6ae"
],
"updatedAt": 1784380502263
},
{
"issueRef": "#47",
"path": "47",
"title": "[Server] Scaffolding serveur Dart headless hexagonal",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784411985578
},
{
"issueRef": "#48",
"path": "48",
"title": "[Server] Auth comptes utilisateurs et tokens API",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784411990980
},
{
"issueRef": "#49",
"path": "49",
"title": "[Server] Schéma PostgreSQL sync-ready GameTime",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784411997184
},
{
"issueRef": "#50",
"path": "50",
"title": "[Server] API de synchronisation incrémentale LWW",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784412001974
},
{
"issueRef": "#51",
"path": "51",
"title": "[Server] Partage ciblé de programmes et séances entre comptes",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784412007538
},
{
"issueRef": "#52",
"path": "52",
"title": "[Server] Packaging Docker Compose et push Gitea Registry",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784412011625
},
{
"issueRef": "#53",
"path": "53",
"title": "[Server] Tests API, contrats OpenAPI et vérification d'intégration",
"status": "open",
"priority": "low",
"sprint": null,
"assignedAgentIds": [],
"updatedAt": 1784412015509
}
]
}

51
server/README.md Normal file
View File

@ -0,0 +1,51 @@
# GameTime server
Headless Dart server for future GameTime account, sync and sharing features.
This package is separate from the Flutter app at the repository root.
## Local run
Install dependencies from this directory:
```bash
dart pub get
```
Run the server:
```bash
dart run bin/server.dart
```
The HTTP server binds to `0.0.0.0` and reads `PORT` from the environment.
When `PORT` is not set, it listens on `8080`.
```bash
PORT=9090 dart run bin/server.dart
```
Healthcheck:
```bash
curl http://localhost:8080/health
```
Expected response:
```json
{"status":"ok"}
```
## Scope
Ticket #47 only scaffolds the Dart server, the hexagonal directory layout and
the `/health` endpoint.
Upcoming tickets will fill the empty adapters and use cases:
- #48: user authentication and API tokens.
- #49: PostgreSQL schema and repositories.
- #50: incremental sync API.
- #51: targeted sharing.
- #52: Docker and registry packaging.
- #53: API, contract and integration tests.

View File

@ -0,0 +1 @@
include: package:lints/recommended.yaml

17
server/bin/server.dart Normal file
View File

@ -0,0 +1,17 @@
import 'dart:io';
import 'package:gametime_server/api/router.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
Future<void> main(List<String> arguments) async {
final port = int.tryParse(Platform.environment['PORT'] ?? '') ?? 8080;
final server = await shelf_io.serve(
buildApiHandler(),
InternetAddress.anyIPv4,
port,
);
print(
'GameTime server listening on ${server.address.address}:${server.port}',
);
}

View File

@ -0,0 +1,16 @@
import 'dart:convert';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
Handler buildApiHandler() {
final router = Router()
..get('/health', (Request request) {
return Response.ok(
jsonEncode({'status': 'ok'}),
headers: {'content-type': 'application/json'},
);
});
return const Pipeline().addMiddleware(logRequests()).addHandler(router.call);
}

View File

@ -0,0 +1,5 @@
# Application
Server use cases, ports and API DTOs independent from Shelf and PostgreSQL.
Concrete adapters are wired from `bin/server.dart`.

View File

@ -0,0 +1,6 @@
# Domain
Pure server domain entities and invariants.
This layer must not import Shelf, PostgreSQL adapters, Docker configuration or
other infrastructure concerns.

View File

@ -0,0 +1,5 @@
# PostgreSQL infrastructure
Placeholder for PostgreSQL repository adapters.
The schema and concrete persistence code are planned for ticket #49.

View File

@ -0,0 +1,5 @@
# Security infrastructure
Placeholder for password hashing, token signing and token verification.
Authentication is planned for ticket #48.

View File

@ -0,0 +1,4 @@
# Migrations
PostgreSQL migrations will be added with the server persistence schema in
ticket #49.

405
server/pubspec.lock Normal file
View File

@ -0,0 +1,405 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "9a3386eea899815698dd55995277cf7cb8572ee52b399a6edfb7ae2b50e5fc19"
url: "https://pub.dev"
source: hosted
version: "105.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "62993bed6eadbe9596c5c20d5c167e7bc563c5fe266657a04ddeb93bdb84f4c9"
url: "https://pub.dev"
source: hosted
version: "14.1.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev"
source: hosted
version: "1.15.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http_methods:
dependency: transitive
description:
name: http_methods
sha256: "6bccce8f1ec7b5d701e7921dca35e202d425b57e317ba1a37f2638590e29e566"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
meta:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
dependency: "direct main"
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_router:
dependency: "direct main"
description:
name: shelf_router
sha256: f5e5d492440a7fb165fe1e2e1a623f31f734d3370900070b2b1e0d0428d59864
url: "https://pub.dev"
source: hosted
version: "1.1.4"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
url: "https://pub.dev"
source: hosted
version: "1.31.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
url: "https://pub.dev"
source: hosted
version: "0.7.13"
test_core:
dependency: transitive
description:
name: test_core
sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
url: "https://pub.dev"
source: hosted
version: "0.6.19"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.0 <4.0.0"

14
server/pubspec.yaml Normal file
View File

@ -0,0 +1,14 @@
name: gametime_server
description: Headless Dart server for GameTime sync and sharing.
publish_to: 'none'
environment:
sdk: ^3.10.0
dependencies:
shelf: ^1.4.2
shelf_router: ^1.1.4
dev_dependencies:
lints: ^6.0.0
test: ^1.25.14

5
server/scripts/README.md Normal file
View File

@ -0,0 +1,5 @@
# Scripts
Operational scripts for the server package.
Docker image publishing is planned for ticket #52.

View File

@ -0,0 +1,18 @@
import 'dart:convert';
import 'package:gametime_server/api/router.dart';
import 'package:shelf/shelf.dart';
import 'package:test/test.dart';
void main() {
test('GET /health returns ok status', () async {
final handler = buildApiHandler();
final response = await handler(
Request('GET', Uri.parse('http://localhost/health')),
);
expect(response.statusCode, 200);
expect(response.headers['content-type'], contains('application/json'));
expect(jsonDecode(await response.readAsString()), {'status': 'ok'});
});
}