Compare commits

...
23 Commits
Author SHA1 Message Date
fabien 5863858b09 fix panic : appels OnChunk sans garde de nil dans AnyLLMEngine.Stream (crash du serveur lors d'un appel d'outil hors streaming, ex. tâche planifiée) 2026-10-06 23:46:17 +02:00
fabien 535603f6d3 README : installation, configuration, authentification OpenID Connect 2026-10-06 22:15:59 +02:00
fabien 058370830f tâches planifiées : prompts cron persistés, outil scheduler, conversations liées aux tâches, rattrapage des exécutions manquées au redémarrage (go-cron MissedRunOnce) 2026-10-06 22:15:59 +02:00
fabien bedb6c98e7 authentification OpenID Connect (Keycloak) : flux authorization code, state signé, validation du token ID via JWKS, provisioning automatique des utilisateurs 2026-10-06 22:03:20 +02:00
fabien 51bbb45b32 compaction de contexte, filtre/pagination/suppression des conversations
- compaction automatique du contexte LLM quand la taille atteint le seuil
  (contexte max - taille de réponse max), par résumé de l'historique ancien
- filtre textuel (?q=) et pagination (20/page) de l'historique des conversations
- correctif ORM : offset de pagination en base 0
- suppression d'une conversation : messages associés supprimés, session en
  cours annulée, popup de confirmation (ConfirmDialog)
- titres de conversation nettoyés du markdown
- styles des listes markdown, variante danger du bouton
- go.mod/go.sum : dépendances go-rod et stealth manquantes du commit précédent
2026-10-05 23:46:12 +02:00
fabien a970bfa991 correction bug nouvelle conversation 2026-10-05 22:21:57 +02:00
fabien 509a3b14c8 Mise à jour du titre (imparfait) 2026-10-05 22:05:07 +02:00
fabien 345129c7bf Ajout de SEARXNG pour les recherches web 2026-10-05 19:05:24 +02:00
fabien 87eeb41364 paufinement de l'UI du chat 2026-10-04 21:55:31 +02:00
fabien 0a6ffab050 prise en charge des outils + weather 2026-10-04 21:03:51 +02:00
fabien a36f1231cd basic system promp 2026-09-23 00:09:26 +02:00
fabien b3be1a81e6 basic system prompt 2026-09-22 23:34:28 +02:00
fabien 1b5abb566e accès à un chat ouvert 2026-09-22 23:01:20 +02:00
fabien a92cc76a9a basic llm request stream 2026-09-22 21:15:23 +02:00
fabien f74e4d1043 Agent par défaut 2026-09-07 21:04:48 +02:00
fabien cf10b9073b divers fixes : bouton sauvegarde 2026-09-05 15:18:10 +02:00
fabien f1b176ae4a fixes 2026-09-05 11:15:36 +02:00
fabien 0bb8d1cb46 correction des pb de space : le spaceId est maintenant dans l'url uniquement 2026-09-02 21:50:26 +02:00
fabien c52460bac0 gestion des espaces dans l'url 2026-09-01 19:45:29 +02:00
fabien 4d72952b2c gestion des providers 2026-08-31 21:23:35 +02:00
fabien 357bf9b41c choix de la timezone dans un select 2026-08-23 13:33:40 +02:00
fabien 21e96702a0 add personal address en mieux 2026-08-19 00:08:58 +02:00
fabien d2a153536f add personal address 2026-08-18 23:57:08 +02:00
252 changed files with 10897 additions and 534 deletions

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
# Lassistanoque
Assistant IA auto-hébergé : backend Go (API + SPA embarquée), base SQLite, connecteurs LLM multiples (via [any-llm](https://github.com/mozilla-ai/any-llm-go)), authentification par mot de passe ou OpenID Connect.
## Prérequis
- Go >= 1.26
- Node.js + npm (build du frontend)
- (optionnel) un serveur OpenID Connect, ex. [Keycloak](https://www.keycloak.org/)
## Installation
```bash
# 1. frontend : build la SPA et l'écrit dans backend/internal/http/web
cd backend/web
npm install
npm run build
# 2. backend
cd ..
go build -o lassistanoque .
```
Le `Makefile` du backend enchaîne les deux et produit les binaires multi-plateformes dans `build/` :
```bash
make build # linux/amd64, linux/arm64, windows, darwin...
```
## Configuration
La configuration se fait par variables d'environnement, chargeables depuis un fichier `.env` placé dans le répertoire de lancement (backend) :
| Variable | Défaut | Description |
|----------------------------|--------------------|----------------------------------------------------|
| `LASSISTANOQUE_DB_TYPE` | `turso` | Type de base (seul `turso`/SQLite est implémenté) |
| `LASSISTANOQUE_DB_URL` | `lassistanoque.db` | Chemin du fichier SQLite |
| `LASSISTANOQUE_HTTP_PORT` | `3000` | Port du serveur HTTP |
| `LASSISTANOQUE_JWT_SECRET` | *(obligatoire)* | Secret de signature des tokens, ex. `openssl rand -base64 32` |
| `LASSISTANOQUE_STORAGE_TYPE`| `database` | Stockage des fichiers (seul `database` est implémenté) |
| `LASSISTANOQUE_SERVER_ID` | `default` | Identifiant du serveur |
Exemple `backend/.env` :
```env
LASSISTANOQUE_JWT_SECRET=<secret généré>
LASSISTANOQUE_HTTP_PORT=3000
```
## Lancement
```bash
./lassistanoque serve
```
- Les migrations de la base sont appliquées automatiquement au démarrage (ou manuellement avec `./lassistanoque migrate`).
- L'application est servie sur `http://localhost:3000` (SPA + API `/api`).
- Créez le premier compte depuis la page d'inscription (`/register`), puis configurez un fournisseur LLM et sa clé d'API dans les réglages de l'espace (`settings/models`).
## Authentification OpenID Connect
Les fournisseurs OIDC sont enregistrés dans la table `oidc` :
```sql
insert into oidc (id, label, domain, client_id, client_secret, wellknown_url, _date_created, _version)
values (
'keycloak', -- identifiant du fournisseur
'Keycloak', -- libellé affiché sur la page de connexion
'sso.example.com', -- domaine du fournisseur
'lassistanoque', -- client_id
'<client_secret>',
'https://<keycloak>/realms/<realm>/.well-known/openid-configuration',
datetime('now'),
'v1'
);
```
Côté fournisseur (Keycloak) :
- client de type *confidential*, *Standard Flow* (authorization code) activé ;
- *Valid Redirect URIs* : `http://localhost:3000/api/auth/oidc/callback` (l'URL de callback est reconstruite depuis le `Host` de la requête ; derrière un reverse proxy, transmettre `X-Forwarded-Proto` et `X-Forwarded-Host`).
Flux : les fournisseurs configurés apparaissent comme boutons sur la page de connexion → redirection vers le fournisseur → au retour, le backend valide le `state` signé, échange le code, vérifie le token ID (signature JWKS, émetteur, audience) puis délivre les tokens applicatifs. À la première connexion, l'utilisateur est créé automatiquement avec son espace personnel.
## Développement
```bash
# backend (API sur :3000)
cd backend
go run . serve
# frontend en mode dev (Vite sur :5173, API proxyfiée vers :3000)
cd web
npm run dev
```
Structure :
```
backend/
├── cmd/ # CLI (serve, migrate, version) et assemblage IoC
├── internal/
│ ├── adapter/ # implémentations : db (turso/SQLite), auth (password, oidc), llm, tools, security (JWT)
│ ├── domain/ # entités et interfaces des repositories
│ ├── http/ # serveur HTTP, routes, SPA embarquée
│ └── service/ # logique métier (auth, chat, agents, tasks...)
└── web/ # frontend SvelteKit
```
Tests : `go test ./backend/...`
+102
View File
@@ -0,0 +1,102 @@
<mxfile host="Electron">
<diagram name="Page-1" id="6ee6LexvoJe0i-CFFPA3">
<mxGraphModel dx="1067" dy="848" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-29" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=light-dark(#FFFFFF,transparent);strokeColor=#7D7D7D;" value="" vertex="1">
<mxGeometry height="270" width="220" x="540" y="140" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-15" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-1" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatService" vertex="1">
<mxGeometry height="120" width="140" x="290" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-2" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat -&amp;gt; Chat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-3" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="AddMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-22" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="getChat" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-16" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-5" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="BrainHandler" vertex="1">
<mxGeometry height="150" width="140" x="590" y="450" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-6" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="ProcessResponse" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-24" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="subscribe" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-7" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Start" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-8" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Stop" vertex="1">
<mxGeometry height="30" width="140" y="120" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-10" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-11" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatRepository" vertex="1">
<mxGeometry height="90" width="140" x="400" y="710" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-12" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-13" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-21" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-23" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-17" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="WebHandler" vertex="1">
<mxGeometry height="120" width="140" x="30" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-18" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-25" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-19" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="GetChatWithMessages" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-26" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="240" y="400" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-27" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-24">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-28" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-38" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="RunningSession" vertex="1">
<mxGeometry height="120" width="140" x="570" y="230" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-39" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 1" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-40" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 2" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-41" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 3" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-42" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Goroutine" vertex="1">
<mxGeometry height="30" width="60" x="560" y="150" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+108
View File
@@ -0,0 +1,108 @@
<mxfile host="Electron">
<diagram name="Page-1" id="6ee6LexvoJe0i-CFFPA3">
<mxGraphModel dx="1067" dy="848" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-43" parent="1" style="rounded=0;whiteSpace=wrap;html=1;" value="" vertex="1">
<mxGeometry height="250" width="510" x="260" y="380" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-29" parent="1" style="rounded=0;whiteSpace=wrap;html=1;fillColor=light-dark(#FFFFFF,transparent);strokeColor=#7D7D7D;" value="" vertex="1">
<mxGeometry height="270" width="220" x="520" y="50" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-15" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-1" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatService" vertex="1">
<mxGeometry height="120" width="140" x="290" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-2" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat -&amp;gt; Chat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-3" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="AddMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-22" parent="Nf3PCM6Bg_LUHkPQi3Rq-1" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="getChat" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-16" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-11">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-5" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="BrainHandler" vertex="1">
<mxGeometry height="150" width="140" x="590" y="450" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-6" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="ProcessResponse" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-24" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="subscribe" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-7" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Start" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-8" parent="Nf3PCM6Bg_LUHkPQi3Rq-5" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Stop" vertex="1">
<mxGeometry height="30" width="140" y="120" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-10" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-11" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="ChatRepository" vertex="1">
<mxGeometry height="90" width="140" x="400" y="710" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-12" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-13" parent="Nf3PCM6Bg_LUHkPQi3Rq-11" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="CreateMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-21" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-3">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-23" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-19" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-22">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-17" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="WebHandler" vertex="1">
<mxGeometry height="120" width="140" x="30" y="425" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-18" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewChat" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-25" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="NewMessage" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-19" parent="Nf3PCM6Bg_LUHkPQi3Rq-17" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="GetChatWithMessages" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-26" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="240" y="400" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-27" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-22" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-24">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-28" edge="1" parent="1" source="Nf3PCM6Bg_LUHkPQi3Rq-3" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="Nf3PCM6Bg_LUHkPQi3Rq-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-38" parent="1" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=30;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;whiteSpace=wrap;html=1;" value="RunningSession" vertex="1">
<mxGeometry height="120" width="140" x="550" y="140" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-39" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 1" vertex="1">
<mxGeometry height="30" width="140" y="30" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-40" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 2" vertex="1">
<mxGeometry height="30" width="140" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-41" parent="Nf3PCM6Bg_LUHkPQi3Rq-38" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;spacingLeft=4;spacingRight=4;overflow=hidden;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;rotatable=0;whiteSpace=wrap;html=1;" value="Item 3" vertex="1">
<mxGeometry height="30" width="140" y="90" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-42" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Goroutine" vertex="1">
<mxGeometry height="30" width="60" x="540" y="60" as="geometry" />
</mxCell>
<mxCell id="Nf3PCM6Bg_LUHkPQi3Rq-44" parent="1" style="text;html=1;whiteSpace=wrap;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;rounded=0;" value="Fusionner" vertex="1">
<mxGeometry height="30" width="60" x="480" y="380" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
+1
View File
@@ -0,0 +1 @@
.env
+163
View File
@@ -0,0 +1,163 @@
package cmd
import (
"time"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/oidc"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/browser"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/scheduler"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/searxng"
"trankilou.fr/lassistanoque/backend/internal/adapter/tools/weather"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/agent"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/chat"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/task"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
var db database.Database
// adapters
var storeAdapter storage.StorageProvider
var llmengine domain.LLMEngine
// services
var authService *auth.Service
var userService *user.Service
var storageService *storage.Service
var providerService *provider.Service
var agentService *agent.Service
var toolService *tool.Service
var chatService *chat.Service
var taskService *task.Service
var tokenManager auth.TokenManager
func init() {
var err error
db, err = database.GetDatabase()
if err != nil {
panic(err)
}
err = db.Migrate()
if err != nil {
panic(err)
}
}
func Close() {
db.Close()
}
// Adapters
func getStoreAdapter() storage.StorageProvider {
if storeAdapter == nil {
var err error
storeAdapter, err = file.GetStorageProvider(db.FileRepository())
if err != nil {
panic(err)
}
}
return storeAdapter
}
func getAuthenticators() map[string]auth.Authenticator {
authenticators := make(map[string]auth.Authenticator)
pwdAuth := password.NewPasswordAuthenticator(GetTokenManager(), db.UserRepository())
authenticators["password"] = pwdAuth
oidcAuth := oidc.NewOIDCAuthenticator(GetTokenManager(), db.OIDCProviderRepository(), db.UserRepository())
authenticators["oidc"] = oidcAuth
return authenticators
}
func GetTokenManager() auth.TokenManager {
if tokenManager == nil {
tokenManager = security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
}
return tokenManager
}
func GetLLMAdapter() domain.LLMEngine {
if llmengine == nil {
llmengine = llm.NewAnyLLMEngine()
}
return llmengine
}
// Services
func GetStorageService() *storage.Service {
if storageService == nil {
storageService = storage.NewService(getStoreAdapter())
}
return storageService
}
func GetAuthService() *auth.Service {
if authService == nil {
authService = auth.NewService(db.SettingsRepository(), db.UserRepository(), db.OIDCProviderRepository(), GetTokenManager(), getAuthenticators())
}
return authService
}
func GetUserService() *user.Service {
if userService == nil {
userService = user.NewService(db.UserRepository())
}
return userService
}
func GetProviderService() *provider.Service {
if providerService == nil {
providerService = provider.NewService(db.ProviderRepository(), db.UserRepository(), GetLLMAdapter())
}
return providerService
}
func GetAgentService() *agent.Service {
if agentService == nil {
agentService = agent.NewService(db.AgentRepository(), db.UserRepository())
}
return agentService
}
func GetChatService() *chat.Service {
if chatService == nil {
chatService = chat.NewService(
db.UserRepository(),
db.AgentRepository(),
db.ProviderRepository(),
db.ChatRepository(),
GetLLMAdapter(),
GetToolService(),
)
// l'exécution des tâches planifiées passe par le service de chat
GetTaskService().SetChatService(chatService)
}
return chatService
}
func GetTaskService() *task.Service {
if taskService == nil {
taskService = task.NewService(db.TaskRepository())
}
return taskService
}
func GetToolService() *tool.Service {
if toolService == nil {
toolService = tool.NewService(db.ToolRepository(), db.UserRepository())
toolService.RegisterToolImpl(&weather.WeatherTool{})
toolService.RegisterToolImpl(&searxng.SearxngTool{})
toolService.RegisterToolImpl(&browser.BrowserTool{})
toolService.RegisterToolImpl(scheduler.New(GetTaskService()))
}
return toolService
}
+58 -45
View File
@@ -1,18 +1,18 @@
package cmd package cmd
import ( import (
"context"
"errors"
"log/slog" "log/slog"
nethttp "net/http"
"os"
"os/signal"
"syscall"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password" "trankilou.fr/lassistanoque/backend/internal/gateway"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
"trankilou.fr/lassistanoque/backend/internal/adapter/file"
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/http" "trankilou.fr/lassistanoque/backend/internal/http"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/user"
) )
func init() { func init() {
@@ -35,48 +35,61 @@ func newServeCmd() *cobra.Command {
} }
func runServe() { func runServe() {
// database
db, err := database.GetDatabase()
if err != nil {
panic(err)
}
defer db.Close()
err = db.Migrate() defer Close()
if err != nil {
panic(err)
}
// storage
store, err := file.GetStorageProvider(db.FileRepository())
if err != nil {
panic(err)
}
// Adapters
tokenManager := security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
pwdAuth := password.NewPasswordAuthenticator(tokenManager, db.UserRepository())
// services
authService := auth.NewService(
db.SettingsRepository(),
db.UserRepository(),
map[string]auth.Authenticator{
"password": pwdAuth,
},
)
userService := user.NewService(db.UserRepository())
storageService := storage.NewService(store)
// http server // http server
router := http.NewRouter(http.Dependencies{ httpRouter := http.NewRouter(http.Dependencies{
StorageService: storageService, StorageService: GetStorageService(),
AuthService: authService, AuthService: GetAuthService(),
UserService: userService, UserService: GetUserService(),
TokenManager: tokenManager, ProviderService: GetProviderService(),
AgentService: GetAgentService(),
ChatService: GetChatService(),
ToolService: GetToolService(),
TaskService: GetTaskService(),
TokenManager: GetTokenManager(),
}) })
if err := router.Start(); err != nil { gatewayRouter := gateway.NewRouter(gateway.Dependencies{})
slog.Error("failed to start server", "error", err)
// task scheduler : replanifie les tâches persistées
GetTaskService().Start()
defer GetTaskService().Stop()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
// start Brain router
go func() {
if err := gatewayRouter.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) {
slog.Error("failed to start llm server", "error", err)
}
}()
// start HTTP router
go func() {
if err := httpRouter.Start(); err != nil && !errors.Is(err, nethttp.ErrServerClosed) {
slog.Error("failed to start http server", "error", err)
}
}()
<-ctx.Done()
// stop HTTP service
ctx, cancel1 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel1()
if err := httpRouter.Stop(ctx); err != nil {
slog.Error("failed to stop http server", "error", err)
} }
slog.Info("HTTP server stopped gracefully")
// stop LLM service
ctx, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel2()
if err := gatewayRouter.Stop(ctx); err != nil {
slog.Error("failed to stop llm server", "error", err)
}
slog.Info("LLM server stopped gracefully")
} }
@@ -0,0 +1,88 @@
package oidc
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
// providerMetadata est le document de découverte OpenID
// (.well-known/openid-configuration).
type providerMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
JwksURI string `json:"jwks_uri"`
}
type metadataCacheEntry struct {
metadata *providerMetadata
expiresAt time.Time
}
// discovery maintient un cache des documents de découverte par fournisseur.
type discovery struct {
httpClient *http.Client
mu sync.Mutex
entries map[string]*metadataCacheEntry
}
func newDiscovery() *discovery {
return &discovery{
httpClient: &http.Client{Timeout: 10 * time.Second},
entries: make(map[string]*metadataCacheEntry),
}
}
// Metadata retourne les endpoints du fournisseur, avec mise en cache.
func (d *discovery) Metadata(provider *domain.OIDCProvider) (*providerMetadata, error) {
d.mu.Lock()
entry, ok := d.entries[provider.ID]
if ok && time.Now().Before(entry.expiresAt) {
d.mu.Unlock()
return entry.metadata, nil
}
d.mu.Unlock()
metadata, err := d.fetch(provider.WellknownURL)
if err != nil {
return nil, fmt.Errorf("error fetching provider discovery document: %s", err)
}
if metadata.Issuer == "" || metadata.AuthorizationEndpoint == "" || metadata.TokenEndpoint == "" || metadata.JwksURI == "" {
return nil, fmt.Errorf("incomplete discovery document for provider %s", provider.ID)
}
d.mu.Lock()
d.entries[provider.ID] = &metadataCacheEntry{
metadata: metadata,
expiresAt: time.Now().Add(1 * time.Hour),
}
d.mu.Unlock()
return metadata, nil
}
func (d *discovery) fetch(wellknownURL string) (*providerMetadata, error) {
res, err := d.httpClient.Get(wellknownURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
metadata := &providerMetadata{}
if err := json.Unmarshal(body, metadata); err != nil {
return nil, err
}
return metadata, nil
}
+199
View File
@@ -0,0 +1,199 @@
package oidc
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"math/big"
"net/http"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
)
// jwk est une clé du document JWKS du fournisseur.
type jwk struct {
Kid string `json:"kid"`
Kty string `json:"kty"`
Alg string `json:"alg"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
Crv string `json:"crv"`
X string `json:"x"`
Y string `json:"y"`
}
type jwksDocument struct {
Keys []jwk `json:"keys"`
}
type jwksCacheEntry struct {
keys map[string]interface{}
expiresAt time.Time
}
// jwksStore maintient un cache des clés publiques de signature par fournisseur.
type jwksStore struct {
httpClient *http.Client
mu sync.Mutex
entries map[string]*jwksCacheEntry
}
func newJwksStore() *jwksStore {
return &jwksStore{
httpClient: &http.Client{Timeout: 10 * time.Second},
entries: make(map[string]*jwksCacheEntry),
}
}
// KeyFunc retourne une fonction de résolution de clé pour golang-jwt,
// valable pour un fournisseur donné.
func (s *jwksStore) KeyFunc(providerID string, jwksURI string) jwt.Keyfunc {
return func(t *jwt.Token) (interface{}, error) {
kid, ok := t.Header["kid"].(string)
if !ok || kid == "" {
return nil, fmt.Errorf("id token: kid manquant")
}
key, err := s.key(providerID, jwksURI, kid)
if err != nil {
return nil, err
}
return key, nil
}
}
// key retourne la clé publique correspondant au kid, avec mise en cache.
// Si le kid est inconnu, le cache est invalidé et le JWKS re-téléchargé
// (rotation de clés côté fournisseur).
func (s *jwksStore) key(providerID string, jwksURI string, kid string) (interface{}, error) {
fetch := func() (map[string]interface{}, error) {
keys, err := s.fetch(jwksURI)
if err != nil {
return nil, fmt.Errorf("error fetching provider JWKS: %s", err)
}
s.mu.Lock()
s.entries[providerID] = &jwksCacheEntry{
keys: keys,
expiresAt: time.Now().Add(1 * time.Hour),
}
s.mu.Unlock()
return keys, nil
}
s.mu.Lock()
entry, ok := s.entries[providerID]
valid := ok && time.Now().Before(entry.expiresAt)
s.mu.Unlock()
if valid {
if key, ok := entry.keys[kid]; ok {
return key, nil
}
}
keys, err := fetch()
if err != nil {
return nil, err
}
key, ok := keys[kid]
if !ok {
return nil, fmt.Errorf("id token: aucune clé JWKS ne correspond au kid %s", kid)
}
return key, nil
}
func (s *jwksStore) fetch(jwksURI string) (map[string]interface{}, error) {
res, err := s.httpClient.Get(jwksURI)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status %d", res.StatusCode)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
document := &jwksDocument{}
if err := json.Unmarshal(body, document); err != nil {
return nil, err
}
keys := make(map[string]interface{})
for _, k := range document.Keys {
if k.Use != "" && k.Use != "sig" {
continue
}
switch k.Kty {
case "RSA":
key, err := rsaKey(k)
if err != nil {
return nil, err
}
keys[k.Kid] = key
case "EC":
key, err := ecKey(k)
if err != nil {
return nil, err
}
keys[k.Kid] = key
}
}
return keys, nil
}
func rsaKey(k jwk) (*rsa.PublicKey, error) {
if k.N == "" || k.E == "" {
return nil, fmt.Errorf("clé RSA JWKS incomplète (kid %s)", k.Kid)
}
nBytes, err := base64.RawURLEncoding.DecodeString(k.N)
if err != nil {
return nil, fmt.Errorf("module RSA invalide: %s", err)
}
eBytes, err := base64.RawURLEncoding.DecodeString(k.E)
if err != nil {
return nil, fmt.Errorf("exposant RSA invalide: %s", err)
}
return &rsa.PublicKey{
N: new(big.Int).SetBytes(nBytes),
E: int(new(big.Int).SetBytes(eBytes).Int64()),
}, nil
}
func ecKey(k jwk) (*ecdsa.PublicKey, error) {
if k.X == "" || k.Y == "" {
return nil, fmt.Errorf("clé EC JWKS incomplète (kid %s)", k.Kid)
}
var curve elliptic.Curve
switch strings.ToUpper(k.Crv) {
case "P-256":
curve = elliptic.P256()
case "P-384":
curve = elliptic.P384()
case "P-521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("courbe EC non supportée: %s", k.Crv)
}
xBytes, err := base64.RawURLEncoding.DecodeString(k.X)
if err != nil {
return nil, fmt.Errorf("coordonnée x invalide: %s", err)
}
yBytes, err := base64.RawURLEncoding.DecodeString(k.Y)
if err != nil {
return nil, fmt.Errorf("coordonnée y invalide: %s", err)
}
return &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(xBytes),
Y: new(big.Int).SetBytes(yBytes),
}, nil
}
+226
View File
@@ -0,0 +1,226 @@
package oidc
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
)
var (
ErrRegistrationUnsupported = errors.New("registration via OIDC is not supported: users are provisioned at login")
ErrNoEmail = errors.New("id token does not contain an email claim")
ErrUserDisabled = errors.New("user is disabled")
)
// tokenResponse est la réponse de l'endpoint token du fournisseur.
type tokenResponse struct {
AccessToken string `json:"access_token"`
IDToken string `json:"id_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// idTokenClaims sont les claims du token ID émis par le fournisseur.
type idTokenClaims struct {
Email string `json:"email"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Picture string `json:"picture"`
jwt.RegisteredClaims
}
type OIDCAuthenticator struct {
tokenManager auth.TokenManager
oidcRepository domain.OIDCProviderRepository
userRepository domain.UserRepository
discovery *discovery
jwks *jwksStore
httpClient *http.Client
}
func NewOIDCAuthenticator(
tokenManager auth.TokenManager,
oidcRepository domain.OIDCProviderRepository,
userRepository domain.UserRepository,
) *OIDCAuthenticator {
return &OIDCAuthenticator{
tokenManager: tokenManager,
oidcRepository: oidcRepository,
userRepository: userRepository,
discovery: newDiscovery(),
jwks: newJwksStore(),
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
// AuthorizeURL construit l'URL d'autorisation vers laquelle rediriger l'utilisateur.
func (s *OIDCAuthenticator) AuthorizeURL(ctx context.Context, provider *domain.OIDCProvider, redirectURI string, state string) (string, error) {
metadata, err := s.discovery.Metadata(provider)
if err != nil {
return "", err
}
params := url.Values{}
params.Set("client_id", provider.ClientID)
params.Set("redirect_uri", redirectURI)
params.Set("response_type", "code")
params.Set("scope", "openid profile email")
params.Set("state", state)
return metadata.AuthorizationEndpoint + "?" + params.Encode(), nil
}
// ExchangeCode échange le code d'autorisation contre un token ID, le valide,
// puis délivre une session applicative.
func (s *OIDCAuthenticator) ExchangeCode(ctx context.Context, provider *domain.OIDCProvider, code string, redirectURI string) (*auth.Session, error) {
metadata, err := s.discovery.Metadata(provider)
if err != nil {
return nil, err
}
tokens, err := s.requestTokens(metadata.TokenEndpoint, provider, code, redirectURI)
if err != nil {
return nil, err
}
if tokens.IDToken == "" {
return nil, fmt.Errorf("provider %s did not return an id token", provider.ID)
}
claims, err := s.validateIDToken(provider, metadata, tokens.IDToken)
if err != nil {
return nil, err
}
if claims.Email == "" {
return nil, ErrNoEmail
}
user, err := s.provisionUser(claims)
if err != nil {
return nil, err
}
accessToken, expiresAt, err := s.tokenManager.GenerateAccessToken(user)
if err != nil {
return nil, fmt.Errorf("unable to generate access token: %s", err)
}
refreshToken, _, err := s.tokenManager.GenerateRefreshToken(user.ID)
if err != nil {
return nil, fmt.Errorf("unable to generate refresh token: %s", err)
}
return &auth.Session{
User: user,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
}, nil
}
// Authenticate implémente auth.Authenticator: le flux OIDC passe par le state
// signé qui transporte l'id du fournisseur, puis l'échange du code.
func (s *OIDCAuthenticator) Authenticate(ctx context.Context, creds auth.Credentials) (*auth.Session, error) {
providerID, err := s.tokenManager.ParseStateToken(creds.OIDCState)
if err != nil {
return nil, fmt.Errorf("invalid oidc state: %s", err)
}
provider, err := s.oidcRepository.FindOIDCProvider(providerID)
if err != nil {
return nil, fmt.Errorf("unknown oidc provider: %s", providerID)
}
return s.ExchangeCode(ctx, provider, creds.OIDCCode, creds.RedirectURI)
}
func (s *OIDCAuthenticator) Register(ctx context.Context, registration auth.Registration) (*auth.Session, error) {
return nil, ErrRegistrationUnsupported
}
func (s *OIDCAuthenticator) requestTokens(tokenEndpoint string, provider *domain.OIDCProvider, code string, redirectURI string) (*tokenResponse, error) {
form := url.Values{}
form.Set("grant_type", "authorization_code")
form.Set("code", code)
form.Set("redirect_uri", redirectURI)
form.Set("client_id", provider.ClientID)
form.Set("client_secret", provider.ClientSecret)
res, err := s.httpClient.Post(tokenEndpoint, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("error calling token endpoint: %s", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token endpoint returned status %d: %s", res.StatusCode, string(body))
}
tokens := &tokenResponse{}
if err := json.Unmarshal(body, tokens); err != nil {
return nil, err
}
return tokens, nil
}
func (s *OIDCAuthenticator) validateIDToken(provider *domain.OIDCProvider, metadata *providerMetadata, idToken string) (*idTokenClaims, error) {
claims := &idTokenClaims{}
_, err := jwt.ParseWithClaims(idToken, claims, s.jwks.KeyFunc(provider.ID, metadata.JwksURI),
jwt.WithValidMethods([]string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES521"}),
jwt.WithIssuer(metadata.Issuer),
jwt.WithAudience(provider.ClientID),
)
if err != nil {
return nil, fmt.Errorf("invalid id token: %s", err)
}
return claims, nil
}
func (s *OIDCAuthenticator) provisionUser(claims *idTokenClaims) (*domain.User, error) {
user, err := s.userRepository.FindUserByEmail(claims.Email)
if err == nil {
if !user.Enabled {
return nil, ErrUserDisabled
}
return user, nil
}
if !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
firstname := claims.GivenName
if firstname == "" {
firstname = claims.PreferredUsername
}
user, err = s.userRepository.CreateUser(&domain.User{
Email: claims.Email,
Firstname: firstname,
Lastname: claims.FamilyName,
Enabled: true,
})
if err != nil {
return nil, fmt.Errorf("error creating oidc user %s: %s", claims.Email, err)
}
_, err = s.userRepository.CreateTeam(user.ID, &domain.Team{
Label: "Espace personnel",
DateCreated: time.Now(),
})
if err != nil {
return nil, fmt.Errorf("error creating personal team for oidc user %s: %s", claims.Email, err)
}
return user, nil
}
@@ -0,0 +1,277 @@
package oidc
import (
"context"
"crypto/rand"
"crypto/rsa"
"database/sql"
"encoding/base64"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type mockOIDCRepo struct {
domain.OIDCProviderRepository
provider *domain.OIDCProvider
}
func (m *mockOIDCRepo) FindOIDCProvider(id string) (*domain.OIDCProvider, error) {
return m.provider, nil
}
type mockUserRepo struct {
domain.UserRepository
created *domain.User
}
func (m *mockUserRepo) FindUserByEmail(email string) (*domain.User, error) {
return nil, sql.ErrNoRows
}
func (m *mockUserRepo) CreateUser(user *domain.User) (*domain.User, error) {
user.ID = "user-1"
m.created = user
return user, nil
}
func (m *mockUserRepo) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) {
return team, nil
}
// fakeIdP simule un fournisseur OpenID Connect (Keycloak-like):
// document de découverte, JWKS et endpoint token.
type fakeIdP struct {
server *httptest.Server
key *rsa.PrivateKey
kid string
idToken string
provider *domain.OIDCProvider
redirectURI string
tokenCalls int
}
func newFakeIdP(t *testing.T) *fakeIdP {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
idp := &fakeIdP{key: key, kid: "test-key-1", redirectURI: "http://localhost:3000/api/auth/oidc/callback"}
mux := http.NewServeMux()
server := httptest.NewServer(mux)
idp.server = server
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"issuer": server.URL,
"authorization_endpoint": server.URL + "/authorize",
"token_endpoint": server.URL + "/token",
"jwks_uri": server.URL + "/jwks",
})
})
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string][]map[string]string{
"keys": {{
"kid": idp.kid,
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": base64.RawURLEncoding.EncodeToString(key.PublicKey.N.Bytes()),
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(key.PublicKey.E)).Bytes()),
}},
})
})
mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
idp.tokenCalls++
if err := r.ParseForm(); err != nil {
t.Errorf("invalid token request form: %s", err)
}
if r.Form.Get("grant_type") != "authorization_code" {
t.Errorf("unexpected grant_type: %s", r.Form.Get("grant_type"))
}
if r.Form.Get("redirect_uri") != idp.redirectURI {
t.Errorf("unexpected redirect_uri: %s", r.Form.Get("redirect_uri"))
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"access_token": "opaque-access-token",
"id_token": idp.idToken,
"token_type": "Bearer",
"expires_in": 300,
})
})
idp.provider = &domain.OIDCProvider{
ID: "kc",
Label: "Keycloak",
ClientID: "lassistanoque",
ClientSecret: "secret",
WellknownURL: server.URL + "/.well-known/openid-configuration",
}
return idp
}
func (idp *fakeIdP) signIDToken(t *testing.T, claims jwt.MapClaims) string {
t.Helper()
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = idp.kid
signed, err := token.SignedString(idp.key)
if err != nil {
t.Fatal(err)
}
return signed
}
func TestMain(m *testing.M) {
os.Setenv("LASSISTANOQUE_JWT_SECRET", "test-secret")
os.Exit(m.Run())
}
func newAuthenticator(idp *fakeIdP) *OIDCAuthenticator {
tokenManager := security.NewJwtTokenManager(time.Hour, 24*time.Hour, "lassistanoque")
return NewOIDCAuthenticator(tokenManager, &mockOIDCRepo{provider: idp.provider}, &mockUserRepo{})
}
func TestAuthorizeURL(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
authenticator := newAuthenticator(idp)
state, err := authenticator.tokenManager.GenerateStateToken(idp.provider.ID)
if err != nil {
t.Fatal(err)
}
url, err := authenticator.AuthorizeURL(context.Background(), idp.provider, idp.redirectURI, state)
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(url, idp.server.URL+"/authorize?") {
t.Errorf("unexpected authorize url: %s", url)
}
for _, expected := range []string{
"client_id=lassistanoque",
"response_type=code",
"scope=openid+profile+email",
"state=",
} {
if !strings.Contains(url, expected) {
t.Errorf("authorize url misses %s: %s", expected, url)
}
}
}
func TestExchangeCodeCreatesUserAndSession(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
idp.idToken = idp.signIDToken(t, jwt.MapClaims{
"iss": idp.server.URL,
"aud": idp.provider.ClientID,
"exp": time.Now().Add(5 * time.Minute).Unix(),
"email": "fabien@example.com",
"given_name": "Fabien",
"family_name": "Dupont",
})
userRepo := &mockUserRepo{}
tokenManager := security.NewJwtTokenManager(time.Hour, 24*time.Hour, "lassistanoque")
authenticator := NewOIDCAuthenticator(tokenManager, &mockOIDCRepo{provider: idp.provider}, userRepo)
session, err := authenticator.ExchangeCode(context.Background(), idp.provider, "auth-code", idp.redirectURI)
if err != nil {
t.Fatal(err)
}
if idp.tokenCalls != 1 {
t.Errorf("expected 1 token endpoint call, got %d", idp.tokenCalls)
}
if userRepo.created == nil {
t.Fatal("user was not provisioned")
}
if userRepo.created.Email != "fabien@example.com" ||
userRepo.created.Firstname != "Fabien" ||
userRepo.created.Lastname != "Dupont" {
t.Errorf("provisioned user mismatch: %+v", userRepo.created)
}
if !userRepo.created.Enabled {
t.Error("provisioned user should be enabled")
}
if session.AccessToken == "" || session.RefreshToken == "" {
t.Error("session tokens are empty")
}
if session.User == nil || session.User.Email != "fabien@example.com" {
t.Errorf("session user mismatch: %+v", session.User)
}
// le token applicatif doit être valide
user, err := tokenManager.ParseAndValidate(session.AccessToken)
if err != nil {
t.Fatalf("generated access token invalid: %s", err)
}
if user.ID != "user-1" {
t.Errorf("unexpected user id in access token: %s", user.ID)
}
}
func TestExchangeCodeRejectsForgedIDToken(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
// token signé par une autre clé que celle du JWKS
forgedKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iss": idp.server.URL,
"aud": idp.provider.ClientID,
"exp": time.Now().Add(5 * time.Minute).Unix(),
"email": "attacker@example.com",
})
token.Header["kid"] = idp.kid
idp.idToken, err = token.SignedString(forgedKey)
if err != nil {
t.Fatal(err)
}
authenticator := newAuthenticator(idp)
if _, err := authenticator.ExchangeCode(context.Background(), idp.provider, "auth-code", idp.redirectURI); err == nil {
t.Fatal("exchange should fail with a forged id token")
}
}
func TestExchangeCodeRejectsWrongIssuer(t *testing.T) {
idp := newFakeIdP(t)
defer idp.server.Close()
idp.idToken = idp.signIDToken(t, jwt.MapClaims{
"iss": "https://evil.example.com",
"aud": idp.provider.ClientID,
"exp": time.Now().Add(5 * time.Minute).Unix(),
"email": "attacker@example.com",
})
authenticator := newAuthenticator(idp)
if _, err := authenticator.ExchangeCode(context.Background(), idp.provider, "auth-code", idp.redirectURI); err == nil {
t.Fatal("exchange should fail with a wrong issuer")
}
}
@@ -11,7 +11,13 @@ import (
type Database interface { type Database interface {
SettingsRepository() domain.SettingsRepository SettingsRepository() domain.SettingsRepository
UserRepository() domain.UserRepository UserRepository() domain.UserRepository
OIDCProviderRepository() domain.OIDCProviderRepository
FileRepository() domain.FileRepository FileRepository() domain.FileRepository
ProviderRepository() domain.ProviderRepository
AgentRepository() domain.AgentRepository
ChatRepository() domain.ChatRepository
ToolRepository() domain.ToolRepository
TaskRepository() domain.TaskRepository
Migrate() error Migrate() error
Close() Close()
} }
@@ -0,0 +1,84 @@
package orm
import (
"fmt"
"strings"
)
func buildInsertSql(tablename string, fields []dbfield, dialect Dialect) string {
cols := make([]string, len(fields))
placeholders := make([]string, len(fields))
for i, f := range fields {
cols[i] = f.dbname
placeholders[i] = fmt.Sprintf("$%d", i+1)
}
return fmt.Sprintf(
"insert into %s (%s) values (%s)",
tablename,
strings.Join(cols, ","),
strings.Join(placeholders, ","),
)
}
func buildUpdateSql(tablename string, fields []dbfield, dialect Dialect) string {
predicates := make([]string, len(fields)-1)
var where string
i := 0
for _, f := range fields {
if f.dbname == "id" {
where = fmt.Sprintf("id=$%d and _version=$%d", len(fields), len(fields)+1)
} else {
predicates[i] = fmt.Sprintf("%s=$%d", f.dbname, i+1)
i++
}
}
return fmt.Sprintf(
"update %s set %s where %s",
tablename,
strings.Join(predicates, ","),
where,
)
}
func buildSelectbyidSql(tablename string, fields []dbfield, dialect Dialect) string {
columns := make([]string, len(fields))
var where string
for i, f := range fields {
columns[i] = fmt.Sprintf("%s", f.dbname)
}
where = "id=$1"
return fmt.Sprintf(
"select %s from %s where %s",
strings.Join(columns, ","),
tablename,
where,
)
}
func buildDeletebyidSql(tablename string, fields []dbfield, dialect Dialect) string {
where := "id=$1"
return fmt.Sprintf(
"delete from %s where %s",
tablename,
where,
)
}
func buildSelectWhere(tablename string, fields []dbfield, dialect Dialect) string {
columns := make([]string, len(fields))
for i, f := range fields {
columns[i] = fmt.Sprintf("%s", f.dbname)
}
return fmt.Sprintf(
"select %s from %s",
strings.Join(columns, ","),
tablename,
)
}
func buildDeleteWhere(tablename string, fields []dbfield, dialect Dialect) string {
return fmt.Sprintf(
"delete from %s",
tablename,
)
}
@@ -0,0 +1,289 @@
package orm
import (
"database/sql"
"fmt"
"reflect"
"strings"
"time"
)
type SqliteDialect struct{}
func NewSqliteDialect() *SqliteDialect {
return &SqliteDialect{}
}
func convertType(f dbfield) string {
switch f.stype {
case "string", "*string":
return "text"
case "bool", "int", "*int", "time.Time", "*time.Time":
return "numeric"
case "[]byte":
return "blob"
}
return "text"
}
func defaultValue(f dbfield) string {
if f.dbname == "_date_created" {
return "current_timestamp"
}
switch f.stype {
case "string":
return "''"
case "bool", "int", "time.Time":
return "0"
}
return "''"
}
func (d *SqliteDialect) TableExists(db *sql.DB, tableName string) (bool, error) {
sql := "select name from sqlite_schema where type='table' and name=$1"
rows, err := db.Query(sql, tableName)
if err != nil {
return false, err
}
if rows.Next() {
rows.Close()
return true, nil
}
return false, nil
}
func (d *SqliteDialect) ColumnSpec(f dbfield) (string, error) {
if f.dbname == "id" {
return "id text not null primary key", nil
}
if strings.HasPrefix(f.stype, "*") || strings.HasPrefix(f.stype, "[]") {
return fmt.Sprintf("%s %s", f.dbname, convertType(f)), nil
}
return fmt.Sprintf("%s %s not null default %s", f.dbname, convertType(f), defaultValue(f)), nil
}
// ScanResult scans a single row into the provided fields slice.
// For SQLite, this handles time.Time fields which are returned as strings.
func (d *SqliteDialect) ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error {
// SQLite returns time.Time as strings, so we need special handling
// Check if any field is a time type
hasTimeField := false
for _, f := range fieldTypes {
if f.stype == "time.Time" || f.stype == "*time.Time" {
hasTimeField = true
break
}
}
if !hasTimeField {
// No time fields, use standard scan
return row.Scan(fields...)
}
// We have time fields - need custom handling
// Create temp destinations for all fields
tempFields := make([]any, len(fields))
fieldInfos := make([]struct {
index int
dest any
isTime bool
isTimePtr bool
}, len(fields))
for i, f := range fieldTypes {
isTime := f.stype == "time.Time"
isTimePtr := f.stype == "*time.Time"
fieldInfos[i] = struct {
index int
dest any
isTime bool
isTimePtr bool
}{
index: i,
dest: fields[i],
isTime: isTime,
isTimePtr: isTimePtr,
}
if isTime || isTimePtr {
var s sql.NullString
tempFields[i] = &s
} else {
tempFields[i] = fields[i]
}
}
if err := row.Scan(tempFields...); err != nil {
return err
}
// Copy time values back to original destinations
for _, info := range fieldInfos {
if info.isTime || info.isTimePtr {
s := tempFields[info.index].(*sql.NullString)
if s.Valid {
// Try to parse the string as time
t, err := tryParseTime(s.String)
if err != nil {
return fmt.Errorf("failed to parse time field: %w", err)
}
// Set the value in the original destination
destVal := reflect.ValueOf(info.dest).Elem()
if info.isTime {
// dest is *time.Time, set the value
destVal.Set(reflect.ValueOf(t))
} else if info.isTimePtr {
// dest is **time.Time, allocate and set
ptr := reflect.New(destVal.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(t))
destVal.Set(ptr)
}
}
}
}
return nil
}
// ScanRows scans multiple rows using the dialect-specific logic
func (d *SqliteDialect) ScanRows(rows *sql.Rows, fieldDest func() []any, fieldTypes []dbfield) ([]any, error) {
// Check if any field is a time type
hasTimeField := false
for _, f := range fieldTypes {
if f.stype == "time.Time" || f.stype == "*time.Time" {
hasTimeField = true
break
}
}
if !hasTimeField {
// No time fields, use standard scan
var results []any
for rows.Next() {
fields := fieldDest()
if err := rows.Scan(fields...); err != nil {
return nil, err
}
results = append(results, fields...)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
// We have time fields - need custom handling
var results []any
for rows.Next() {
fields := fieldDest()
// Create temp destinations for all fields
tempFields := make([]any, len(fields))
fieldInfos := make([]struct {
index int
dest any
isTime bool
isTimePtr bool
}, len(fields))
for i, f := range fieldTypes {
isTime := f.stype == "time.Time"
isTimePtr := f.stype == "*time.Time"
fieldInfos[i] = struct {
index int
dest any
isTime bool
isTimePtr bool
}{
index: i,
dest: fields[i],
isTime: isTime,
isTimePtr: isTimePtr,
}
if isTime || isTimePtr {
var s sql.NullString
tempFields[i] = &s
} else {
tempFields[i] = fields[i]
}
}
if err := rows.Scan(tempFields...); err != nil {
return nil, err
}
// Copy time values back to original destinations
for _, info := range fieldInfos {
if info.isTime || info.isTimePtr {
s := tempFields[info.index].(*sql.NullString)
if s.Valid {
t, err := tryParseTime(s.String)
if err != nil {
return nil, fmt.Errorf("failed to parse time field: %w", err)
}
// Set the value in the original destination
destVal := reflect.ValueOf(info.dest).Elem()
if info.isTime {
// dest is *time.Time, set the value
destVal.Set(reflect.ValueOf(t))
} else if info.isTimePtr {
// dest is **time.Time, allocate and set
ptr := reflect.New(destVal.Type().Elem())
ptr.Elem().Set(reflect.ValueOf(t))
destVal.Set(ptr)
}
}
}
}
results = append(results, fields...)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
// tryParseTime attempts to parse a time string in various common formats
// SQLite driver returns time as "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000"
// We need to strip the monotonic clock part (m=...) before parsing
func tryParseTime(s string) (time.Time, error) {
// Remove monotonic clock part if present
// Format: "2006-01-02 15:04:05.999999999 -0700 MST m=+0.000000000"
if idx := strings.Index(s, " m="); idx != -1 {
s = s[:idx]
}
// Try common formats
formats := []string{
time.RFC3339,
time.RFC3339Nano,
"2006-01-02T15:04:05Z07:00",
"2006-01-02 15:04:05.999999999-07:00",
"2006-01-02 15:04:05",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05+00:00",
// Go's default time.String() format (without monotonic clock)
"2006-01-02 15:04:05.999999999 -0700 MST",
// SQLite without timezone
"2006-01-02 15:04:05.999999",
}
var err error
var t time.Time
for _, format := range formats {
t, err = time.Parse(format, s)
if err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unable to parse time: %s", s)
}
@@ -0,0 +1,376 @@
package orm
import (
"database/sql"
"fmt"
"reflect"
"strings"
"time"
)
type dbfield struct {
name string
stype string
dbname string
}
type Dialect interface {
TableExists(db *sql.DB, tableName string) (bool, error)
ColumnSpec(f dbfield) (string, error)
ScanResult(row interface{ Scan(dest ...any) error }, fields []any, fieldTypes []dbfield) error
}
type Table[T any] struct {
db *sql.DB
dialect Dialect
tablename string
fields []dbfield
insertsql string
updatesql string
selectbyid string
deletebyid string
selectwhere string
deletewhere string
}
func NewTable[T any](db *sql.DB, dialect Dialect, tablename string) (Table[T], error) {
var instance T
t := reflect.TypeOf(instance)
fields := make([]dbfield, 0)
for field := range t.Fields() {
if field.Tag.Get("db") != "-" {
f := dbfield{
name: field.Name,
stype: field.Type.String(),
dbname: field.Tag.Get("db"),
}
fields = append(fields, f)
}
}
return Table[T]{
db: db,
dialect: dialect,
tablename: tablename,
fields: fields,
insertsql: buildInsertSql(tablename, fields, dialect),
updatesql: buildUpdateSql(tablename, fields, dialect),
selectbyid: buildSelectbyidSql(tablename, fields, dialect),
deletebyid: buildDeletebyidSql(tablename, fields, dialect),
selectwhere: buildSelectWhere(tablename, fields, dialect),
deletewhere: buildDeleteWhere(tablename, fields, dialect),
}, nil
}
func (t Table[T]) Sync() error {
if exists, err := t.dialect.TableExists(t.db, t.tablename); exists {
return nil
} else if err != nil {
return err
}
columns := make([]string, len(t.fields))
for i, f := range t.fields {
c, err := t.dialect.ColumnSpec(f)
if err != nil {
return err
}
columns[i] = c
}
sql := fmt.Sprintf("create table %s (%s)", t.tablename, strings.Join(columns, ","))
_, err := t.db.Exec(sql)
if err != nil {
return err
}
return nil
}
func (t Table[T]) Get(id string) (*T, error) {
row := t.db.QueryRow(t.selectbyid, id)
if row == nil {
return nil, sql.ErrNoRows
}
var obj T
// Create field destinations
ptrVal := reflect.ValueOf(&obj).Elem()
fields := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := ptrVal.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
fields[i] = fieldValue.Addr().Interface()
}
// Use dialect to scan with proper type handling
if err := t.dialect.ScanResult(row, fields, t.fields); err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, err
}
return &obj, nil
}
func (t Table[T]) SelectOne(where string, args ...any) (*T, error) {
query := t.selectwhere + " where " + where + " limit 1"
row := t.db.QueryRow(query, args...)
if row == nil {
return nil, sql.ErrNoRows
}
var obj T
ptrVal := reflect.ValueOf(&obj).Elem()
fields := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := ptrVal.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
fields[i] = fieldValue.Addr().Interface()
}
if err := t.dialect.ScanResult(row, fields, t.fields); err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, err
}
return &obj, nil
}
type query struct {
where string
args []any
orderby string
pagenum int
pagesize int
}
type Option func(*query)
func WithWhere(where string, args ...any) Option {
return func(q *query) {
q.where = where
q.args = args
}
}
func WithOrder(orderby string) Option {
return func(q *query) {
q.orderby = orderby
}
}
func WithPagination(pagenum, pagesize int) Option {
return func(q *query) {
q.pagenum = pagenum
q.pagesize = pagesize
}
}
func (t Table[T]) Select(options ...Option) ([]*T, error) {
q := &query{}
for _, o := range options {
o(q)
}
query := t.selectwhere
if q.where != "" {
query += " where " + q.where
}
if q.orderby != "" {
query += " order by " + q.orderby
}
if q.pagesize != 0 {
query += fmt.Sprintf(" limit %d", q.pagesize)
if q.pagenum > 0 {
// pages en base 0 : la page 0 est la première page
query += fmt.Sprintf(" offset %d", q.pagesize*q.pagenum)
}
}
var err error
var rows *sql.Rows
if q.args == nil {
rows, err = t.db.Query(query)
} else {
rows, err = t.db.Query(query, q.args...)
}
if err != nil {
return nil, err
}
defer rows.Close()
var results []*T
for rows.Next() {
var obj T
ptrVal := reflect.ValueOf(&obj).Elem()
fields := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := ptrVal.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
fields[i] = fieldValue.Addr().Interface()
}
// Use dialect to scan with proper type handling
// *sql.Rows implements Scanner interface
if err := t.dialect.ScanResult(rows, fields, t.fields); err != nil {
return nil, err
}
results = append(results, &obj)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
func (t Table[T]) Delete(id string) error {
_, err := t.db.Exec(t.deletebyid, id)
return err
}
func (t Table[T]) DeleteWhere(where string, args ...any) error {
query := t.deletewhere
if where != "" {
query += " where " + where
}
_, err := t.db.Exec(query, args...)
return err
}
func (t Table[T]) Insert(value *T) (*T, error) {
// Get a settable reflect value of the struct
// value is *T, &value is **T, .Elem() gives *T (settables), .Elem() gives T (settables)
val := reflect.ValueOf(&value).Elem().Elem()
// Auto-fill ID field if it exists and is empty
if fieldID := val.FieldByName("ID"); fieldID.IsValid() && fieldID.CanSet() && fieldID.Kind() == reflect.String {
if fieldID.String() == "" {
fieldID.SetString(GenID())
}
}
// Auto-fill VersionId field if it exists and is empty
if fieldVersion := val.FieldByName("VersionId"); fieldVersion.IsValid() && fieldVersion.CanSet() && fieldVersion.Kind() == reflect.String {
if fieldVersion.String() == "" {
fieldVersion.SetString(GenID())
}
}
// Auto-fill DateCreated or DateCreate field if it exists
now := time.Now()
if fieldCreate := val.FieldByName("DateCreated"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) {
fieldCreate.Set(reflect.ValueOf(now))
} else if fieldCreate := val.FieldByName("DateCreate"); fieldCreate.IsValid() && fieldCreate.CanSet() && fieldCreate.Type() == reflect.TypeOf(now) {
fieldCreate.Set(reflect.ValueOf(now))
}
vals := make([]any, len(t.fields))
for i, f := range t.fields {
fieldValue := val.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
vals[i] = fieldValue.Interface()
}
_, err := t.db.Exec(t.insertsql, vals...)
if err != nil {
return nil, err
}
return value, nil
}
func (t Table[T]) Update(value *T) (*T, error) {
// Get a settable reflect value of the struct
val := reflect.ValueOf(&value).Elem().Elem()
// Save old VersionId for WHERE clause
versionField := val.FieldByName("VersionId")
if !versionField.IsValid() {
return nil, fmt.Errorf("VersionId field not found")
}
oldVersionId := versionField.String()
// Generate new VersionId for optimistic locking
newVersionId := GenID()
versionField.SetString(newVersionId)
// Update DateUpdated field if it exists
if fieldUpdated := val.FieldByName("DateUpdated"); fieldUpdated.IsValid() && fieldUpdated.CanSet() {
if fieldUpdated.Kind() == reflect.Ptr {
now := time.Now()
if fieldUpdated.IsNil() {
fieldUpdated.Set(reflect.New(fieldUpdated.Type().Elem()))
}
fieldUpdated.Elem().Set(reflect.ValueOf(now))
} else if fieldUpdated.Type() == reflect.TypeOf(time.Time{}) {
fieldUpdated.Set(reflect.ValueOf(time.Now()))
}
}
// Build the values slice for the prepared statement
// UPDATE SQL expects: SET fields (excluding ID), then WHERE: ID and VersionId (old version)
vals := make([]any, 0, len(t.fields))
// Add all fields except ID for SET clause (includes new VersionId)
for _, f := range t.fields {
if f.dbname == "id" {
continue // Skip ID field for SET clause
}
fieldValue := val.FieldByName(f.name)
if !fieldValue.IsValid() {
return nil, fmt.Errorf("field %s not found in struct", f.name)
}
vals = append(vals, fieldValue.Interface())
}
// Add ID and OLD VersionId at the end for WHERE clause
idField := val.FieldByName("ID")
if !idField.IsValid() {
return nil, fmt.Errorf("ID field not found")
}
vals = append(vals, idField.Interface(), oldVersionId)
result, err := t.db.Exec(t.updatesql, vals...)
if err != nil {
return nil, err
}
// Check if any row was affected
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, fmt.Errorf("no rows affected: record not found or version mismatch")
}
return value, nil
}
func (t Table[T]) Debug() {
fmt.Println("insert: ", t.insertsql)
fmt.Println("update: ", t.updatesql)
fmt.Println("selectbyid: ", t.selectbyid)
fmt.Println("deletebyid: ", t.deletebyid)
fmt.Println("selectwhere: ", t.selectwhere)
fmt.Println("deletewhere: ", t.deletewhere)
}
@@ -0,0 +1,21 @@
package orm
import (
"crypto/rand"
"encoding/hex"
"github.com/sixafter/nanoid"
)
func GenID() string {
id, err := nanoid.New()
if err != nil {
// Fallback to random hex
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "fallback-id"
}
return hex.EncodeToString(b)
}
return id.String()
}
@@ -0,0 +1,63 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoAgentRepository struct {
db *sql.DB
agentTable orm.Table[domain.Agent]
}
func NewTursoAgentRepository(db *sql.DB) *TursoAgentRepository {
dialect := orm.NewSqliteDialect()
agentTable, err := orm.NewTable[domain.Agent](db, dialect, "agents")
if err != nil {
log.Fatalf("error creating lasebuche agent table")
}
return &TursoAgentRepository{
db: db,
agentTable: agentTable,
}
}
func (r *TursoAgentRepository) ListAgents(userID string, teamID string) ([]*domain.Agent, error) {
return r.agentTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
)
}
func (r *TursoAgentRepository) GetAgent(userID string, teamID string, id string) (*domain.Agent, error) {
return r.agentTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoAgentRepository) CreateAgent(userID string, agent *domain.Agent) (*domain.Agent, error) {
return r.agentTable.Insert(agent)
}
func (r *TursoAgentRepository) UpdateAgent(userID string, agent *domain.Agent) (*domain.Agent, error) {
return r.agentTable.Update(agent)
}
func (r *TursoAgentRepository) DeleteAgent(userID string, teamID string, id string) error {
return r.agentTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
@@ -0,0 +1,140 @@
package turso
import (
"database/sql"
"fmt"
"log"
"strings"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoChatRepository struct {
db *sql.DB
chatTable orm.Table[domain.Chat]
messageTable orm.Table[domain.Message]
}
func NewTursoChatRepository(db *sql.DB) *TursoChatRepository {
dialect := orm.NewSqliteDialect()
chatTable, err := orm.NewTable[domain.Chat](db, dialect, "chats")
messageTable, err := orm.NewTable[domain.Message](db, dialect, "chat_messages")
if err != nil {
log.Fatalf("error creating lasebuche agent table")
}
return &TursoChatRepository{
db: db,
chatTable: chatTable,
messageTable: messageTable,
}
}
func (r *TursoChatRepository) ListChats(userID string, teamID string, page int, query string, taskID string) ([]*domain.Chat, error) {
where := "team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)"
args := []any{teamID, userID}
if query != "" {
// % et _ sont des jokers LIKE : on les neutralise
escaped := strings.ReplaceAll(query, `\`, `\\`)
escaped = strings.ReplaceAll(escaped, `%`, `\%`)
escaped = strings.ReplaceAll(escaped, `_`, `\_`)
where += " and title like $3 escape '\\'"
args = append(args, "%"+escaped+"%")
}
if taskID != "" {
where += fmt.Sprintf(" and task_id=$%d", len(args)+1)
args = append(args, taskID)
}
return r.chatTable.Select(
orm.WithWhere(where, args...),
orm.WithOrder("_date_created desc"),
orm.WithPagination(page, 20),
)
}
func (r *TursoChatRepository) ListChatsWithUnfreshTitles(teamID string) ([]*domain.Chat, error) {
return r.chatTable.Select(
orm.WithWhere(
"team_id=$1 and fresh_title=0",
teamID,
),
)
}
func (r *TursoChatRepository) GetChat(userID string, teamID string, id string) (*domain.Chat, error) {
return r.chatTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoChatRepository) CreateChat(userID string, chat *domain.Chat) (*domain.Chat, error) {
return r.chatTable.Insert(chat)
}
func (r *TursoChatRepository) UpdateChat(userID string, chat *domain.Chat) (*domain.Chat, error) {
return r.chatTable.Update(chat)
}
func (r *TursoChatRepository) DeleteChat(userID string, teamID string, id string) error {
// les messages n'ont pas de clé étrangère cascade : suppression explicite
err := r.messageTable.DeleteWhere(
"chat_id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
if err != nil {
return err
}
return r.chatTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoChatRepository) GetChatMessages(userID string, teamID string, chatID string, withToolCallResponses bool) ([]*domain.Message, error) {
var messages []*domain.Message
var err error
if !withToolCallResponses {
messages, err = r.messageTable.Select(
orm.WithWhere(
`chat_id=$1
and team_id=$2
and team_id in (select team_id from user_teams where user_id=$3)
and (tool_call_id='' or tool_call_id is null)`,
chatID,
teamID,
userID,
),
orm.WithOrder("_date_created asc"),
)
} else {
messages, err = r.messageTable.Select(
orm.WithWhere(
"chat_id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
chatID,
teamID,
userID,
),
orm.WithOrder("_date_created asc"),
)
}
if err != nil {
return nil, err
}
for _, m := range messages {
m.Json2toolCall()
}
return messages, nil
}
func (r *TursoChatRepository) CreateChatMessage(userID string, message *domain.Message) (*domain.Message, error) {
message.ToolCall2Json()
return r.messageTable.Insert(message)
}
+25 -1
View File
@@ -24,7 +24,7 @@ type TursoDB struct {
func NewTursoDB(cfg *config.Config) (*TursoDB, error) { func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
db, err := sql.Open("turso", cfg.DatabaseURL) db, err := sql.Open("turso", cfg.DatabaseURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err) return nil, fmt.Errorf("error connecting to db %s: %s", cfg.DatabaseURL, err)
} }
return &TursoDB{ return &TursoDB{
DB: db, DB: db,
@@ -64,6 +64,30 @@ func (db *TursoDB) SettingsRepository() domain.SettingsRepository {
return NewTursoSettingsRepository(db.DB) return NewTursoSettingsRepository(db.DB)
} }
func (db *TursoDB) OIDCProviderRepository() domain.OIDCProviderRepository {
return NewTursoOIDCProviderRepository(db.DB)
}
func (db *TursoDB) FileRepository() domain.FileRepository { func (db *TursoDB) FileRepository() domain.FileRepository {
return NewTursoFileRepository(db.DB) return NewTursoFileRepository(db.DB)
} }
func (db *TursoDB) ProviderRepository() domain.ProviderRepository {
return NewTursoProviderRepository(db.DB)
}
func (db *TursoDB) AgentRepository() domain.AgentRepository {
return NewTursoAgentRepository(db.DB)
}
func (db *TursoDB) ChatRepository() domain.ChatRepository {
return NewTursoChatRepository(db.DB)
}
func (db *TursoDB) ToolRepository() domain.ToolRepository {
return NewTursoToolRepository(db.DB)
}
func (db *TursoDB) TaskRepository() domain.TaskRepository {
return NewTursoTaskRepository(db.DB)
}
@@ -6,19 +6,19 @@ import (
"log" "log"
"time" "time"
"gitea.trankilou.fr/fabien/lasebuche" "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility" "trankilou.fr/lassistanoque/backend/internal/utility"
) )
type TursoFileRepository struct { type TursoFileRepository struct {
db *sql.DB db *sql.DB
FileTable lasebuche.Table[domain.File] FileTable orm.Table[domain.File]
} }
func NewTursoFileRepository(db *sql.DB) *TursoFileRepository { func NewTursoFileRepository(db *sql.DB) *TursoFileRepository {
dialect := lasebuche.NewSqliteDialect() dialect := orm.NewSqliteDialect()
fileTable, err := lasebuche.NewTable[domain.File](db, dialect, "settings") fileTable, err := orm.NewTable[domain.File](db, dialect, "settings")
if err != nil { if err != nil {
log.Fatalf("error creating lasebuche team table") log.Fatalf("error creating lasebuche team table")
} }
@@ -59,7 +59,6 @@ func (fr *TursoFileRepository) Upload(reader io.Reader, path string, name string
ContentType: contentType, ContentType: contentType,
VersionId: utility.GenID(), VersionId: utility.GenID(),
DateCreated: time.Now(), DateCreated: time.Now(),
DateUpdated: time.Now(),
Content: bytes, Content: bytes,
} }
@@ -0,0 +1,109 @@
package turso
import (
"database/sql"
"testing"
_ "github.com/mattn/go-sqlite3"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
func TestListChatsFilter(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
schema := `
create table chats (
id text not null primary key, team_id text not null, start_datetime numeric,
end_datetime numeric, agent_id text, user_id text, task_id text, title text,
fresh_title numeric, _date_created numeric not null, _date_updated numeric,
_version text not null);
create table user_teams (user_id text, team_id text);
insert into chats (id, team_id, user_id, agent_id, task_id, title, fresh_title, _date_created, _version)
values ('c1','t1','u1','','','Problème de timeout',0,'2026-01-01 10:00:00','v'),
('c2','t1','u1','','task-9','Discussion navigateur',0,'2026-01-02 10:00:00','v'),
('c3','t1','u1','','','Recette de cuisine 100%',0,'2026-01-03 10:00:00','v');
insert into user_teams (user_id, team_id) values ('u1','t1');
insert into chats (id, team_id, user_id, agent_id, task_id, title, fresh_title, _date_created, _version)
select 'bulk-'||(n+1), 't1','u1','','','Conversation '||(n+1), 0,
datetime('2026-01-01','+'||(n+10)||' minutes'), 'v'
from (with recursive cnt(n) as (select 0 union all select n+1 from cnt where n < 24) select n from cnt);
`
if _, err := db.Exec(schema); err != nil {
t.Fatal(err)
}
repo := NewTursoChatRepository(db)
// sans filtre, page 0 : 20 conversations (28 en base)
p0, err := repo.ListChats("u1", "t1", 0, "", "")
if err != nil {
t.Fatal(err)
}
if len(p0) != 20 {
t.Fatalf("page 0: expected 20, got %d", len(p0))
}
p1, err := repo.ListChats("u1", "t1", 1, "", "")
if err != nil {
t.Fatal(err)
}
if len(p1) != 8 {
t.Fatalf("page 1: expected 8, got %d", len(p1))
}
seen := map[string]bool{}
for _, c := range append(append([]*domain.Chat{}, p0...), p1...) {
if seen[c.ID] {
t.Fatalf("chat %s appears twice across pages", c.ID)
}
seen[c.ID] = true
}
p2, err := repo.ListChats("u1", "t1", 2, "", "")
if err != nil {
t.Fatal(err)
}
if len(p2) != 0 {
t.Fatalf("page 2: expected 0, got %d", len(p2))
}
// filtre simple
res, err := repo.ListChats("u1", "t1", 0, "timeout", "")
if err != nil {
t.Fatal(err)
}
if len(res) != 1 || res[0].ID != "c1" {
t.Fatalf("filter 'timeout': %+v", res)
}
// jokers LIKE neutralisés : '100%' ne doit rien matcher de spécial
res, err = repo.ListChats("u1", "t1", 0, "100%", "")
if err != nil {
t.Fatal(err)
}
if len(res) != 1 || res[0].ID != "c3" {
t.Fatalf("filter '100%%': %+v", res)
}
// pas de résultat
res, err = repo.ListChats("u1", "t1", 0, "inexistant", "")
if err != nil {
t.Fatal(err)
}
if len(res) != 0 {
t.Fatalf("expected 0, got %d", len(res))
}
// filtre par tâche
taskRes, err := repo.ListChats("u1", "t1", 0, "", "task-9")
if err != nil {
t.Fatal(err)
}
if len(taskRes) != 1 || taskRes[0].ID != "c2" {
t.Fatalf("filter task-9: expected [c2], got %v", taskRes)
}
_ = domain.Chat{}
}
@@ -9,6 +9,7 @@ drop table user_teams;
drop table channels; drop table channels;
drop table tools; drop table tools;
drop table tasks; drop table tasks;
drop table history; drop table chats;
drop table chat_contents;
drop table files; drop table files;
drop table share; drop table agents;
@@ -1,25 +1,16 @@
create table providers ( create table providers (
id text not null primary key, id text not null primary key,
team_id text not null, team_id text not null,
type text not null,
name text not null, name text not null,
key text, key text,
url text, url text,
models text,
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _version text not null
); );
create table models (
id text not null primary key,
team_id text not null,
provider_id text not null,
name text not null default '',
modelname text not null default '',
configuration text not null default '{}',
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
create table settings ( create table settings (
id text not null primary key, id text not null primary key,
@@ -52,7 +43,8 @@ create table users (
picture text not null default '', picture text not null default '',
enabled numeric default true, enabled numeric default true,
theme text default 'auto', theme text default 'auto',
lang text default 'en', lang text default 'fr',
timezone text default 'Europe/Paris',
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _version text not null
@@ -71,7 +63,7 @@ create table user_addresses (
create table teams ( create table teams (
id text not null primary key, id text not null primary key,
label text not null, label text not null,
default_model_id text, default_agent_id text,
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric , _date_updated numeric ,
_version text not null _version text not null
@@ -103,9 +95,8 @@ create table tools (
id text not null primary key, id text not null primary key,
team_id text not null, team_id text not null,
name text not null, name text not null,
type text not null,
enabled numeric not null default 1, enabled numeric not null default 1,
configuration text not null default '{}', configuration text not null default '',
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _version text not null
@@ -125,14 +116,31 @@ create table tasks (
_version text not null _version text not null
); );
create table history ( create table chats (
id text not null primary key, id text not null primary key,
task_id text not null, team_id text not null,
start_datetime numeric, start_datetime numeric not null,
end_datetime numeric, end_datetime numeric,
prompt text not null, agent_id text,
log text not null, user_id text,
response text not null, task_id text,
title text,
fresh_title numeric default 0,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
create table chat_messages (
id text not null primary key,
team_id text not null,
chat_id text not null,
content_datetime numeric,
role text not null,
content text,
agent_id text,
tool_call_id text,
tool_calls text,
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _version text not null
@@ -150,13 +158,23 @@ create table files (
_version text not null _version text not null
); );
create table share ( create table agents (
id text not null primary key, id text not null primary key,
object_id text not null, team_id text not null,
object_type text not null, name text not null default '',
team_id text not null, system_prompt text not null default '',
enabled numeric not null default 0, def_provider_id text not null default '',
_date_created numeric not null default current_timestamp, def_model_id text not null default '',
_date_updated numeric, tools_policy text not null default '',
_version text not null sub_agents numeric not null default 0,
loop_strategy text not null default '',
max_iterations numeric not null default 1,
stopping_criteria text not null default '',
-- kb text not null default '',
-- tools text not null default '',
-- skills text not null default '',
-- channels text not null default '',
_date_created numeric not null default current_timestamp,
_date_updated numeric ,
_version text not null
); );
@@ -0,0 +1 @@
alter table tasks drop column repeat;
@@ -0,0 +1 @@
alter table tasks add column repeat integer not null default 1;
@@ -0,0 +1 @@
alter table tasks drop column last_datetime;
@@ -0,0 +1 @@
alter table tasks add column last_datetime numeric;
@@ -0,0 +1,34 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoOIDCProviderRepository struct {
db *sql.DB
OIDCTable orm.Table[domain.OIDCProvider]
}
func NewTursoOIDCProviderRepository(db *sql.DB) *TursoOIDCProviderRepository {
dialect := orm.NewSqliteDialect()
oidcTable, err := orm.NewTable[domain.OIDCProvider](db, dialect, "oidc")
if err != nil {
log.Fatalf("error creating oidc provider table")
}
return &TursoOIDCProviderRepository{
db: db,
OIDCTable: oidcTable,
}
}
func (or *TursoOIDCProviderRepository) ListOIDCProviders() ([]*domain.OIDCProvider, error) {
return or.OIDCTable.Select(orm.WithOrder("label asc"))
}
func (or *TursoOIDCProviderRepository) FindOIDCProvider(id string) (*domain.OIDCProvider, error) {
return or.OIDCTable.Get(id)
}
@@ -0,0 +1,64 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoProviderRepository struct {
db *sql.DB
providerTable orm.Table[domain.Provider]
}
func NewTursoProviderRepository(db *sql.DB) *TursoProviderRepository {
dialect := orm.NewSqliteDialect()
providerTable, err := orm.NewTable[domain.Provider](db, dialect, "providers")
if err != nil {
log.Fatalf("error creating lasebuche provider table")
}
return &TursoProviderRepository{
db: db,
providerTable: providerTable,
}
}
func (r *TursoProviderRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
return r.providerTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
orm.WithOrder("name"),
)
}
func (r *TursoProviderRepository) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) {
return r.providerTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoProviderRepository) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
return r.providerTable.Insert(provider)
}
func (r *TursoProviderRepository) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
return r.providerTable.Update(provider)
}
func (r *TursoProviderRepository) DeleteProvider(userID string, teamID string, id string) error {
return r.providerTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
@@ -4,18 +4,18 @@ import (
"database/sql" "database/sql"
"log" "log"
"gitea.trankilou.fr/fabien/lasebuche" "trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
) )
type TursoSettingsRepository struct { type TursoSettingsRepository struct {
db *sql.DB db *sql.DB
SettingsTable lasebuche.Table[domain.Settings] SettingsTable orm.Table[domain.Settings]
} }
func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository { func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository {
dialect := lasebuche.NewSqliteDialect() dialect := orm.NewSqliteDialect()
settingsTable, err := lasebuche.NewTable[domain.Settings](db, dialect, "settings") settingsTable, err := orm.NewTable[domain.Settings](db, dialect, "settings")
if err != nil { if err != nil {
log.Fatalf("error creating lasebuche team table") log.Fatalf("error creating lasebuche team table")
} }
@@ -0,0 +1,85 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoTaskRepository struct {
db *sql.DB
taskTable orm.Table[domain.Task]
}
func NewTursoTaskRepository(db *sql.DB) *TursoTaskRepository {
dialect := orm.NewSqliteDialect()
taskTable, err := orm.NewTable[domain.Task](db, dialect, "tasks")
if err != nil {
log.Fatalf("error creating task table")
}
return &TursoTaskRepository{
db: db,
taskTable: taskTable,
}
}
func (r *TursoTaskRepository) ListTasks(userID string, teamID string) ([]*domain.Task, error) {
return r.taskTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
orm.WithOrder("_date_created desc"),
)
}
func (r *TursoTaskRepository) ListActiveTasks() ([]*domain.Task, error) {
return r.taskTable.Select(
orm.WithWhere("status=$1", domain.TaskStatusActive),
)
}
func (r *TursoTaskRepository) GetTaskByID(userID string, teamID string, id string) (*domain.Task, error) {
return r.taskTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoTaskRepository) CreateTask(task *domain.Task) (*domain.Task, error) {
return r.taskTable.Insert(task)
}
func (r *TursoTaskRepository) UpdateTask(task *domain.Task) (*domain.Task, error) {
return r.taskTable.Update(task)
}
func (r *TursoTaskRepository) DeleteTask(userID string, teamID string, id string) error {
return r.taskTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
// FirstUserOfTeam retourne le premier utilisateur d'une équipe. Les tâches
// planifiées ne portent pas d'utilisateur : c'est lui qui porte la conversation
// générée lors de l'exécution.
func (r *TursoTaskRepository) FirstUserOfTeam(teamID string) (string, error) {
var userID string
err := r.db.QueryRow(
"select user_id from user_teams where team_id=$1 order by _date_created asc limit 1",
teamID,
).Scan(&userID)
if err != nil {
return "", err
}
return userID, nil
}
@@ -0,0 +1,72 @@
package turso
import (
"database/sql"
"log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoToolRepository struct {
db *sql.DB
ToolTable orm.Table[domain.Tool]
}
func NewTursoToolRepository(db *sql.DB) *TursoToolRepository {
dialect := orm.NewSqliteDialect()
ToolTable, err := orm.NewTable[domain.Tool](db, dialect, "tools")
if err != nil {
log.Fatalf("error creating lasebuche Tool table")
}
return &TursoToolRepository{
db: db,
ToolTable: ToolTable,
}
}
func (r *TursoToolRepository) ListTools(userID string, teamID string) ([]*domain.Tool, error) {
return r.ToolTable.Select(
orm.WithWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
),
)
}
func (r *TursoToolRepository) GetToolByName(userID string, teamID string, name string) (*domain.Tool, error) {
return r.ToolTable.SelectOne(
"name=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
name,
teamID,
userID,
)
}
func (r *TursoToolRepository) GetToolByID(userID string, teamID string, id string) (*domain.Tool, error) {
return r.ToolTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoToolRepository) CreateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
return r.ToolTable.Insert(Tool)
}
func (r *TursoToolRepository) UpdateTool(userID string, Tool *domain.Tool) (*domain.Tool, error) {
return r.ToolTable.Update(Tool)
}
func (r *TursoToolRepository) DeleteTool(userID string, teamID string, name string) error {
return r.ToolTable.DeleteWhere(
"name=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
name,
teamID,
userID,
)
}
@@ -4,34 +4,33 @@ import (
"database/sql" "database/sql"
"log" "log"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/orm"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"gitea.trankilou.fr/fabien/lasebuche"
) )
type TursoUserRepository struct { type TursoUserRepository struct {
DB *sql.DB DB *sql.DB
UserTable lasebuche.Table[domain.User] UserTable orm.Table[domain.User]
TeamTable lasebuche.Table[domain.Team] TeamTable orm.Table[domain.Team]
UserTeamTable lasebuche.Table[domain.UserTeam] UserTeamTable orm.Table[domain.UserTeam]
AddressTable lasebuche.Table[domain.UserAddress] AddressTable orm.Table[domain.UserAddress]
} }
func NewTursoUserRepository(db *sql.DB) *TursoUserRepository { func NewTursoUserRepository(db *sql.DB) *TursoUserRepository {
dialect := lasebuche.NewSqliteDialect() dialect := orm.NewSqliteDialect()
userTable, err := lasebuche.NewTable[domain.User](db, dialect, "users") userTable, err := orm.NewTable[domain.User](db, dialect, "users")
if err != nil { if err != nil {
log.Fatalf("error creating lasebuche user table") log.Fatalf("error creating lasebuche user table")
} }
teamTable, err := lasebuche.NewTable[domain.Team](db, dialect, "teams") teamTable, err := orm.NewTable[domain.Team](db, dialect, "teams")
if err != nil { if err != nil {
log.Fatalf("error creating lasebuche team table") log.Fatalf("error creating lasebuche team table")
} }
userTeamTable, err := lasebuche.NewTable[domain.UserTeam](db, dialect, "user_teams") userTeamTable, err := orm.NewTable[domain.UserTeam](db, dialect, "user_teams")
if err != nil { if err != nil {
log.Fatalf("error creating lasebuche team table") log.Fatalf("error creating lasebuche team table")
} }
userAddressTable, err := lasebuche.NewTable[domain.UserAddress](db, dialect, "user_addresses") userAddressTable, err := orm.NewTable[domain.UserAddress](db, dialect, "user_addresses")
if err != nil { if err != nil {
log.Fatalf("error creating lasebuche team table") log.Fatalf("error creating lasebuche team table")
} }
@@ -53,7 +52,7 @@ func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, erro
} }
func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) { func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) {
return ur.UserTable.SelectWhere("") return ur.UserTable.Select()
} }
func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) { func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) {
@@ -69,11 +68,14 @@ func (ur *TursoUserRepository) DeleteUser(id string) error {
} }
func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.Team, error) { func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.Team, error) {
return ur.TeamTable.SelectOne("id=$1 and user_id=$2", teamid, userid) return ur.TeamTable.SelectOne("id=$1 and id in (select team_id from user_teams where user_id=$2)", teamid, userid)
} }
func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) { func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) {
return ur.TeamTable.SelectWhere("user_id=$1", userid) return ur.TeamTable.Select(
orm.WithWhere("id in (select team_id from user_teams where user_id=$1)", userid),
orm.WithOrder("label asc"),
)
} }
func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) { func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) {
@@ -108,7 +110,9 @@ func (ur *TursoUserRepository) FindUserTeam(userid string, teamid string) (*doma
} }
func (ur *TursoUserRepository) ListUserTeams(userid string) ([]*domain.UserTeam, error) { func (ur *TursoUserRepository) ListUserTeams(userid string) ([]*domain.UserTeam, error) {
return ur.UserTeamTable.SelectWhere("user_id=$1", userid) return ur.UserTeamTable.Select(
orm.WithWhere("user_id=$1", userid),
)
} }
func (ur *TursoUserRepository) CreateUserTeam(userid string, team *domain.UserTeam) (*domain.UserTeam, error) { func (ur *TursoUserRepository) CreateUserTeam(userid string, team *domain.UserTeam) (*domain.UserTeam, error) {
@@ -123,21 +127,23 @@ func (ur *TursoUserRepository) DeleteUserTeam(userid string, teamid string) erro
return ur.UserTeamTable.Delete(teamid) return ur.UserTeamTable.Delete(teamid)
} }
func (ur *TursoUserRepository) PopulateUserWithTeams(user *domain.User) (*domain.UserWithTeams, error) {
return nil, nil
}
func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) { func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) {
return ur.AddressTable.SelectWhere("user_id=$1", id) return ur.AddressTable.Select(
orm.WithWhere("user_id=$1", id),
orm.WithOrder("type asc"),
)
} }
func (ur *TursoUserRepository) CreateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) { func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) {
addr.UserID = userid return ur.AddressTable.Get(addrID)
}
func (ur *TursoUserRepository) CreateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Insert(addr) return ur.AddressTable.Insert(addr)
} }
func (ur *TursoUserRepository) UpdateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) { func (ur *TursoUserRepository) UpdateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Update(addr) return ur.AddressTable.Update(addr)
} }
func (ur *TursoUserRepository) DeleteUserAddress(userid string, addrID string) error { func (ur *TursoUserRepository) DeleteUserAddress(addrID string) error {
return ur.AddressTable.DeleteWhere("id=$1 and user_id=$2", addrID, userid) return ur.AddressTable.DeleteWhere("id=$1", addrID)
} }
+253
View File
@@ -0,0 +1,253 @@
package llm
import (
"context"
"fmt"
"log/slog"
anyllm "github.com/mozilla-ai/any-llm-go"
"github.com/mozilla-ai/any-llm-go/providers"
"github.com/mozilla-ai/any-llm-go/providers/anthropic"
"github.com/mozilla-ai/any-llm-go/providers/ollama"
"github.com/mozilla-ai/any-llm-go/providers/openai"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
var providerTypes = []domain.Item{
{Value: "anthropic", Label: "Anthropic"},
{Value: "openai", Label: "OpenAI"},
{Value: "ollama", Label: "Ollama"},
{Value: "openaicomp", Label: "OpenAI compatible"},
{Value: "openrouter", Label: "Openrouter"},
}
type AnyLLMEngine struct {
}
func NewAnyLLMEngine() *AnyLLMEngine {
return &AnyLLMEngine{}
}
func (e *AnyLLMEngine) ListProviderTypes() []domain.Item {
return providerTypes
}
func providerFactory(provider *domain.Provider) (anyllm.Provider, error) {
switch provider.Type {
case "ollama":
return ollama.New(
anyllm.WithBaseURL(provider.URL),
)
case "openai":
return openai.New(
anyllm.WithAPIKey(provider.APIKey),
)
case "openaicomp":
return openai.New(
anyllm.WithBaseURL(provider.URL),
anyllm.WithAPIKey(provider.APIKey),
)
case "anthropic":
return anthropic.New(
anyllm.WithAPIKey(provider.APIKey),
)
}
return nil, fmt.Errorf("unknown provider type: %s", provider.Type)
}
func (e *AnyLLMEngine) ListModelsFromProvider(ctx context.Context, provider *domain.Provider) ([]string, error) {
models := make([]string, 0)
prov, err := providerFactory(provider)
if err != nil {
return nil, err
}
if lister, ok := prov.(anyllm.ModelLister); ok {
response, err := lister.ListModels(ctx)
if err != nil {
return nil, err
}
for _, m := range response.Data {
models = append(models, m.ID)
}
} else {
models = append(models, "Default")
}
return models, nil
}
func (e *AnyLLMEngine) Stream(
ctx context.Context,
provider *domain.Provider,
modelID string,
params *domain.LLMParams,
messages []*domain.Message,
) *domain.Message {
p, err := providerFactory(provider)
if err != nil {
return &domain.Message{
Role: string(domain.RoleAssistant),
Content: err.Error(),
}
}
anyllmMessages := make([]anyllm.Message, 0)
for _, m := range messages {
anyllmMessages = append(anyllmMessages, anyllm.Message{
Role: m.Role,
Content: m.Content,
})
}
if params.ReasoningEffort == "" {
params.ReasoningEffort = "none"
}
tools := make([]providers.Tool, len(params.Tools))
for i, t := range params.Tools {
tools[i] = toolConvert(t)
}
toolCalling := false
toolCalls := make([]domain.ToolCall, 0)
chunkChan, errChan := p.CompletionStream(ctx, anyllm.CompletionParams{
Model: modelID,
Messages: anyllmMessages,
Stream: true,
ReasoningEffort: providers.ReasoningEffort(params.ReasoningEffort),
Tools: tools,
})
fullContent := ""
for chunk := range chunkChan {
if len(chunk.Choices) > 0 {
content := chunk.Choices[0].Delta.Content
reasoning := chunk.Choices[0].Delta.Reasoning
if params.OnChunk != nil {
if content != "" {
fullContent += content
params.OnChunk(&domain.Chunk{
Done: false,
Role: anyllm.RoleAssistant,
Content: content,
Reasoning: false,
})
} else if reasoning != nil {
params.OnChunk(&domain.Chunk{
Done: false,
Role: anyllm.RoleAssistant,
Content: reasoning.Content,
Reasoning: true,
})
}
}
for _, tc := range chunk.Choices[0].Delta.ToolCalls {
toolCalling = true
slog.Info("Model is calling tool", "tool", tc.Function.Name, "arguments", tc.Function.Arguments)
extra := make(map[string]map[string]any)
for k, v := range tc.Extra {
m := map[string]any(v)
extra[k] = m
}
toolCalls = append(toolCalls, domain.ToolCall{
ID: tc.ID,
Type: tc.Type,
Function: domain.ToolCallFunction{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
Extra: extra,
})
if params.OnChunk != nil {
params.OnChunk(&domain.Chunk{
Done: false,
ToolName: tc.Function.Name,
Role: anyllm.RoleAssistant,
Content: tc.Function.Arguments,
Reasoning: false,
})
}
}
}
}
if !toolCalling {
if params.OnChunk != nil {
params.OnChunk(&domain.Chunk{
Done: true,
Role: anyllm.RoleAssistant,
Content: "",
})
}
}
if err := <-errChan; err != nil {
return &domain.Message{
Role: string(domain.RoleAssistant),
Content: err.Error(),
}
}
return &domain.Message{
Role: string(domain.RoleAssistant),
Content: fullContent,
ToolCalls: toolCalls,
}
}
func (e *AnyLLMEngine) Generate(
ctx context.Context,
provider *domain.Provider,
modelID string,
messages []*domain.Message,
) (string, error) {
p, err := providerFactory(provider)
if err != nil {
return "", err
}
anyllmMessages := make([]anyllm.Message, 0)
for _, m := range messages {
anyllmMessages = append(anyllmMessages, anyllm.Message{
Role: m.Role,
Content: m.Content,
})
}
response, err := p.Completion(ctx, anyllm.CompletionParams{
Model: modelID,
Messages: anyllmMessages,
Stream: false,
})
if err != nil {
return "", err
}
return response.Choices[0].Message.Content.(string), nil
}
func toolConvert(domainTool *domain.ToolDefinition) providers.Tool {
return providers.Tool{
Type: domainTool.Type,
Function: providers.Function{
Name: domainTool.Function.Name,
Description: domainTool.Function.Description,
Parameters: domainTool.Function.Parameters,
},
}
}
+34 -2
View File
@@ -75,6 +75,38 @@ func (tm *JwtTokenManager) GenerateRefreshToken(userID string) (string, time.Tim
return signed, expiresAt, err return signed, expiresAt, err
} }
// GenerateStateToken crée un JWT court (10 min) utilisé comme paramètre state
// du flux OIDC, porteur d'une donnée arbitraire (ex: l'id du fournisseur).
func (tm *JwtTokenManager) GenerateStateToken(data string) (string, error) {
claims := jwt.RegisteredClaims{
Subject: data,
Issuer: tm.issuer,
ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(tm.secret)
}
// ParseStateToken vérifie un state généré par GenerateStateToken et retourne
// la donnée qu'il transporte.
func (tm *JwtTokenManager) ParseStateToken(state string) (string, error) {
claims := &jwt.RegisteredClaims{}
token, err := jwt.ParseWithClaims(state, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("méthode de signature inattendue")
}
return tm.secret, nil
})
if err != nil {
return "", err
}
if !token.Valid {
return "", errors.New("token invalide")
}
return claims.Subject, nil
}
// ParseAndValidate décode et vérifie la signature + l'expiration d'un JWT. // ParseAndValidate décode et vérifie la signature + l'expiration d'un JWT.
func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, error) { func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, error) {
claims := &Claims{} claims := &Claims{}
@@ -117,8 +149,8 @@ func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFu
// Rend les infos disponibles aux handlers suivants via c.Get(...) // Rend les infos disponibles aux handlers suivants via c.Get(...)
c.Set(auth.ContextUserIDKey, claims.ID) c.Set(auth.ContextUserIDKey, claims.ID)
c.Set(auth.ContextEmailKey, claims.Email) //c.Set(auth.ContextEmailKey, claims.Email)
c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname) //c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname)
return next(c) return next(c)
} }
@@ -0,0 +1,314 @@
package browser
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/stealth"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type BrowserConfig struct {
// Headless est un pointeur pour distinguer "non renseigné" (défaut : true) de "explicitement false".
Headless *bool `yaml:"headless"`
TimeoutSeconds int `yaml:"timeoutSeconds"`
}
// BrowserTool maintains a persistent browser session across LLM tool calls.
type BrowserTool struct{}
var (
browserInstance *rod.Browser
currentPage *rod.Page
browserMu sync.Mutex
launchedHeadless bool
)
func (t *BrowserTool) Name() string {
return "browser"
}
func (t *BrowserTool) Description() string {
return "Control a web browser to navigate pages and interact with content. " +
"Maintains session state across calls (cookies, current page). " +
"Actions: 'navigate' (go to URL), 'click' (click element by CSS selector), " +
"'fill' (type into an input field), 'content' (get current page text), " +
"'links' (list all links on current page)."
}
func (t *BrowserTool) Params() []domain.ToolParam {
return []domain.ToolParam{
{JsonName: "headless", Type: "bool", Mandatory: true, Description: "Headless"},
{JsonName: "timeoutSeconds", Type: "int", Mandatory: true, Description: "Timeout en secondes"},
}
}
func (t *BrowserTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"navigate", "click", "fill", "content", "links"},
"description": "The browser action to perform.",
},
"url": map[string]any{
"type": "string",
"description": "URL to navigate to. Required for action='navigate'.",
},
"selector": map[string]any{
"type": "string",
"description": "CSS selector of the element to interact with. Required for 'click' and 'fill'.",
},
"value": map[string]any{
"type": "string",
"description": "Text value to type into the field. Required for action='fill'.",
},
},
"required": []string{"action"},
},
},
}
}
func (t *BrowserTool) Execute(_ context.Context, input []byte, configuration string) ([]byte, error) {
slog.Debug("[browser]", "input", string(input))
var params struct {
Action string `json:"action"`
URL string `json:"url"`
Selector string `json:"selector"`
Value string `json:"value"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
var toolConfig BrowserConfig
err := json.Unmarshal([]byte(configuration), &toolConfig)
if err != nil {
return nil, err
}
timeout := time.Duration(toolConfig.TimeoutSeconds) * time.Second
// Headless est nil quand la configuration ne le précise pas : défaut à true.
headless := true
if toolConfig.Headless != nil {
headless = *toolConfig.Headless
}
if timeout <= 0 {
timeout = 60 * time.Second
}
browserMu.Lock()
defer browserMu.Unlock()
if err := ensureBrowser(headless); err != nil {
return nil, fmt.Errorf("browser init failed: %w", err)
}
switch params.Action {
case "navigate":
if params.URL == "" {
return nil, fmt.Errorf("'url' is required for action 'navigate'")
}
return t.navigate(params.URL, timeout)
case "click":
if params.Selector == "" {
return nil, fmt.Errorf("'selector' is required for action 'click'")
}
return t.click(params.Selector, timeout)
case "fill":
if params.Selector == "" || params.Value == "" {
return nil, fmt.Errorf("'selector' and 'value' are required for action 'fill'")
}
return t.fill(params.Selector, params.Value, timeout)
case "content":
return t.pageContent(timeout)
case "links":
return t.pageLinks(timeout)
default:
return nil, fmt.Errorf("unknown action %q", params.Action)
}
}
func browserProfileDir() string {
if dir, err := os.UserCacheDir(); err == nil {
return filepath.Join(dir, "lassistanoque", "browser-profile")
}
return filepath.Join(os.TempDir(), "lassistanoque-browser-profile")
}
// ensureBrowser lance le navigateur au premier appel et le relance si le mode
// headless a changé dans la configuration.
func ensureBrowser(headless bool) error {
if browserInstance != nil {
if headless == launchedHeadless {
return nil
}
_ = browserInstance.Close()
browserInstance = nil
currentPage = nil
}
if err := initBrowser(headless); err != nil {
return err
}
launchedHeadless = headless
return nil
}
func initBrowser(headless bool) error {
path, _ := launcher.LookPath()
controlURL, err := launcher.New().
Bin(path).
Headless(headless).
UserDataDir(browserProfileDir()).
Delete("enable-automation").
Launch()
if err != nil {
return fmt.Errorf("failed to launch browser: %w", err)
}
b := rod.New().ControlURL(controlURL)
if err := b.Connect(); err != nil {
return fmt.Errorf("failed to connect to browser: %w", err)
}
browserInstance = b
return nil
}
func ensurePage(timeout time.Duration) error {
if currentPage == nil {
page, err := stealth.Page(browserInstance.Timeout(timeout))
if err != nil {
return fmt.Errorf("failed to open page: %w", err)
}
currentPage = page
}
return nil
}
func (t *BrowserTool) navigate(rawURL string, timeout time.Duration) ([]byte, error) {
if err := ensurePage(timeout); err != nil {
return nil, err
}
if err := currentPage.Timeout(timeout).Navigate(rawURL); err != nil {
return nil, fmt.Errorf("navigation failed: %w", err)
}
if err := currentPage.Timeout(timeout).WaitLoad(); err != nil {
return nil, fmt.Errorf("page load failed: %w", err)
}
return t.pageContent(timeout)
}
func (t *BrowserTool) click(selector string, timeout time.Duration) ([]byte, error) {
if err := ensurePage(timeout); err != nil {
return nil, err
}
el, err := currentPage.Timeout(timeout).Element(selector)
if err != nil {
return nil, fmt.Errorf("element %q not found: %w", selector, err)
}
if err := el.Click(proto.InputMouseButtonLeft, 1); err != nil {
return nil, fmt.Errorf("click failed: %w", err)
}
if err := currentPage.WaitIdle(timeout); err != nil {
return nil, fmt.Errorf("wait after click failed: %w", err)
}
return t.pageContent(timeout)
}
func (t *BrowserTool) fill(selector, value string, timeout time.Duration) ([]byte, error) {
if err := ensurePage(timeout); err != nil {
return nil, err
}
el, err := currentPage.Timeout(timeout).Element(selector)
if err != nil {
return nil, fmt.Errorf("element %q not found: %w", selector, err)
}
if err := el.SelectAllText(); err != nil {
return nil, fmt.Errorf("failed to select text: %w", err)
}
if err := el.Input(value); err != nil {
return nil, fmt.Errorf("fill failed: %w", err)
}
return t.pageContent(timeout)
}
func (t *BrowserTool) pageContent(timeout time.Duration) ([]byte, error) {
if currentPage == nil {
return []byte("No page loaded."), nil
}
info, err := currentPage.Info()
if err != nil {
return nil, fmt.Errorf("failed to get page info: %w", err)
}
titleRes, err := currentPage.Eval("() => document.title")
if err != nil {
return nil, fmt.Errorf("failed to get page title: %w", err)
}
title := titleRes.Value.String()
body, err := currentPage.Timeout(timeout).Element("body")
if err != nil {
return nil, fmt.Errorf("failed to get page body: %w", err)
}
text, err := body.Text()
if err != nil {
return nil, fmt.Errorf("failed to extract page text: %w", err)
}
var sb strings.Builder
fmt.Fprintf(&sb, "Page: %s\nURL: %s\n\n%s", title, info.URL, text)
return []byte(sb.String()), nil
}
func (t *BrowserTool) pageLinks(timeout time.Duration) ([]byte, error) {
if currentPage == nil {
return []byte("No page loaded."), nil
}
elements, err := currentPage.Timeout(timeout).Elements("a[href]")
if err != nil {
return nil, fmt.Errorf("failed to get links: %w", err)
}
var sb strings.Builder
fmt.Fprintf(&sb, "Links on current page (%d):\n\n", len(elements))
for i, el := range elements {
href, _ := el.Attribute("href")
text, _ := el.Text()
if href == nil {
continue
}
label := strings.TrimSpace(text)
if label == "" {
label = "(no text)"
}
fmt.Fprintf(&sb, "%d. %s — %s\n", i+1, label, *href)
}
return []byte(sb.String()), nil
}
@@ -0,0 +1,186 @@
package scheduler
import (
"context"
"encoding/json"
"fmt"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/task"
)
// SchedulerTool permet à l'agent de planifier des tâches pour l'utilisateur :
// exécution d'un prompt selon une spécification cron, avec ou sans répétition.
type SchedulerTool struct {
taskService *task.Service
}
func New(taskService *task.Service) *SchedulerTool {
return &SchedulerTool{taskService: taskService}
}
func (t *SchedulerTool) Name() string {
return "scheduler"
}
func (t *SchedulerTool) Description() string {
return "Schedule tasks that run automatically at specified times, with or without repetition. " +
"Use this tool whenever the user asks to do something later or on a recurring basis " +
"(e.g. 'every day at 8am', 'every Monday at 9am', 'tomorrow at 6pm', 'every hour'). " +
"Actions: 'create' (schedule a new task), 'list' (list scheduled tasks), " +
"'update' (modify a task's title, prompt, cron or repetition), " +
"'delete' (delete a task by its id)."
}
func (t *SchedulerTool) Params() []domain.ToolParam {
return []domain.ToolParam{}
}
func (t *SchedulerTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"action": map[string]any{
"type": "string",
"enum": []string{"create", "list", "update", "delete"},
"description": "The scheduling action to perform.",
},
"title": map[string]any{
"type": "string",
"description": "Short task title, e.g. 'Météo du matin'. Required for 'create'.",
},
"prompt": map[string]any{
"type": "string",
"description": "Self-contained prompt to execute at the scheduled time. " +
"It will run in a fresh conversation with no memory of this one: " +
"include all needed context. Required for 'create'.",
},
"cron": map[string]any{
"type": "string",
"description": "Cron spec, 5 fields 'minute hour day-of-month month day-of-week' " +
"(e.g. '0 8 * * *' every day at 08:00, '0 9 * * 1' every Monday at 09:00, " +
"'30 18 1 * *' on the 1st at 18:30) or a descriptor like '@daily', '@hourly', '@weekly'. " +
"Required for 'create'.",
},
"repeat": map[string]any{
"type": "boolean",
"description": "true: the task repeats on schedule. " +
"false: it runs only once, at the first time matching the cron spec, then is marked done. " +
"Default true. Use false for one-shot requests like 'tomorrow at 8am'.",
},
"id": map[string]any{
"type": "string",
"description": "Task id, as returned by 'list'. Required for 'update' and 'delete'.",
},
},
"required": []string{"action"},
},
},
}
}
func (t *SchedulerTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
var params struct {
Action string `json:"action"`
Title string `json:"title"`
Prompt string `json:"prompt"`
Cron string `json:"cron"`
Repeat *bool `json:"repeat"`
ID string `json:"id"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
userID, ok := domain.UserIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("scheduler tool: missing user context")
}
teamID, ok := domain.TeamIDFromContext(ctx)
if !ok {
return nil, fmt.Errorf("scheduler tool: missing team context")
}
modelID, _ := domain.ModelIDFromContext(ctx)
switch params.Action {
case "create":
repeat := true
if params.Repeat != nil {
repeat = *params.Repeat
}
taskModel, err := t.taskService.CreateTask(userID, teamID, &domain.Task{
ModelID: modelID,
Label: params.Title,
Prompt: params.Prompt,
Cron: params.Cron,
Repeat: repeat,
})
if err != nil {
return nil, err
}
result, err := json.Marshal(taskModel)
if err != nil {
return nil, err
}
return result, nil
case "list":
tasks, err := t.taskService.ListTasks(userID, teamID)
if err != nil {
return nil, err
}
result, err := json.Marshal(tasks)
if err != nil {
return nil, err
}
return result, nil
case "update":
if params.ID == "" {
return nil, fmt.Errorf("'id' is required for action 'update'")
}
current, err := t.taskService.GetTask(userID, teamID, params.ID)
if err != nil {
return nil, err
}
// mise à jour partielle : seuls les champs fournis changent
if params.Title != "" {
current.Label = params.Title
}
if params.Prompt != "" {
current.Prompt = params.Prompt
}
if params.Cron != "" {
current.Cron = params.Cron
}
if params.Repeat != nil {
current.Repeat = *params.Repeat
}
updated, err := t.taskService.UpdateTask(userID, teamID, current)
if err != nil {
return nil, err
}
result, err := json.Marshal(updated)
if err != nil {
return nil, err
}
return result, nil
case "delete":
if params.ID == "" {
return nil, fmt.Errorf("'id' is required for action 'delete'")
}
if err := t.taskService.DeleteTask(userID, teamID, params.ID); err != nil {
return nil, err
}
return []byte("task deleted"), nil
default:
return nil, fmt.Errorf("unknown action %q", params.Action)
}
}
@@ -0,0 +1,151 @@
package searxng
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type SearxngConfig struct {
URL string `json:"url"`
}
type SearxngTool struct {
}
func (t *SearxngTool) Name() string {
return "searxng"
}
func (t *SearxngTool) Description() string {
return "Search the web using a SearXNG instance. Returns a list of relevant results with title, URL, and snippet."
}
func (t *SearxngTool) Params() []domain.ToolParam {
return []domain.ToolParam{
{JsonName: "url", Type: "string", Mandatory: true, Description: "Searxng URL"},
}
}
func (t *SearxngTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "The search query",
},
"categories": map[string]any{
"type": "string",
"description": "Comma-separated list of categories: general, news, images, science, it, social_media. Defaults to 'general'.",
},
"language": map[string]any{
"type": "string",
"description": "Language code for results, e.g. 'fr', 'en'. Defaults to 'all'.",
},
},
"required": []string{"query"},
},
},
}
}
type searxngResponse struct {
Results []searxngResult `json:"results"`
}
type searxngResult struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
Engine string `json:"engine"`
}
func (t *SearxngTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
slog.Debug("[searxng]", "input", string(input))
var params struct {
Query string `json:"query"`
Categories string `json:"categories"`
Language string `json:"language"`
}
if err := json.Unmarshal(input, &params); err != nil {
return nil, fmt.Errorf("invalid parameters: %w", err)
}
var toolCfg SearxngConfig
err := json.Unmarshal([]byte(configuration), &toolCfg)
if err != nil {
return nil, fmt.Errorf("searxng tool not configured: %w", err)
}
if toolCfg.URL == "" {
return nil, fmt.Errorf("searxng tool config missing 'url' field")
}
baseURL := toolCfg.URL
categories := "general"
if params.Categories != "" {
categories = params.Categories
}
language := "all"
if params.Language != "" {
language = params.Language
}
reqURL := fmt.Sprintf("%s/search?q=%s&format=json&categories=%s&language=%s",
strings.TrimRight(baseURL, "/"),
url.QueryEscape(params.Query),
url.QueryEscape(categories),
url.QueryEscape(language),
)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to build request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("searxng request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("searxng returned status %d", resp.StatusCode)
}
var searchResp searxngResponse
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(searchResp.Results) == 0 {
return []byte("No results found."), nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "Search results for \"%s\":\n\n", params.Query)
for i, r := range searchResp.Results {
fmt.Fprintf(&sb, "%d. %s\n", i+1, r.Title)
fmt.Fprintf(&sb, " URL: %s\n", r.URL)
if r.Content != "" {
fmt.Fprintf(&sb, " %s\n", r.Content)
}
fmt.Fprintf(&sb, "\n")
}
return []byte(sb.String()), nil
}
@@ -0,0 +1,187 @@
package weather
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type WeatherTool struct {
}
type WeatherConfig struct {
}
func (t *WeatherTool) Name() string {
return "weather"
}
func (t *WeatherTool) Description() string {
return "Get the current weather for a given location"
}
func (t *WeatherTool) Params() []domain.ToolParam {
return []domain.ToolParam{}
}
func (t *WeatherTool) Definition(ctx context.Context) *domain.ToolDefinition {
return &domain.ToolDefinition{
Type: "function",
Function: &domain.ToolFunction{
Name: t.Name(),
Description: t.Description(),
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{
"type": "string",
"description": "The city name, e.g., 'Paris' or 'New York'",
},
},
"required": []string{"location"},
},
},
}
}
func (t *WeatherTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) {
var params struct {
Location string `json:"location"`
}
err := json.Unmarshal(input, &params)
if err != nil {
return nil, err
}
resp, err := http.Get("https://geocoding-api.open-meteo.com/v1/search?name=" + url.QueryEscape(params.Location))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var location struct {
Results []struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
} `json:"results"`
}
err = json.NewDecoder(resp.Body).Decode(&location)
if err != nil {
return nil, err
}
if len(location.Results) == 0 {
return nil, fmt.Errorf("location not found : %s", input)
}
lon := location.Results[0].Longitude
lat := location.Results[0].Latitude
weatherResp, err := http.Get(fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f&current_weather=true&daily=weather_code,temperature_2m_max,temperature_2m_min,rain_sum,wind_speed_10m_max&hourly=temperature_2m&forecast_days=15", lat, lon))
if err != nil {
return nil, err
}
defer weatherResp.Body.Close()
var weatherData struct {
CurrentWeatherUnits struct {
Temperature string `json:"temperature"`
WindSpeed string `json:"windspeed"`
} `json:"current_weather_units"`
CurrentWeather struct {
Time string `json:"time"`
Temperature float64 `json:"temperature"`
WeatherCode int `json:"weathercode"`
} `json:"current_weather"`
DailyUnits struct {
TemperatureMax string `json:"temperature_2m_max"`
TemperatureMin string `json:"temperature_2m_min"`
Rain string `json:"rain_sum"`
WindSpeed string `json:"wind_speed_10m_max"`
} `json:"daily_units"`
Daily struct {
Time []string `json:"time"`
TemperatureMax []float64 `json:"temperature_2m_max"`
TemperatureMin []float64 `json:"temperature_2m_min"`
Rain []float64 `json:"rain_sum"`
WindSpeed []float64 `json:"wind_speed_10m_max"`
} `json:"daily"`
}
err = json.NewDecoder(weatherResp.Body).Decode(&weatherData)
if err != nil {
return nil, err
}
var builder strings.Builder
fmt.Fprintf(&builder, "Current weather in %s\n", params.Location)
fmt.Fprintf(&builder, "- Time: %s\n", weatherData.CurrentWeather.Time)
fmt.Fprintf(&builder, "- Temperature: %.2f %s\n", weatherData.CurrentWeather.Temperature, weatherData.CurrentWeatherUnits.Temperature)
fmt.Fprintf(&builder, "- Weather code: %d - %s\n", weatherData.CurrentWeather.WeatherCode, wmoCodeToDescription(weatherData.CurrentWeather.WeatherCode))
if len(weatherData.Daily.WindSpeed) > 0 {
fmt.Fprintf(&builder, "- Wind speed: %.2f %s\n", weatherData.Daily.WindSpeed[0], weatherData.DailyUnits.WindSpeed)
}
if len(weatherData.Daily.TemperatureMax) > 0 {
fmt.Fprintf(&builder, "- Max temperature: %.2f %s\n", weatherData.Daily.TemperatureMax[0], weatherData.DailyUnits.TemperatureMax)
}
if len(weatherData.Daily.TemperatureMin) > 0 {
fmt.Fprintf(&builder, "- Min temperature: %.2f %s\n", weatherData.Daily.TemperatureMin[0], weatherData.DailyUnits.TemperatureMin)
}
if len(weatherData.Daily.Rain) > 0 {
fmt.Fprintf(&builder, "- Rainfall: %.2f %s\n", weatherData.Daily.Rain[0], weatherData.DailyUnits.Rain)
}
fmt.Fprintf(&builder, "\nForecast for the next days:\n")
// Iterate over the daily data and append to the repor
for i := 0; i < len(weatherData.Daily.Time); i++ {
fmt.Fprintf(&builder, "%s: Max Temp: %.2f %s, Min Temp: %.2f %s, Rain: %.2f %s, Wind Speed: %.2f %s\n",
weatherData.Daily.Time[i],
weatherData.Daily.TemperatureMax[i], weatherData.DailyUnits.TemperatureMax,
weatherData.Daily.TemperatureMin[i], weatherData.DailyUnits.TemperatureMin,
weatherData.Daily.Rain[i], weatherData.DailyUnits.Rain,
weatherData.Daily.WindSpeed[i], weatherData.DailyUnits.WindSpeed)
}
return []byte(builder.String()), nil
}
func wmoCodeToDescription(code int) string {
switch code {
case 0:
return "Clear sky"
case 1, 2, 3:
return "Mainly clear, partly cloudy, and overcast"
case 45, 48:
return "Fog and depositing rime fog"
case 51, 53, 55:
return "Drizzle: Light, moderate, and dense intensity"
case 56, 57:
return "Freezing Drizzle: Light and dense intensity"
case 61, 63, 65:
return "Rain: Slight, moderate and heavy intensity"
case 66, 67:
return "Freezing Rain: Light and heavy intensity"
case 71, 73, 75:
return "Snowfall: Slight, moderate, and heavy intensity"
case 77:
return "Snow grains"
case 80, 81, 82:
return "Rain showers: Slight, moderate, and violent"
case 85, 86:
return "Snow showers slight and heavy"
case 95:
return "Thunderstorm: Slight and heavy intensity"
case 96, 99:
return "Thunderstorm with slight and heavy hail"
default:
return "N/A"
}
}
+8
View File
@@ -13,6 +13,7 @@ const (
DEFAULT_DB_URL = "lassistanoque.db" DEFAULT_DB_URL = "lassistanoque.db"
DEFAULT_HTTP_PORT = 3000 DEFAULT_HTTP_PORT = 3000
DEFAULT_STORAGE_TYPE = "database" DEFAULT_STORAGE_TYPE = "database"
DEFAULT_SERVER_ID = "default"
) )
type Config struct { type Config struct {
@@ -27,6 +28,8 @@ type Config struct {
StorageS3AccessKeyID string // S3 StorageS3AccessKeyID string // S3
StorageS3AccessKeySecret string // S3 StorageS3AccessKeySecret string // S3
StorageS3UseSSL string // S3 StorageS3UseSSL string // S3
ServerID string
SearXngUrl string
} }
var config *Config var config *Config
@@ -56,6 +59,10 @@ func GetConfig() *Config {
if storageType == "" { if storageType == "" {
storageType = DEFAULT_STORAGE_TYPE storageType = DEFAULT_STORAGE_TYPE
} }
serverID := os.Getenv("LASSISTANOQUE_SERVER_ID")
if serverID == "" {
serverID = DEFAULT_SERVER_ID
}
config = &Config{ config = &Config{
DatabaseType: databaseType, DatabaseType: databaseType,
@@ -63,6 +70,7 @@ func GetConfig() *Config {
HttpPort: httpPort, HttpPort: httpPort,
JWTSecret: jwtsecret, JWTSecret: jwtsecret,
StorageType: storageType, StorageType: storageType,
ServerID: serverID,
} }
} }
+28
View File
@@ -0,0 +1,28 @@
package domain
import "time"
type Agent struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
Name string `db:"name" json:"name"`
SystemPrompt string `db:"system_prompt" json:"systemPrompt"`
DefaultProviderID string `db:"def_provider_id" json:"defaultProviderId"`
DefaultModelID string `db:"def_model_id" json:"defaultModelId"`
ToolsPolicy string `db:"tools_policy" json:"toolsPolicy"`
SubAgents bool `db:"sub_agents" json:"subAgents"`
LoopStrategy string `db:"loop_strategy" json:"loopStrategy"`
MaxIterations string `db:"max_iterations" json:"maxIterations"`
StoppingCriteria string `db:"stopping_criteria" json:"stoppingCriteria"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type AgentRepository interface {
ListAgents(userID string, teamID string) ([]*Agent, error)
GetAgent(userID string, teamID string, id string) (*Agent, error)
CreateAgent(userID string, agent *Agent) (*Agent, error)
UpdateAgent(userID string, agent *Agent) (*Agent, error)
DeleteAgent(userID string, teamID string, id string) error
}
+9 -9
View File
@@ -6,15 +6,15 @@ import (
) )
type File struct { type File struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"` Name string `db:"name" json:"name"`
ContentType string `db:"content_type" json:"contentType"` ContentType string `db:"content_type" json:"contentType"`
StoragePath string `db:"storage_path" json:"-"` StoragePath string `db:"storage_path" json:"-"`
StorageFilename string `db:"storage_filename" json:"-"` StorageFilename string `db:"storage_filename" json:"-"`
Content []byte `db:"content" json:"-"` Content []byte `db:"content" json:"-"`
DateCreated time.Time `db:"date_created" json:"-"` DateCreated time.Time `db:"date_created" json:"-"`
DateUpdated time.Time `db:"date_updated" json:"-"` DateUpdated *time.Time `db:"date_updated" json:"-"`
VersionId string `db:"_version" json:"-"` VersionId string `db:"_version" json:"-"`
} }
type FileRepository interface { type FileRepository interface {
+162
View File
@@ -0,0 +1,162 @@
package domain
import (
"context"
"encoding/json"
"time"
)
type Chat struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
StartDatetime *time.Time `db:"start_datetime" json:"startDatetime"`
EndDatetime *time.Time `db:"end_datetime" json:"endDatetime"`
AgentID string `db:"agent_id" json:"agentId"`
UserID string `db:"user_id" json:"userId"`
TaskID string `db:"task_id" json:"taskId,omitempty"`
Title string `db:"title" json:"title"`
FreshTitle bool `db:"fresh_title" json:"freshTitle"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type Message struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
ChatID string `db:"chat_id" json:"chatId"`
Role string `db:"role" json:"role"`
Content string `db:"content" json:"content"`
ToolCallID string `db:"tool_call_id" json:"toolCallId"`
ToolCalls []ToolCall `db:"-" json:"toolcalls"`
ToolCallsJson string `db:"tool_calls" json:"-"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
func (msg *Message) ToolCall2Json() error {
if msg.ToolCalls != nil {
toolCallJson, err := json.Marshal(&msg.ToolCalls)
if err != nil {
return err
}
msg.ToolCallsJson = string(toolCallJson)
}
return nil
}
func (msg *Message) Json2toolCall() error {
if msg.ToolCallsJson != "" {
err := json.Unmarshal([]byte(msg.ToolCallsJson), &msg.ToolCalls)
if err != nil {
return err
}
}
return nil
}
type ChatWithMessages struct {
Chat *Chat `json:"chat"`
Messages []*Message `json:"messages"`
}
type ChatParams struct {
ProviderID string
ModelID string
ContextMaxTokens int // taille de contexte du modèle ; 0 = défaut
MaxResponseTokens int // réserve de réponse ; 0 = défaut
OnChunk StreamCallback
}
type ToolParam struct {
JsonName string `json:"name"`
Type string `json:"type"`
Description string `json:"description"`
Mandatory bool `json:"mandatory"`
}
type ITool interface {
Name() string
Description() string
Params() []ToolParam
Definition(ctx context.Context) *ToolDefinition
Execute(ctx context.Context, input []byte, configuration string) ([]byte, error)
}
type ToolDefinition struct {
Type string
Function *ToolFunction
}
type ToolFunction struct {
Name string
Description string
Parameters map[string]any
}
type ToolCall struct {
ID string
Type string
Function ToolCallFunction
Extra map[string]map[string]any
}
type ToolCallFunction struct {
Name string
Arguments string
}
type Chunk struct {
Done bool `json:"done"`
ToolName string `json:"toolName,omitempty"`
Role string `json:"role,omitempty"`
Content string `json:"content,omitempty"`
Reasoning bool `json:"reasoning,omitempty"`
}
type StreamCallback func(chunk *Chunk)
type Role string
const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
RoleTool Role = "tool"
)
type LLMParams struct {
Tools []*ToolDefinition
OnChunk StreamCallback
ReasoningEffort string
}
type LLMEngine interface {
ListProviderTypes() []Item
ListModelsFromProvider(ctx context.Context, provider *Provider) ([]string, error)
Stream(
ctx context.Context,
provider *Provider,
modelID string,
params *LLMParams,
messages []*Message,
) *Message
Generate(
ctx context.Context,
provider *Provider,
modelID string,
messages []*Message,
) (string, error)
}
type ChatRepository interface {
ListChats(userID string, teamID string, page int, query string, taskID string) ([]*Chat, error)
ListChatsWithUnfreshTitles(teamID string) ([]*Chat, error)
GetChat(userID string, teamID string, id string) (*Chat, error)
CreateChat(userID string, chat *Chat) (*Chat, error)
UpdateChat(userID string, chat *Chat) (*Chat, error)
DeleteChat(userID string, teamID string, id string) error
GetChatMessages(userID string, teamID string, id string, withToolCallResponses bool) ([]*Message, error)
CreateChatMessage(userID string, message *Message) (*Message, error)
}
+21
View File
@@ -0,0 +1,21 @@
package domain
import "time"
// OIDCProvider décrit un fournisseur OpenID Connect enregistré dans la table oidc.
type OIDCProvider struct {
ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"`
Domain string `db:"domain" json:"domain"`
ClientID string `db:"client_id" json:"clientId"`
ClientSecret string `db:"client_secret" json:"-"`
WellknownURL string `db:"wellknown_url" json:"wellknownUrl"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type OIDCProviderRepository interface {
ListOIDCProviders() ([]*OIDCProvider, error)
FindOIDCProvider(id string) (*OIDCProvider, error)
}
+24
View File
@@ -0,0 +1,24 @@
package domain
import "time"
type Provider struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
APIKey string `db:"key" json:"apiKey"`
URL string `db:"url" json:"baseUrl"`
Models string `db:"models" json:"models"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type ProviderRepository interface {
ListProviders(userID string, teamID string) ([]*Provider, error)
GetProvider(userID string, teamID string, id string) (*Provider, error)
CreateProvider(userID string, provider *Provider) (*Provider, error)
UpdateProvider(userID string, provider *Provider) (*Provider, error)
DeleteProvider(userID string, teamID string, id string) error
}
+12
View File
@@ -0,0 +1,12 @@
package domain
type Item struct {
Value string `json:"value"`
Label string `json:"label"`
}
type ComplexItem struct {
Value string `json:"value"`
Label string `json:"label"`
Group string `json:"group"`
}
+1 -1
View File
@@ -7,7 +7,7 @@ type Settings struct {
DefaultLang string `db:"default_lang"` DefaultLang string `db:"default_lang"`
RegisterEnabled bool `db:"register_enabled"` RegisterEnabled bool `db:"register_enabled"`
PasswordEnabled bool `db:"password_enabled"` PasswordEnabled bool `db:"password_enabled"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
+35
View File
@@ -0,0 +1,35 @@
package domain
import "time"
const (
// statuts d'une tâche planifiée
TaskStatusActive = "active"
TaskStatusDone = "done"
)
type Task struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
ModelID string `db:"model_id" json:"modelId"`
Label string `db:"label" json:"label"`
Prompt string `db:"prompt" json:"prompt"`
Cron string `db:"cron" json:"cron"`
Repeat bool `db:"repeat" json:"repeat"`
Status string `db:"status" json:"status"`
NextDatetime *time.Time `db:"next_datetime" json:"nextDatetime"`
LastDatetime *time.Time `db:"last_datetime" json:"lastDatetime"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type TaskRepository interface {
ListTasks(userID string, teamID string) ([]*Task, error)
ListActiveTasks() ([]*Task, error)
GetTaskByID(userID string, teamID string, id string) (*Task, error)
CreateTask(task *Task) (*Task, error)
UpdateTask(task *Task) (*Task, error)
DeleteTask(userID string, teamID string, id string) error
FirstUserOfTeam(teamID string) (string, error)
}
+58
View File
@@ -0,0 +1,58 @@
package domain
import (
"context"
"time"
)
// Clés de contexte passées aux outils lors de l'exécution : identité de
// l'appelant et modèle en cours, pour les outils qui en dépendent
// (ex. scheduler crée une tâche pour l'équipe courante).
type contextKey int
const (
contextKeyUserID contextKey = iota
contextKeyTeamID
contextKeyModelID
)
func WithToolContext(ctx context.Context, userID string, teamID string, modelID string) context.Context {
ctx = context.WithValue(ctx, contextKeyUserID, userID)
ctx = context.WithValue(ctx, contextKeyTeamID, teamID)
return context.WithValue(ctx, contextKeyModelID, modelID)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(contextKeyUserID).(string)
return v, ok
}
func TeamIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(contextKeyTeamID).(string)
return v, ok
}
func ModelIDFromContext(ctx context.Context) (string, bool) {
v, ok := ctx.Value(contextKeyModelID).(string)
return v, ok
}
type Tool struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
Name string `db:"name" json:"name"`
Enabled bool `db:"enabled" json:"enabled"`
Configuration string `db:"configuration" json:"configuration"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type ToolRepository interface {
ListTools(userID string, teamID string) ([]*Tool, error)
GetToolByName(userID string, teamID string, name string) (*Tool, error)
GetToolByID(userID string, teamID string, id string) (*Tool, error)
CreateTool(userID string, agent *Tool) (*Tool, error)
UpdateTool(userID string, agent *Tool) (*Tool, error)
DeleteTool(userID string, teamID string, name string) error
}
+39 -29
View File
@@ -5,36 +5,37 @@ import (
) )
type User struct { type User struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Firstname string `db:"firstname" json:"firstname,omitempty"` Firstname string `db:"firstname" json:"firstname,omitempty"`
Lastname string `db:"lastname" json:"lastname,omitempty"` Lastname string `db:"lastname" json:"lastname,omitempty"`
Password string `db:"password" json:"-"` Password string `db:"password" json:"-"`
Email string `db:"email" json:"email,omitempty"` Email string `db:"email" json:"email,omitempty"`
PictureID string `db:"picture" json:"picture,omitempty"` PictureID string `db:"picture" json:"picture,omitempty"`
Enabled bool `db:"enabled" json:"enabled"` Enabled bool `db:"enabled" json:"enabled"`
Theme string `db:"theme" json:"theme"` Theme string `db:"theme" json:"theme"`
Lang string `db:"lang" json:"lang"` Lang string `db:"lang" json:"lang"`
DateCreated time.Time `db:"_date_created" json:"_date_created"` Timezone string `db:"timezone" json:"timezone"`
DateUpdated time.Time `db:"_date_updated" json:"_date_updated"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
VersionId string `db:"_version" json:"_version"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"`
} }
type Team struct { type Team struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"` Label string `db:"label" json:"label"`
DefaultModelID *string `db:"default_model_id" json:"default_model_id"` DefaultAgentID *string `db:"default_agent_id" json:"defaultAgentId"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
type UserTeam struct { type UserTeam struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"` UserID string `db:"user_id" json:"userId"`
TeamID string `db:"team_id" json:"team_id"` TeamID string `db:"team_id" json:"teamId"`
Administrator bool `db:"administrator" json:"administrator"` Administrator bool `db:"administrator" json:"administrator"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -42,9 +43,9 @@ type UserAddress struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"` UserID string `db:"user_id" json:"user_id"`
Type string `db:"type" json:"type"` Type string `db:"type" json:"type"`
Address string `db:"address_id" json:"address"` Address string `db:"address" json:"address"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -54,8 +55,18 @@ type TeamRelation struct {
} }
type UserWithTeams struct { type UserWithTeams struct {
User *User `json:"user"` User *User `json:"user"`
Teams []*TeamRelation `json:"teams"` UserTeams []*UserTeam `json:"user_teams"`
Teams []*Team `json:"teams"`
}
func AddressTypes() []string {
return []string{
"email",
"telegram",
"whatsapp",
"signal",
}
} }
type UserRepository interface { type UserRepository interface {
@@ -78,10 +89,9 @@ type UserRepository interface {
UpdateUserTeam(userid string, team *UserTeam) (*UserTeam, error) UpdateUserTeam(userid string, team *UserTeam) (*UserTeam, error)
DeleteUserTeam(userid string, teamid string) error DeleteUserTeam(userid string, teamid string) error
PopulateUserWithTeams(user *User) (*UserWithTeams, error)
ListUserAddresses(userid string) ([]*UserAddress, error) ListUserAddresses(userid string) ([]*UserAddress, error)
CreateUserAddress(userid string, addr *UserAddress) (*UserAddress, error) GetUserAddress(addrID string) (*UserAddress, error)
UpdateUserAddress(userid string, addr *UserAddress) (*UserAddress, error) CreateUserAddress(addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(userid string, addrID string) error UpdateUserAddress(addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(addrID string) error
} }
+66
View File
@@ -0,0 +1,66 @@
package gateway
import (
"context"
"fmt"
"sync"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
)
type Dependencies struct {
ChatRepository domain.ChatRepository
}
type Router struct {
started bool
mu sync.Mutex
sessions map[string]*Session // running sessions
newSessionChan chan string
closeChan chan bool
}
func NewRouter(deps Dependencies) *Router {
return &Router{
started: true,
sessions: make(map[string]*Session),
newSessionChan: make(chan string),
closeChan: make(chan bool),
}
}
func (s *Router) Start() error {
for {
select {
case _ = <-s.closeChan:
fmt.Println("Closing LLM Server")
return nil
}
}
}
func (s *Router) Stop(ctx context.Context) error {
s.closeChan <- true
s.started = false
return nil
}
func (s *Router) StartSession() (*Session, error) {
sid := utility.GenID()
session := &Session{}
s.mu.Lock()
s.sessions[sid] = session
s.mu.Unlock()
return session, nil
}
func (s *Router) Process(ctx context.Context, Chat *domain.ChatWithMessages) error {
s.closeChan <- true
s.started = false
return nil
}
+6
View File
@@ -0,0 +1,6 @@
package gateway
type Session struct {
serverID string
chatID string
}
+117
View File
@@ -0,0 +1,117 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/agent"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewAgentGroup(
prefix string,
e *echo.Group,
agentService *agent.Service,
userService *user.Service,
middlewares ...echo.MiddlewareFunc) *echo.Group {
agentHandler := &AgentHandler{
agentService: agentService,
userService: userService,
}
agent := e.Group(prefix, middlewares...)
agent.GET("/:space", agentHandler.ListAgents)
agent.GET("/:space/:agent", agentHandler.GetAgent)
agent.PUT("/:space", agentHandler.UpdateAgent)
agent.POST("/:space", agentHandler.CreateAgent)
agent.DELETE("/:space/:agent", agentHandler.DeleteAgent)
return agent
}
type AgentHandler struct {
agentService *agent.Service
userService *user.Service
}
func (h *AgentHandler) ListAgents(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
agents, err := h.agentService.ListAgents(userID, teamID)
if err != nil {
c.Logger().Error("error listing agents", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing agents")
}
return c.JSON(http.StatusOK, agents)
}
func (h *AgentHandler) GetAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
agentID := c.Param("agent")
agent, err := h.agentService.GetAgent(userID, teamID, agentID)
if err != nil {
c.Logger().Error("error getting agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting agent")
}
return c.JSON(http.StatusOK, agent)
}
func (h *AgentHandler) CreateAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var agent domain.Agent
err := c.Bind(&agent)
if err != nil {
c.Logger().Error("error binding agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding agent")
}
agent.TeamID = teamID
updagent, err := h.agentService.CreateAgent(userID, &agent)
if err != nil {
c.Logger().Error("error creating agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating agent")
}
return c.JSON(http.StatusOK, updagent)
}
func (h *AgentHandler) UpdateAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var agent domain.Agent
err := c.Bind(&agent)
if err != nil {
c.Logger().Error("error binding agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding agent")
}
agent.TeamID = teamID
updagent, err := h.agentService.UpdateAgent(userID, &agent)
if err != nil {
c.Logger().Error("error updating agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error updating agent")
}
return c.JSON(http.StatusOK, updagent)
}
func (h *AgentHandler) DeleteAgent(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
agentID := c.Param("agent")
err := h.agentService.DeleteAgent(userID, teamID, agentID)
if err != nil {
c.Logger().Error("error creating agent", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating agent")
}
return c.JSON(http.StatusOK, agentID)
}
type DefaulAgentRequest struct {
AgentID string `json:"AgentId"`
}
type DefaulAgentResponse struct {
NewDefaultAgentID *string `json:"newDefaultAgentId"`
OldDefaultAgentID *string `json:"oldDefaultAgentId"`
}
+72 -1
View File
@@ -17,6 +17,8 @@ func NewAuthGroup(prefix string, e *echo.Group, service *auth.Service) *echo.Gro
} }
auth := e.Group(prefix) auth := e.Group(prefix)
auth.POST("/login", authHandler.Login) auth.POST("/login", authHandler.Login)
auth.GET("/oidc/providers", authHandler.OIDCProviders)
auth.GET("/oidc/login", authHandler.OIDCLogin)
auth.GET("/oidc/callback", authHandler.OIDCCallback) auth.GET("/oidc/callback", authHandler.OIDCCallback)
auth.POST("/register", authHandler.Register) auth.POST("/register", authHandler.Register)
return auth return auth
@@ -69,8 +71,77 @@ func (h AuthHandler) Login(c *echo.Context) error {
return c.JSON(http.StatusOK, resp) return c.JSON(http.StatusOK, resp)
} }
// oidcRedirectURI reconstruit l'URL de callback du serveur à partir de la
// requête. Elle doit être identique entre /oidc/login et /oidc/callback.
func oidcRedirectURI(c *echo.Context) string {
scheme := "http"
if proto := c.Request().Header.Get("X-Forwarded-Proto"); proto != "" {
scheme = proto
} else if c.Request().TLS != nil {
scheme = "https"
}
host := c.Request().Header.Get("X-Forwarded-Host")
if host == "" {
host = c.Request().Host
}
return scheme + "://" + host + "/api/auth/oidc/callback"
}
func (h AuthHandler) OIDCProviders(c *echo.Context) error {
providers, err := h.authService.ListOIDCProviders()
if err != nil {
c.Logger().Error(fmt.Sprintf("error listing oidc providers: %s", err))
return echo.NewHTTPError(http.StatusInternalServerError, "error listing oidc providers")
}
return c.JSON(http.StatusOK, providers)
}
func (h AuthHandler) OIDCLogin(c *echo.Context) error {
providerID := c.QueryParam("provider")
if providerID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "provider query parameter is required")
}
authorizeURL, err := h.authService.OIDCAuthorizeURL(context.Background(), providerID, oidcRedirectURI(c))
if err != nil {
c.Logger().Error(fmt.Sprintf("error building oidc authorize url: %s", err))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
return c.Redirect(http.StatusFound, authorizeURL)
}
func (h AuthHandler) OIDCCallback(c *echo.Context) error { func (h AuthHandler) OIDCCallback(c *echo.Context) error {
return nil if errParam := c.QueryParam("error"); errParam != "" {
c.Logger().Error(fmt.Sprintf("oidc provider returned an error: %s", errParam))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
code := c.QueryParam("code")
state := c.QueryParam("state")
if code == "" || state == "" {
return echo.NewHTTPError(http.StatusBadRequest, "code and state are required")
}
session, err := h.authService.OIDCCallback(context.Background(), state, code, oidcRedirectURI(c))
if err != nil {
c.Logger().Error(fmt.Sprintf("error while oidc callback: %s", err))
return c.Redirect(http.StatusFound, "/login?error=oidc")
}
// L'application SPA est servie par ce même serveur (même origine) et lit
// ses tokens dans localStorage: on les dépose ici puis on redirige vers
// l'application, sans jamais les faire transiter dans une URL.
page := fmt.Sprintf(`<!doctype html>
<html>
<head><meta charset="utf-8"><title>Connexion en cours...</title></head>
<body>
<script>
localStorage.setItem("accessToken", %q);
localStorage.setItem("refreshToken", %q);
location.replace("/");
</script>
</body>
</html>`, session.AccessToken, session.RefreshToken)
return c.Blob(http.StatusOK, "text/html; charset=utf-8", []byte(page))
} }
func (h AuthHandler) Register(c *echo.Context) error { func (h AuthHandler) Register(c *echo.Context) error {
+192
View File
@@ -0,0 +1,192 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"strconv"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/chat"
)
func NewChatGroup(prefix string, e *echo.Group, service *chat.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
chatHandler := &ChatHandler{
chatService: service,
}
auth := e.Group(prefix, middlewares...)
auth.POST("/:space", chatHandler.NewChat)
auth.POST("/:space/:id", chatHandler.NewChatMessage)
auth.GET("/:space/all/:page", chatHandler.List)
auth.GET("/:space/:id", chatHandler.Get)
auth.GET("/:space/:id/subscribe", chatHandler.Subscribe)
auth.DELETE("/:space/:id", chatHandler.Delete)
return auth
}
type ChatHandler struct {
chatService *chat.Service
}
type ChatRequest struct {
Prompt string `json:"prompt"`
AgentID string `json:"agentID"`
ProviderID string `json:"providerID"`
ModelID string `json:"modelID"`
ReasoningLevel string `json:"reasoningLevel"`
}
func (h *ChatHandler) NewChat(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var request ChatRequest
err := c.Bind(&request)
if err != nil {
c.Logger().Error("error binding request", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error binding request")
}
chat, err := h.chatService.NewChat(
userID,
teamID,
request.AgentID,
"",
)
if err != nil {
c.Logger().Error("error creating chat", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error creating chat")
}
return c.JSON(http.StatusOK, chat)
}
func (h *ChatHandler) NewChatMessage(c *echo.Context) error {
ctx := context.Background()
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
var request ChatRequest
err := c.Bind(&request)
if err != nil {
c.Logger().Error("error binding request", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error binding request")
}
enc := json.NewEncoder(c.Response())
chunkChan := make(chan *domain.Chunk)
_, err = h.chatService.AddChatMessage(
ctx,
userID,
teamID,
chatID,
request.AgentID,
request.Prompt,
&domain.ChatParams{
OnChunk: func(chunk *domain.Chunk) {
chunkChan <- chunk
},
},
)
if err != nil {
c.Logger().Error("error creating chat message", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error creating chat message")
}
enc.Encode(&domain.Chunk{
Done: false,
Role: string(domain.RoleUser),
Content: request.Prompt,
})
http.NewResponseController(c.Response()).Flush()
for {
chunk := <-chunkChan
enc.Encode(chunk)
http.NewResponseController(c.Response()).Flush()
if chunk.Done {
return nil
}
}
}
func (h *ChatHandler) List(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
page := c.Param("page")
pageInt, _ := strconv.Atoi(page)
query := c.QueryParam("q")
taskID := c.QueryParam("task")
chats, err := h.chatService.ListChats(userID, teamID, pageInt, query, taskID)
if err != nil {
c.Logger().Error("error listing chats", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing chats")
}
// évite "null" dans la réponse quand il n'y a pas de conversation
if chats == nil {
chats = []*domain.Chat{}
}
return c.JSON(http.StatusOK, chats)
}
func (h *ChatHandler) Get(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
chatWithMessages, err := h.chatService.GetChatWithMessages(userID, teamID, chatID)
if err != nil {
c.Logger().Error("error getting chat with messages", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting chat")
}
return c.JSON(http.StatusOK, chatWithMessages)
}
func (h *ChatHandler) Delete(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
err := h.chatService.DeleteChat(userID, teamID, chatID)
if err != nil {
c.Logger().Error("error deleting chat", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error deleting chat")
}
return c.JSON(http.StatusOK, "chat deleted")
}
func (h *ChatHandler) Subscribe(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
chatID := c.Param("id")
enc := json.NewEncoder(c.Response())
chunkChan := make(chan *domain.Chunk)
err := h.chatService.Subscribe(
userID,
teamID,
chatID,
func(chunk *domain.Chunk) {
chunkChan <- chunk
},
)
if err != nil {
c.Logger().Error("error subscribing to chat", "error", err.Error())
return echo.NewHTTPError(http.StatusBadRequest, "error creating chat")
}
for {
chunk := <-chunkChan
if chunk.Done {
return nil
}
enc.Encode(chunk)
http.NewResponseController(c.Response()).Flush()
}
}
+623
View File
@@ -0,0 +1,623 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
)
type MiscHandler struct {
}
func NewMiscGroup(prefix string, e *echo.Group, middlewares ...echo.MiddlewareFunc) *echo.Group {
miscHandler := &MiscHandler{}
misc := e.Group(prefix, middlewares...)
misc.GET("/timezones", miscHandler.Timezones)
return misc
}
func (h MiscHandler) Timezones(c *echo.Context) error {
tz := []string{
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmara",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Timbuktu",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/ComodRivadavia",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Atikokan",
"America/Atka",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Coyhaique",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Ensenada",
"America/Fort_Nelson",
"America/Fort_Wayne",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Knox_IN",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montreal",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Acre",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Rosario",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Shiprock",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Virgin",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/South_Pole",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Ashkhabad",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Chongqing",
"Asia/Chungking",
"Asia/Colombo",
"Asia/Dacca",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Harbin",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Istanbul",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kashgar",
"Asia/Kathmandu",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macao",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Tel_Aviv",
"Asia/Thimbu",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ujung_Pandang",
"Asia/Ulaanbaatar",
"Asia/Ulan_Bator",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Faroe",
"Atlantic/Jan_Mayen",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/ACT",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Canberra",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/LHI",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/NSW",
"Australia/North",
"Australia/Perth",
"Australia/Queensland",
"Australia/South",
"Australia/Sydney",
"Australia/Tasmania",
"Australia/Victoria",
"Australia/West",
"Australia/Yancowinna",
"Brazil/Acre",
"Brazil/DeNoronha",
"Brazil/East",
"Brazil/West",
"CET",
"CST6CDT",
"Canada/Atlantic",
"Canada/Central",
"Canada/Eastern",
"Canada/Mountain",
"Canada/Newfoundland",
"Canada/Pacific",
"Canada/Saskatchewan",
"Canada/Yukon",
"Chile/Continental",
"Chile/EasterIsland",
"Cuba",
"EET",
"EST",
"EST5EDT",
"Egypt",
"Eire",
"Etc/GMT",
"Etc/GMT+0",
"Etc/GMT+1",
"Etc/GMT+10",
"Etc/GMT+11",
"Etc/GMT+12",
"Etc/GMT+2",
"Etc/GMT+3",
"Etc/GMT+4",
"Etc/GMT+5",
"Etc/GMT+6",
"Etc/GMT+7",
"Etc/GMT+8",
"Etc/GMT+9",
"Etc/GMT-0",
"Etc/GMT-1",
"Etc/GMT-10",
"Etc/GMT-11",
"Etc/GMT-12",
"Etc/GMT-13",
"Etc/GMT-14",
"Etc/GMT-2",
"Etc/GMT-3",
"Etc/GMT-4",
"Etc/GMT-5",
"Etc/GMT-6",
"Etc/GMT-7",
"Etc/GMT-8",
"Etc/GMT-9",
"Etc/GMT0",
"Etc/Greenwich",
"Etc/UCT",
"Etc/UTC",
"Etc/Universal",
"Etc/Zulu",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belfast",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Kyiv",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Nicosia",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Tiraspol",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Factory",
"GB",
"GB-Eire",
"GMT",
"GMT+0",
"GMT-0",
"GMT0",
"Greenwich",
"HST",
"Hongkong",
"Iceland",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Iran",
"Israel",
"Jamaica",
"Japan",
"Kwajalein",
"Libya",
"MET",
"MST",
"MST7MDT",
"Mexico/BajaNorte",
"Mexico/BajaSur",
"Mexico/General",
"NZ",
"NZ-CHAT",
"Navajo",
"PRC",
"PST8PDT",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kanton",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Pohnpei",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Samoa",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
"Pacific/Yap",
"Poland",
"Portugal",
"ROC",
"ROK",
"Singapore",
"Turkey",
"UCT",
"US/Alaska",
"US/Aleutian",
"US/Arizona",
"US/Central",
"US/East-Indiana",
"US/Eastern",
"US/Hawaii",
"US/Indiana-Starke",
"US/Michigan",
"US/Mountain",
"US/Pacific",
"US/Samoa",
"UTC",
"Universal",
"W-SU",
"WET",
"Zulu",
}
return c.JSON(http.StatusOK, tz)
}
+135
View File
@@ -0,0 +1,135 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
)
func NewModelGroup(prefix string, e *echo.Group, service *provider.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
modelHandler := &ModelHandler{
providerService: service,
}
model := e.Group(prefix, middlewares...)
model.GET("/providerTypes", modelHandler.ListProviderTypes)
model.GET("/:space", modelHandler.ListProviders)
model.GET("/:space/models", modelHandler.ListProvidersModels)
model.POST("/:space/avalable-models", modelHandler.ListAvailableModels)
model.GET("/:space/:provider", modelHandler.GetProvider)
model.PUT("/:space", modelHandler.UpdateProvider)
model.POST("/:space", modelHandler.CreateProvider)
model.DELETE("/:space/:provider", modelHandler.DeleteProvider)
return model
}
type ModelHandler struct {
providerService *provider.Service
}
func (h *ModelHandler) ListProviderTypes(c *echo.Context) error {
return c.JSON(http.StatusOK, h.providerService.ListProviderTypes())
}
func (h *ModelHandler) ListProviders(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
providers, err := h.providerService.ListProviders(userID, teamID)
if err != nil {
c.Logger().Error("error listing providers", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing providers")
}
return c.JSON(http.StatusOK, providers)
}
func (h *ModelHandler) GetProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
providerID := c.Param("provider")
provider, err := h.providerService.GetProvider(userID, teamID, providerID)
if err != nil {
c.Logger().Error("error getting provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting provider")
}
return c.JSON(http.StatusOK, provider)
}
func (h *ModelHandler) CreateProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
updprovider, err := h.providerService.CreateProvider(userID, &provider)
if err != nil {
c.Logger().Error("error creating provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, updprovider)
}
func (h *ModelHandler) UpdateProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
updprovider, err := h.providerService.UpdateProvider(userID, &provider)
if err != nil {
c.Logger().Error("error updating provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, updprovider)
}
func (h *ModelHandler) DeleteProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
providerID := c.Param("provider")
err := h.providerService.DeleteProvider(userID, teamID, providerID)
if err != nil {
c.Logger().Error("error creating provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, providerID)
}
func (h *ModelHandler) ListAvailableModels(c *echo.Context) error {
teamID := c.Param("space")
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
list, err := h.providerService.ListAvailableModels(c.Request().Context(), &provider)
if err != nil {
c.Logger().Error("error listing available models", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing available models")
}
return c.JSON(http.StatusOK, list)
}
func (h *ModelHandler) ListProvidersModels(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
list, err := h.providerService.ListProvidersModels(userID, teamID)
if err != nil {
c.Logger().Error("error listing providers models", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing providers models")
}
return c.JSON(http.StatusOK, list)
}
+70
View File
@@ -0,0 +1,70 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/task"
)
type TaskHandler struct {
taskService *task.Service
}
func NewTaskGroup(prefix string, e *echo.Group, service *task.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
taskHandler := &TaskHandler{taskService: service}
auth := e.Group(prefix, middlewares...)
auth.GET("/:space", taskHandler.List)
auth.PUT("/:space/:id", taskHandler.Update)
auth.DELETE("/:space/:id", taskHandler.Delete)
return auth
}
func (h *TaskHandler) List(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
tasks, err := h.taskService.ListTasks(userID, teamID)
if err != nil {
c.Logger().Error("error listing tasks", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing tasks")
}
// évite "null" dans la réponse quand il n'y a pas de tâche
if tasks == nil {
tasks = []*domain.Task{}
}
return c.JSON(http.StatusOK, tasks)
}
func (h *TaskHandler) Update(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
taskID := c.Param("id")
var taskModel domain.Task
if err := c.Bind(&taskModel); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "error binding request")
}
taskModel.ID = taskID
updated, err := h.taskService.UpdateTask(userID, teamID, &taskModel)
if err != nil {
c.Logger().Error("error updating task", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error updating task")
}
return c.JSON(http.StatusOK, updated)
}
func (h *TaskHandler) Delete(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
taskID := c.Param("id")
if err := h.taskService.DeleteTask(userID, teamID, taskID); err != nil {
c.Logger().Error("error deleting task", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error deleting task")
}
return c.JSON(http.StatusOK, "task deleted")
}
+55
View File
@@ -0,0 +1,55 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewTeamGroup(prefix string, e *echo.Group, service *user.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
teamHandler := &TeamHandler{
userService: service,
}
h := e.Group(prefix, middlewares...)
h.GET("/:team", teamHandler.Get)
h.PUT("/:team", teamHandler.Update)
return h
}
type TeamHandler struct {
userService *user.Service
}
func (h TeamHandler) Get(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("team")
user, err := h.userService.GetTeam(userID, teamID)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
}
return c.JSON(http.StatusOK, user)
}
func (h TeamHandler) Update(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("team")
var updTeam domain.Team
if err := c.Bind(&updTeam); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
if teamID != updTeam.ID {
return c.String(http.StatusBadRequest, "bad request")
}
user, err := h.userService.UpdateTeam(userID, &updTeam)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, user)
}
+123
View File
@@ -0,0 +1,123 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewToolGroup(
prefix string,
e *echo.Group,
ToolService *tool.Service,
userService *user.Service,
middlewares ...echo.MiddlewareFunc) *echo.Group {
ToolHandler := &ToolHandler{
ToolService: ToolService,
userService: userService,
}
Tool := e.Group(prefix, middlewares...)
Tool.GET("/:space", ToolHandler.ListTools)
Tool.GET("/:space/:tool", ToolHandler.GetTool)
Tool.GET("/:space/:tool/params", ToolHandler.GetToolParams)
Tool.PUT("/:space", ToolHandler.UpdateTool)
Tool.POST("/:space", ToolHandler.CreateTool)
//Tool.DELETE("/:space/:tool", ToolHandler.DeleteTool)
return Tool
}
type ToolHandler struct {
ToolService *tool.Service
userService *user.Service
}
func (h *ToolHandler) ListTools(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
Tools, err := h.ToolService.ListTools(userID, teamID)
if err != nil {
c.Logger().Error("error listing Tools", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing Tools")
}
return c.JSON(http.StatusOK, Tools)
}
func (h *ToolHandler) GetTool(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
ToolID := c.Param("tool")
Tool, err := h.ToolService.GetTool(userID, teamID, ToolID)
if err != nil {
c.Logger().Error("error getting Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting Tool")
}
return c.JSON(http.StatusOK, Tool)
}
func (h *ToolHandler) GetToolParams(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
toolID := c.Param("tool")
tool, err := h.ToolService.GetTool(userID, teamID, toolID)
if err != nil {
c.Logger().Error("error getting Tool", "error", err, "teamid", teamID, "toolID", toolID)
return echo.NewHTTPError(http.StatusBadRequest, "error getting Tool")
}
toolImpl := h.ToolService.GetToolImpl(tool.Name)
return c.JSON(http.StatusOK, toolImpl.Params())
}
func (h *ToolHandler) CreateTool(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var Tool domain.Tool
err := c.Bind(&Tool)
if err != nil {
c.Logger().Error("error binding Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding Tool")
}
Tool.TeamID = teamID
updTool, err := h.ToolService.CreateTool(userID, &Tool)
if err != nil {
c.Logger().Error("error creating Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating Tool")
}
return c.JSON(http.StatusOK, updTool)
}
func (h *ToolHandler) UpdateTool(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var Tool domain.Tool
err := c.Bind(&Tool)
if err != nil {
c.Logger().Error("error binding Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding Tool")
}
Tool.TeamID = teamID
updTool, err := h.ToolService.UpdateTool(userID, &Tool)
if err != nil {
c.Logger().Error("error updating Tool", "error", err)
return echo.NewHTTPError(http.StatusBadRequest, "error updating Tool")
}
return c.JSON(http.StatusOK, updTool)
}
// func (h *ToolHandler) DeleteTool(c *echo.Context) error {
// userID := c.Get(auth.ContextUserIDKey).(string)
// teamID := c.Param("space")
// ToolID := c.Param("tool")
// err := h.ToolService.DeleteTool(userID, teamID, ToolID)
// if err != nil {
// c.Logger().Error("error creating Tool", "error", err)
// return echo.NewHTTPError(http.StatusBadRequest, "error creating Tool")
// }
// return c.JSON(http.StatusOK, ToolID)
// }
+79 -5
View File
@@ -13,10 +13,13 @@ func NewUserGroup(prefix string, e *echo.Group, service *user.Service, middlewar
userHandler := &UserHandler{ userHandler := &UserHandler{
userService: service, userService: service,
} }
_ = userHandler
auth := e.Group(prefix, middlewares...) auth := e.Group(prefix, middlewares...)
auth.GET("/me", userHandler.Me) auth.GET("/me", userHandler.Me)
auth.PUT("/:id", userHandler.Update) auth.PUT("/:id", userHandler.Update)
auth.GET("/:id/address", userHandler.Addresses)
auth.POST("/:id/address", userHandler.CreateAddress)
auth.PUT("/:id/address", userHandler.UpdateAddress)
auth.DELETE("/:id/address/:address", userHandler.DeleteAddress)
return auth return auth
} }
@@ -26,7 +29,7 @@ type UserHandler struct {
func (h UserHandler) Me(c *echo.Context) error { func (h UserHandler) Me(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string) userID := c.Get(auth.ContextUserIDKey).(string)
user, err := h.userService.GetUser(userID) user, err := h.userService.GetUserWithTeams(userID)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
} }
@@ -43,15 +46,86 @@ func (h UserHandler) Update(c *echo.Context) error {
} }
updUser.ID = id updUser.ID = id
isAdmin := false // TODO : check if caller is admin if !(userID == id) {
if !(isAdmin || userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user") return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user")
} }
user, err := h.userService.UpdateUser(&updUser, isAdmin) user, err := h.userService.UpdateUser(&updUser, false)
if err != nil { if err != nil {
return c.String(http.StatusBadRequest, err.Error()) return c.String(http.StatusBadRequest, err.Error())
} }
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
func (h UserHandler) Addresses(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
addresses, err := h.userService.ListAdresses(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, addresses)
}
func (h UserHandler) CreateAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
var address domain.UserAddress
if err := c.Bind(&address); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
address.UserID = userID
newaddress, err := h.userService.AddAddress(userID, address.Type, address.Address)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, newaddress)
}
func (h UserHandler) UpdateAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
var address domain.UserAddress
if err := c.Bind(&address); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
address.UserID = userID
newaddress, err := h.userService.UpdateAddress(userID, &address)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, newaddress)
}
func (h UserHandler) DeleteAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
addressID := c.Param("address")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
err := h.userService.DeleteAddress(userID, addressID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, "deleted")
}
+36 -8
View File
@@ -1,6 +1,7 @@
package http package http
import ( import (
"context"
"embed" "embed"
"fmt" "fmt"
"io/fs" "io/fs"
@@ -10,8 +11,13 @@ import (
"github.com/labstack/echo/v5/middleware" "github.com/labstack/echo/v5/middleware"
"trankilou.fr/lassistanoque/backend/internal/config" "trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/http/handlers" "trankilou.fr/lassistanoque/backend/internal/http/handlers"
"trankilou.fr/lassistanoque/backend/internal/service/agent"
"trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/chat"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage" "trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/task"
"trankilou.fr/lassistanoque/backend/internal/service/tool"
"trankilou.fr/lassistanoque/backend/internal/service/user" "trankilou.fr/lassistanoque/backend/internal/service/user"
) )
@@ -28,18 +34,25 @@ var indexhtml []byte
var robotstxt []byte var robotstxt []byte
type Router struct { type Router struct {
echo *echo.Echo server *http.Server
} }
type Dependencies struct { type Dependencies struct {
StorageService *storage.Service StorageService *storage.Service
AuthService *auth.Service AuthService *auth.Service
UserService *user.Service UserService *user.Service
TokenManager auth.TokenManager ProviderService *provider.Service
AgentService *agent.Service
ChatService *chat.Service
ToolService *tool.Service
TaskService *task.Service
TokenManager auth.TokenManager
} }
func NewRouter(deps Dependencies) *Router { func NewRouter(deps Dependencies) *Router {
cfg := config.GetConfig()
e := echo.New() e := echo.New()
e.Use(middleware.RequestLogger()) e.Use(middleware.RequestLogger())
e.Use(middleware.CORS("http://localhost:5173")) e.Use(middleware.CORS("http://localhost:5173"))
@@ -70,13 +83,28 @@ func NewRouter(deps Dependencies) *Router {
api := e.Group("/api") api := e.Group("/api")
_ = handlers.NewAuthGroup("/auth", api, deps.AuthService) _ = handlers.NewAuthGroup("/auth", api, deps.AuthService)
_ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware) _ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware)
_ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewAgentGroup("/agent", api, deps.AgentService, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewToolGroup("/tool", api, deps.ToolService, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewTeamGroup("/team", api, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewChatGroup("/chat", api, deps.ChatService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewTaskGroup("/task", api, deps.TaskService, deps.TokenManager.TokenMiddleware)
server := http.Server{
Addr: fmt.Sprintf(":%d", cfg.HttpPort),
Handler: e,
}
return &Router{ return &Router{
echo: e, &server,
} }
} }
func (r *Router) Start() error { func (r *Router) Start() error {
cfg := config.GetConfig() return r.server.ListenAndServe()
return r.echo.Start(fmt.Sprintf(":%d", cfg.HttpPort)) }
func (r *Router) Stop(ctx context.Context) error {
return r.server.Shutdown(ctx)
} }
@@ -0,0 +1,2 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-duration:initial;--tw-leading:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-100:oklch(93.6% .032 17.717);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-green-300:oklch(87.1% .15 154.449);--color-green-500:oklch(72.3% .219 149.579);--color-green-700:oklch(52.7% .154 150.069);--color-cyan-600:oklch(60.9% .126 221.723);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-700:oklch(50% .134 242.749);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-900:oklch(37.9% .146 265.522);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-3xl:48rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-medium:500;--font-weight-bold:700;--radius-sm:.25rem;--radius-lg:.5rem;--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-heightLine truncated
@@ -1,2 +0,0 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-outline-style:solid;--tw-font-weight:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-700:oklch(50% .134 242.749);--color-sky-950:oklch(29.3% .066 243.157);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-bold:700;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiLine truncated
@@ -0,0 +1 @@
img.svelte-1hoglcg{opacity:0;max-width:100%;height:auto}img.fade-in.svelte-1hoglcg{opacity:1;transition:opacity .3s ease-in-out}img.visible.svelte-1hoglcg{opacity:1;transition:none}img.error.svelte-1hoglcg{opacity:.5;filter:grayscale()}
@@ -0,0 +1 @@
import{A as e,B as t,K as n,M as r,N as i,Q as a,W as o,Z as s,at as c,dt as l,ft as u,it as d,j as f,k as p,nt as m,rt as h,tt as g,vt as _}from"./DGrvnoUE.js";import"./xihTtKlq.js";var v=class extends Map{#e=new Map;#t=d(0);#n=d(0);#r=o||-1;constructor(e){if(super(),e){for(var[t,n]of e)super.set(t,n);this.#n.v=super.size}}#i(e){return o===this.#r?d(e):h(e)}has(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else return t(this.#t),!1;return t(r),!0}forEach(e,t){this.#a(),super.forEach(e,t)}get(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else{t(this.#t);return}return t(r),super.get(e)}set(e,t){var n=this.#e,r=n.get(e),i=super.get(e),a=super.set(e,t),o=this.#t;if(r===void 0)r=this.#i(0),n.set(e,r),m(this.#n,super.size),g(o);else if(i!==t){g(r);var s=o.reactions===null?null:new Set(o.reactions);(s===null||!r.reactions?.every(e=>s.has(e)))&&g(o)}return a}delete(e){var t=this.#e,n=t.get(e),r=super.delete(e);return n!==void 0&&(t.delete(e),m(n,-1)),r&&(m(this.#n,super.size),g(this.#t)),r}clear(){if(super.size!==0){super.clear();var e=this.#e;m(this.#n,0);for(var t of e.values())m(t,-1);g(this.#t),e.clear()}}#a(){t(this.#t);var e=this.#e;if(this.#n.v!==e.size){for(var n of super.keys())if(!e.has(n)){var r=this.#i(0);e.set(n,r)}}for([,r]of this.#e)t(r)}keys(){return t(this.#t),super.keys()}values(){return this.#a(),super.values()}entries(){return this.#a(),super.entries()}[Symbol.iterator](){return this.entries()}get size(){return t(this.#n),super.size}},y=i(`<span class="text-red-700 dark:text-red-400 text-xs"> </span>`);function b(i,o){u(o,!0);var d=r(),m=a(d),h=t=>{var r=y(),i=s(r,!0);_(r),n(t=>e(i,t),[()=>o.errors.get(o.key)]),f(t,r)},g=c(()=>o.errors.has(o.key));p(m,e=>{t(g)&&e(h)}),f(i,d),l()}export{v as n,b as t};
@@ -0,0 +1 @@
import{gt as e}from"./DGrvnoUE.js";e();
@@ -1 +0,0 @@
import"./DxmhE1z1.js";import{a as e,r as t}from"./DZTG_XPZ.js";var n={get data(){return e.data},get error(){return e.error},get form(){return e.form},get params(){return e.params},get route(){return e.route},get state(){return e.state},get status(){return e.status},get url(){return e.url}};t.updated.check;var r=n;export{r as t};
@@ -0,0 +1 @@
var e=`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXoAAAA1CAYAAABP2SCdAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAClxJREFUeJzt3W2MXFUZB/D/c+6d7WK7c6e73btbWgSUlshrlQ8YAoKQIIkhBk2lO01rEz6YUrTxJTRqsKyaygfxLUgUTKi4c7dQEU2IiIIaRAOUtxZTlZgUQdqdu9vuzvR9Z+Y8fmi7vXdmdu/c05md5fL8PvWePec8596982x67pxziZkhhBAiuVS7ByCEEKK17FP/WDg0ch2T6pipMpOeKGT7X2z9sIRozPxtY2enKvqSYBlZ+o3xVf1vtmlIQsw5U4leK/UYgO6Zq6sXAVzZ2iEJ0ThL841MeChUyNbXANzTnhEJMffI1I0QQiScJHohhEg4SfRCCJFwU3P0RPg5M+ZPHYM6GXxbe4YlWiE9nL+ZWJ0bLFOTqaHxdZmJdo1JCNF6U4l+YsDdFPxB1/aRXlVSkugThDTdDuKbgmWVeZWnAUiiFyLBZOpGCCESThK9EEIknCR6IYRIODu6ikgKTXiQgGeCZWQf9ds1HiHE7JBE/x5yMOv+ut1jEELMPpm6EUKIhJNEL4QQCSeJXgghEq6tc/SO598H0Pvjt+RXCln3bqOYudHvgHBZ/JZ6TyHbt9EkZtrzv0qgj8VvyeOFrPs5s5j52wjqU1H1SmW1/sjanndMYlTLDPufYSaj8arJ1NqoFbrpR8aWU4W/Fywj8DnV9VhjreONXhUZU1W+aLKdcWZo7COs+O647U7ExObxVb2vzlSHtu/uSJd6f2XSf92YqPxgPNv/57jtMl5hIWPyFyYxNXiryTMhAijtjf7WJGbd/pjvm1jt/iFYlh7ObyFWoa2trZS95sDKhYW4/XcNj12rmL8SikkYmhjofTSqrZPL/xikzosbE8DOQrb3rjgN2vswlul6EH/IoOU885C4isAfj90OtMs0JoGuAPhmg6Z545ikLgFHx7RTuNM0Rp2oywzPE6X5pc7I3suVbhBF93/inoq+r8oq1oflFLbYbeTa1qM1fhJdK22ZXse6MaGM/miUdKnTVmbjIMZzJu0wCMKy5p07CE/UFtHVAF8TLCsdmzTKKYr1UgCh8TJjR0NDI7qOwZfGjck4vVVNo2TqRgghEk4SvRBCJJwkeiGESLi2ztGT1p/Uth17boy4vMLx/H+FysAPT2T7tgTLnGH/m2Bkg2UKdJcm6/a4MS0qHWuknpMb3QDiL4THy/dqZQ/GjUkVXY7b5hRd1t8l2/5ZqD9UHgDjmunaxJHJ5W9gotB8M4MeA1kmz1xwGD2jUXWKpaOvdc3rCvWvtL6FibdUVf0+k/VgVH8TPeN7ADfmSIGzyvqvR2zb6DyJ9WbH838ULCsrdf3hVYv2ztiQ6Z+s1KfNYlY2Op7/9WCZRfjsgQE39NzJ8fI7AOo6dWwrHGDD3yeovLb6M6qg11c/FHZy/pMgnH/qOH0BaeOYXFlHwKboirPP8UZ/CIR3jgWwkcn6b9y+FPRl1dcWQK6Qdb89XZu2JvqJNX17TNo5nn8ugAuDZQz01VRk9FfXA+n9xYHe6ovUPMSLasamcKg40NO6mHUcXNPnAwhtb+Dk/MOg5vSvFXURV11bsNXK8+R15x0DEOo/7Y3uqz4lIhotNDSOHqNxjKzpO1w9jkY5nt+NqvujQ5dSkQ2Jj5teW8fz51fHrBCfVSfIcgDpqUPGSDFrFjOT822mqpj69B+R0yHxQQDLTh8zm55netj3wSYtW4+Jl1R/XizCvgMG55rJ5ZeAKPzZO5HrpiVTN0IIkXCS6IUQIuEk0QshRMK1e2XsPwBc1Kr+i1l3AwZxR7Csa5n/R8fznzbobmch6344qlIh6w7SIEIPRdIX+EOO5+cMYo4Usu7ZBu1arjjg/oYGYQXLnOWjdzqer036qxAWHxpwjReIzSbH8z8B4EnD5k16SiLe7SqM1xzPYJdwin8LtXtlrAJxy258Bhibw49n0h4RED8mx/jfD29GKNk5hjExx//HVX2emWEAponMNrh724WIwK27b8V7xqzdQ3M6kQghhDhzkuiFECLhZm3qJv3wOz2pDnpfqFDZY8z4X9y+COgEsMhkHAT4jPgxAdrfs21vaKfE0iQfKa5dsj+6Le83ikmoiTlpqWMHV/ZHLi5qBwYXza4tkDrOfT3b9k59n/zo5DzdrF01zwRth9Vd2Rt6TkI6tUCT2XnSifs2cgM3MTPa+mZnd2dHb6iM7UxLYz6AVHd6b/j76pTqBht/eT/PQOmMBwaAQOMz/XzWEr1KpR4sa9wSLNOsLz64un933L5OPgz7vck4CtneVSbtFg75l5dhvxUsI4sfBXBrdEz3DiD8ULgRC7aP9pdL9r5gmaXxLIBr4/Y1GwoD7v0A7jdp63j+iNb21KK3lF0pAGjpB7cRmcrIOWVthxf2ET9VzLo12yM3wvH8pwDc2IyxvZelOxfcUNa6ZmfKVnIWjF1W1vZL4VLzFVoW4cbq1cmtIlM3QgiRcJLohRAi4STRCyFEwk07R5/CvMkySn8KlhHwTiaXvyGqUyL9n/Hs4tCubMzYxYATLLNAl2Zy+cVxB02kVnADc2PpobELFVWWxu2/HrbgMlP4ehBer66X+WX+fCh8oBkxFWM+q3BMADub0fcU1h/N5PJLgkUTq/ueiWqWHt63SGl1edPGoegFZiw4dUiEQw01Y96rCeFrxNpu5D7t6FAv+Ct7Z4xTYnVUoap/wluN9F8XoXvOrpki/CX0O1A4ZHqeDDXJ4PB1s6j2SwSEvzPj7dOH4MZyDC3RVb8XAs5BcIO06Vu/zODQQ1CycWUmlz8yUysm6ufamH0ALo6MCLzOQHewrMJ8USaX752uTRzatt4u3rrojel+Pm2iP/n+xNAFd7blr2RNz0eHVd8AENo6tt47Xp3c6G6zVwk29gCElN7IoPXx+68TkbGrmHUjExtbtBbA3c2ISYR8IevOuCvdGccAP8S1a5UayETW1Ux4vFnjqNi0+NDK3pG47U6+DzT8TlDPvweEyNXPpeO8AhF/OE+u1g1/DoZHb+IG+n+3KQy4oXcMzx8aW2wrPfP2ydNg5k3FrBuZsAsD7rrgMQ1CpZf5lQYi/K6Y7Qv1nx72v0yMe6Nj9n6puszxfJ8JUUn3lWLWvaKq3WoAQ9Ex3W9Vl2U8fxcTYr9KsB4q6/sBbJju5zJ1I4QQCSeJXgghEk4SvRBCJFysBVMKnNegn0bVY9DLjXXI28CI/TC2HoJ6rs5InjXa6q0ORdzgSkjaAXDkNWoEA4Vm9HMaPQHwm83oyapgj1ZoynkCwLxJa8YHYbEQngdHj+24tsZMute68pYi1bRz16nKwVDB+NIyFvih/gn8Ngwx0zNEfDhYpsCRO4V2kjpchjY6T6Xwqkk7bAbDa+C+Iqr5IsSZIMZWJtS+ASsUkmpf+8f6DVTfC0wv1dSrH/QRMP0txjCn7wr87MyhzJfvCiHEnFHvYSwBn5/Iug+0a0xzhUzdCCFEwkmiF0KIhJNEL4QQCSeJXgghEk4SvRBCJJwkeiGESDhJ9EIIkXCS6IUQIuFm7VWCQgjRSqrMj3PK+newzMLkrLyqb66TlbFCCJFw/weRJ1LNZA9BxAAAAABJRU5ErkJggg==`;export{e as t};
@@ -1 +0,0 @@
import{B as e,C as t,D as n,F as r,H as i,K as a,P as o,Q as s,R as c,V as l,_ as u,g as d,h as f,k as p,m,q as h,v as g,z as _}from"./DxmhE1z1.js";import"./xihTtKlq.js";var v=class extends Map{#e=new Map;#t=l(0);#n=l(0);#r=n||-1;constructor(e){if(super(),e){for(var[t,n]of e)super.set(t,n);this.#n.v=super.size}}#i(t){return n===this.#r?l(t):e(t)}has(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else return t(this.#t),!1;return t(r),!0}forEach(e,t){this.#a(),super.forEach(e,t)}get(e){var n=this.#e,r=n.get(e);if(r===void 0)if(super.has(e))r=this.#i(0),n.set(e,r);else{t(this.#t);return}return t(r),super.get(e)}set(e,t){var n=this.#e,r=n.get(e),i=super.get(e),a=super.set(e,t),o=this.#t;if(r===void 0)r=this.#i(0),n.set(e,r),_(this.#n,super.size),c(o);else if(i!==t){c(r);var s=o.reactions===null?null:new Set(o.reactions);(s===null||!r.reactions?.every(e=>s.has(e)))&&c(o)}return a}delete(e){var t=this.#e,n=t.get(e),r=super.delete(e);return n!==void 0&&(t.delete(e),_(n,-1)),r&&(_(this.#n,super.size),c(this.#t)),r}clear(){if(super.size!==0){super.clear();var e=this.#e;_(this.#n,0);for(var t of e.values())_(t,-1);c(this.#t),e.clear()}}#a(){t(this.#t);var e=this.#e;if(this.#n.v!==e.size){for(var n of super.keys())if(!e.has(n)){var r=this.#i(0);e.set(n,r)}}for([,r]of this.#e)t(r)}keys(){return t(this.#t),super.keys()}values(){return this.#a(),super.values()}entries(){return this.#a(),super.entries()}[Symbol.iterator](){return this.entries()}get size(){return t(this.#n),super.size}},y=g(`<span class="text-red-700 dark:text-red-400 text-xs"> </span>`),b=g(`<span class="text-xs">&nbsp;</span>`);function x(e,n){h(n,!0);var c=u(),l=r(c),g=e=>{var t=y(),r=o(t,!0);s(t),p(e=>f(r,e),[()=>n.errors.get(n.key)]),d(e,t)},_=i(()=>n.errors.has(n.key)),v=e=>{var t=b();d(e,t)};m(l,e=>{t(_)?e(g):e(v,-1)}),d(e,c),a()}export{v as n,x as t};
@@ -0,0 +1 @@
var e=e=>{e.focus()};export{e as t};
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={createChat:(t,n)=>e.post(`/chat/${t}`,n),createChatMessage:(t,n,r)=>e.postStream(`/chat/${t}/${n}`,r),allChats:(t,n,r,i)=>e.get(`/chat/${t}/all/${n}${r||i?`?${[r&&`q=${encodeURIComponent(r)}`,i&&`task=${i}`].filter(Boolean).join(`&`)}`:``}`),getChat:(t,n)=>e.get(`/chat/${t}/${n}`),subscribe:(t,n)=>e.get(`/chat/${t}/${n}/subscribe`),deleteChat:(t,n)=>e.delete(`/chat/${t}/${n}`)};export{t};
@@ -0,0 +1 @@
import{B as e,H as t,V as n,ct as r,it as i,nt as a,r as o,t as s}from"./DGrvnoUE.js";var c=class{constructor(e,t){this.status=e,this.body=typeof t==`string`?{message:t}:t||{message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}},l=class{constructor(e,t){try{new Headers({location:t})}catch{throw Error(`Invalid redirect location ${JSON.stringify(t)}: this string contains characters that cannot be used in HTTP headers`)}this.status=e,this.location=t}},u=class extends Error{constructor(e,t,n){super(n),this.status=e,this.text=t}};new URL(`sveltekit-internal://`);function d(e,t){return e===`/`||t===`ignore`?e:t===`never`?e.endsWith(`/`)?e.slice(0,-1):e:t===`always`&&!e.endsWith(`/`)?e+`/`:e}function f(e){return e.split(`%25`).map(decodeURI).join(`%25`)}function p(e){for(let t in e)e[t]=decodeURIComponent(e[t]);return e}function m({href:e}){return e.split(`#`)[0]}function h(e,t,n,r=!1){let i=new URL(e);Object.defineProperty(i,"searchParams",{value:new Proxy(i.searchParams,{get(e,r){if(r===`get`||r===`getAll`||r===`has`)return(t,...i)=>(n(t),e[r](t,...i));t();let i=Reflect.get(e,r);return typeof i==`function`?i.bind(e):i}}),enumerable:!0,configurable:!0});let a=[`href`,`pathname`,`search`,`toString`,`toJSON`];r&&a.push(`hash`);for(let n of a)Object.defineProperty(i,n,{get(){return t(),e[n]},enumerable:!0,configurable:!0});return i}function g(){}function _(...e){let t=5381;for(let n of e)if(typeof n==`string`){let e=n.length;for(;e;)t=t*33^n.charCodeAt(--e)}else if(ArrayBuffer.isView(n)){let e=new Uint8Array(n.buffer,n.byteOffset,n.byteLength),r=e.length;for(;r;)t=t*33^e[--r]}else throw TypeError(`value must be a string or TypedArray`);return(t>>>0).toString(36)}new TextEncoder;function v(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}var ee=window.fetch;window.fetch=(e,t)=>((e instanceof Request?e.method:t?.method||`GET`)!==`GET`&&y.delete(x(e)),ee(e,t));var y=new Map;function b(e,t){let n=x(e,t),r=document.querySelector(n);if(r?.textContent){r.remove();let{body:e,...t}=JSON.parse(r.textContent);r.getAttribute(`data-b64`)!==null&&(e=v(e));let i=r.getAttribute(`data-ttl`);return i&&y.set(n,{body:e,init:t,ttl:1e3*Number(i)}),Promise.resolve(new Response(e,t))}return window.fetch(e,t)}function te(e,t,n){if(y.size>0){let t=x(e,n),r=y.get(t);if(r){if(performance.now()<r.ttl&&[`default`,`force-cache`,`only-if-cached`,void 0].includes(n?.cache))return new Response(r.body,r.init);y.delete(t)}}return window.fetch(t,n)}function x(e,t){let n=`script[data-sveltekit-fetched][data-url=${JSON.stringify(e instanceof Request?e.url:e)}]`;if(t?.headers||t?.body){let e=[];t.headers&&e.push([...new Headers(t.headers)].join(`,`)),t.body&&(typeof t.body==`string`||ArrayBuffer.isView(t.body))&&e.push(t.body),n+=`[data-hash="${_(...e)}"]`}return n}var ne=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/,re=/^\/\((?:[^)]+)\)$/;function ie(e){let t=[];return{pattern:e===`/`||re.test(e)?/^\/$/:RegExp(`^${oe(e).map(e=>{let n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(e);if(n)return t.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),`(?:/([^]*))?`;let r=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(e);if(r)return t.push({name:r[1],matcher:r[2],optional:!0,rest:!1,chained:!0}),`(?:/([^/]+))?`;if(!e)return;let i=e.split(/\[(.+?)\](?!\])/);return`/`+i.map((e,n)=>{if(n%2){if(e.startsWith(`x+`))return ce(String.fromCharCode(parseInt(e.slice(2),16)));if(e.startsWith(`u+`))return ce(String.fromCharCode(...e.slice(2).split(`-`).map(e=>parseInt(e,16))));let[,r,a,o,s]=ne.exec(e);return t.push({name:o,matcher:s,optional:!!r,rest:!!a,chained:a?n===1&&i[0]===``:!1}),a?`([^]*?)`:r?`([^/]*)?`:`([^/]+?)`}return ce(e)}).join(``)}).join(``)}/?$`),params:t}}function ae(e){return e!==``&&!/^\([^)]+\)$/.test(e)}function oe(e){return e.slice(1).split(`/`).filter(ae)}function se(e,t,n){let r={},i=e.slice(1),a=i.filter(e=>e!==void 0),o=0;for(let e=0;e<t.length;e+=1){let s=t[e],c=i[e-o];if(s.chained&&s.rest&&o&&(c=i.slice(e-o,e+1).filter(e=>e).join(`/`),o=0),c===void 0)if(s.rest)c=``;else continue;if(!s.matcher||n[s.matcher](c)){r[s.name]=c;let n=t[e+1],l=i[e+1];n&&!n.rest&&n.optional&&l&&s.chained&&(o=0),!n&&!l&&Object.keys(r).length===a.length&&(o=0);continue}if(s.optional&&s.chained){o++;continue}return}if(!o)return r}function ce(e){return e.normalize().replace(/[[\]]/g,`\\$&`).replace(/%/g,`%25`).replace(/\//g,`%2[Ff]`).replace(/\?/g,`%3[Ff]`).replace(/#/g,`%23`).replace(/[.*+?^${}()|\\]/g,`\\$&`)}function le({nodes:e,server_loads:t,dictionary:n,matchers:r}){let i=new Set(t);return Object.entries(n).map(([t,[n,i,s]])=>{let{pattern:c,params:l}=ie(t),u={id:t,exec:e=>{let t=c.exec(e);if(t)return se(t,l,r)},errors:[1,...s||[]].map(t=>e[t]),layouts:[0,...i||[]].map(o),leaf:a(n)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function a(t){let n=t<0;return n&&(t=~t),[n,e[t]]}function o(t){return t===void 0?t:[i.has(t),e[t]]}}function ue(e,t=JSON.parse){try{return t(sessionStorage Line truncated
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={providerTypes:()=>e.get(`/provider/providerTypes`),getProviders:t=>e.get(`/provider/${t}`),getProvider:(t,n)=>e.get(`/provider/${t}/${n}`),createProvider:(t,n)=>e.post(`/provider/${t}`,n),updateProvider:(t,n)=>e.put(`/provider/${t}`,n),deleteProvider:(t,n)=>e.delete(`/provider/${t}/${n}`),getAvailableModels:(t,n)=>e.post(`/provider/${t}/avalable-models`,n),getProvidersModels:t=>e.get(`/provider/${t}/models`)};export{t};
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={register:t=>e.post(`/auth/register`,t),status:()=>e.get(`/auth/status`),login:t=>e.post(`/auth/login`,t),oidcProviders:()=>e.get(`/auth/oidc/providers`,{auth:!1}),oidcLoginUrl:e=>`/api/auth/oidc/login?provider=${encodeURIComponent(e)}`};export{t};
@@ -0,0 +1,4 @@
import{$ as e,A as t,C as n,I as r,K as i,L as a,M as o,N as s,Q as c,R as l,X as u,Z as d,a as f,dt as p,et as m,ft as h,j as g,k as _,vt as v}from"./DGrvnoUE.js";import"./xihTtKlq.js";import{t as y}from"./DtfiK5fE.js";var b={listTasks:e=>y.get(`/task/${e}`),updateTask:(e,t,n)=>y.put(`/task/${e}/${t}`,n),deleteTask:(e,t)=>y.delete(`/task/${e}/${t}`)},x=0,S=m([]),C=(e,t)=>{S.push({id:x++,type:e,text:t,status:`appearing`})},w=e=>e;function T(e){let t=e-1;return t*t*t+1}function E(e,{delay:t=0,duration:n=400,easing:r=w}={}){let i=+getComputedStyle(e).opacity;return{delay:t,duration:n,easing:r,css:e=>`opacity: ${e*i}`}}function D(e,{delay:t=0,duration:n=400,easing:r=T,start:i=0,opacity:a=0}={}){let o=getComputedStyle(e),s=+o.opacity,c=o.transform===`none`?``:o.transform,l=1-i,u=s*(1-a);return{delay:t,duration:n,easing:r,css:(e,t)=>`
transform: ${c} scale(${1-l*t});
opacity: ${s-u*t}
`}}var O=s(`<div class="fixed inset-0 z-50 flex items-center justify-center p-4"><button type="button" class="absolute inset-0 cursor-default bg-black/50" aria-label="Fermer"></button> <div class="relative w-full max-w-sm rounded-lg bg-stone-50 p-4 shadow-xl dark:bg-stone-900" role="dialog" aria-modal="true" aria-labelledby="confirm-dialog-title"><div class="flex items-center gap-3"><div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-600 dark:bg-red-950 dark:text-red-400"><i class="icon-trash-empty"></i></div> <h2 id="confirm-dialog-title" class="text-base font-bold"> </h2></div> <p class="mt-3 text-sm text-stone-600 dark:text-stone-400"> </p> <div class="mt-4 flex justify-end gap-2"><button type="button" class="bt small"> </button> <button type="button" class="bt small danger"> </button></div></div></div>`);function k(r,s){h(s,!0);let m=f(s,`open`,3,!1),y=f(s,`title`,3,``),b=f(s,`message`,3,``),x=f(s,`confirmLabel`,3,`Confirmer`),S=f(s,`cancelLabel`,3,`Annuler`),C=()=>s.onCancel?.(),w=e=>{m()&&e.key===`Escape`&&C()};var T=o();l(`keydown`,u,w);var k=c(T),A=r=>{var o=O(),c=d(o),l=e(c,2),u=d(l),f=e(d(u),2),p=d(f,!0);v(f),v(u);var m=e(u,2),h=d(m,!0);v(m);var _=e(m,2),w=d(_),T=d(w,!0);v(w);var k=e(w,2),A=d(k,!0);v(k),v(_),v(l),v(o),i(()=>{t(p,y()),t(h,b()),t(T,S()),t(A,x())}),a(`click`,c,C),n(3,c,()=>E,()=>({duration:150})),a(`click`,w,C),a(`click`,k,function(...e){s.onConfirm?.apply(this,e)}),n(3,l,()=>D,()=>({duration:150,start:.95})),g(r,o)};_(k,e=>{m()&&e(A)}),g(r,T),p()}r([`click`]);export{C as n,b as r,k as t};
@@ -0,0 +1 @@
import"./BZuNOv8h.js";
@@ -0,0 +1 @@
import{et as e}from"./DGrvnoUE.js";var t=e({spaces:[],current:``});export{t};
@@ -0,0 +1 @@
var e=/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,t=/^https?:\/\/([a-zA-Z0-9.-]+|\[[0-9a-fA-F:.]+\])(:\d+)?(\/[^\s]*)?$/;function n(t){return e.test(t)}function r(e){return t.test(e)}export{r as n,n as t};
@@ -0,0 +1,3 @@
var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r},n=Array.isArray,r=Array.prototype.indexOf,i=Array.prototype.includes,a=Array.from,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyDescriptors,l=Object.prototype,u=Array.prototype,d=Object.getPrototypeOf,f=Object.isExtensible;function p(e){return typeof e==`function`}var m=()=>{};function h(e){for(var t=0;t<e.length;t++)e[t]()}function g(){var e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}var _=1<<24,v=1024,y=2048,b=4096,x=8192,S=16384,ee=32768,te=1<<25,ne=65536,re=1<<18,ie=1<<19,ae=1<<20,oe=1<<25,se=65536,ce=1<<21,le=1<<22,ue=1<<23,C=Symbol(`$state`),de=Symbol(`legacy props`),fe=Symbol(``),pe=Symbol(`proxy path`),me=Symbol(`attributes`),he=Symbol(`class`),ge=Symbol(`style`),_e=Symbol(`text`),ve=Symbol(`form reset`),ye=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},be=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function xe(e){throw Error(`https://svelte.dev/e/experimental_async_required`)}function Se(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function Ce(){throw Error(`https://svelte.dev/e/missing_context`)}function we(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function Te(e,t,n){throw Error(`https://svelte.dev/e/each_key_duplicate`)}function Ee(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function De(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function Oe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function ke(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ae(){throw Error(`https://svelte.dev/e/fork_discarded`)}function je(){throw Error(`https://svelte.dev/e/fork_timing`)}function Me(){throw Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`)}function Ne(){throw Error(`https://svelte.dev/e/hydration_failed`)}function Pe(e){throw Error(`https://svelte.dev/e/lifecycle_legacy_only`)}function Fe(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function Ie(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Le(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Re(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function ze(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}var Be={},w=Symbol(`uninitialized`),Ve=`http://www.w3.org/1999/xhtml`,He=`http://www.w3.org/2000/svg`;function Ue(){console.warn(`https://svelte.dev/e/derived_inert`)}function We(e){console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`)}function Ge(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ke(){console.warn(`https://svelte.dev/e/select_multiple_invalid_value`)}function qe(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var T=!1;function E(e){T=e}var D;function O(e){if(e===null)throw Ge(),Be;return D=e}function k(){return O(R(D))}function Je(e){if(T){if(R(D)!==null)throw Ge(),Be;D=e}}function Ye(e=1){if(T){for(var t=e,n=D;t--;)n=R(n);D=n}}function Xe(e=!0){for(var t=0,n=D;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else(r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=R(n);e&&n.remove(),n=i}}function Ze(e){if(!e||e.nodeType!==8)throw Ge(),Be;return e.data}function Qe(e){return e===this.v}function $e(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function et(e){return!$e(e,this.v)}var tt=!1;function nt(){tt=!0}function rt(e,t){return e.label=t,it(e.v,t),e}function it(e,t){return e?.[pe]?.(t),e}function at(e){return typeof e==`symbol`?`Symbol(${e.description})`:typeof e==`function`?`<function>`:typeof e==`object`&&e?`<object>`:String(e)}var A=null;function ot(e){A=e}function st(){let e={};return[()=>(ut(e)||Ce(),ct(e)),t=>lt(e,t)]}function ct(e){return ht(`getContext`).get(e)}function lt(e,t){return ht(`setContext`).set(e,t),t}function ut(e){return ht(`hasContext`).has(e)}function dt(){return ht(`getAllContexts`)}function ft(e,t=!1,n){A={p:A,i:!1,c:null,e:null,s:e,x:null,r:G,l:tt&&!t?{s:null,u:null,$:[]}:null}}function pt(e){var t=A,n=t.e;if(n!==null){t.e=null;for(var r of n)$n(r)}return e!==void 0&&(t.x=e),t.i=!0,A=t.p,e??{}}function mt(){return!tt||A!==null&&A.l===null}function ht(e){return A===null&&Se(e),A.c??=new Map(gt(A)||void 0)}function gt(e){let t=e.p;for(;t!==null;){let e=t.c;if(e!==null)return e;t=t.p}return null}var _t=[];function vt(){var e=_t;_t=[],h(e)}function j(e){if(_t.length===0&&!rn){var t=_t;queueMicrotask(()=>{t===_t&&vt()})}_t.push(e)}function yt(){for(;_t.length>0;)vt()}function bt(e){var t=G;if(t===null)return H.f|=ue,e;if(!(t.f&32768)&&!(t.f&4))throw e;xt(e,t)}function xt(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);rLine truncated
`).some(e=>e.startsWith(`https://svelte.dev/e/`)))throw r;return r!==Be&&console.warn(`Failed to hydrate: `,r),t.recover===!1&&Ne(),zn(),Un(n),E(!1),yi(e,t)}finally{E(r),O(i)}}var xi=new Map;function Si(e,{target:t,anchor:n,props:r={},events:i,context:o,intro:s=!0,transformError:c}){zn();var l=void 0,u=tr(()=>{var u=n??t.appendChild(I());Rt(u,{pending:()=>{}},t=>{ft({});var n=A;if(o&&(n.c=o),i&&(r.$$events=i),T&&$(t,null),gi=s,l=e(t,r)||{},gi=!0,T&&(G.nodes.end=D,D===null||D.nodeType!==8||D.data!==`]`))throw Ge(),Be;pt()},c);var d=new Set,f=e=>{for(var n=0;n<e.length;n++){var r=e[n];if(!d.has(r)){d.add(r);var i=Jr(r);for(let e of[t,document]){var a=xi.get(e);a===void 0&&(a=new Map,xi.set(e,a));var o=a.get(r);o===void 0?(e.addEventListener(r,oi,{passive:i}),a.set(r,1)):a.set(r,o+1)}}}};return f(a(Qr)),$r.add(f),()=>{for(var e of d)for(let n of[t,document]){var r=xi.get(n),i=r.get(e);--i==0?(n.removeEventListener(e,oi),r.delete(e),r.size===0&&xi.delete(n)):r.set(e,i)}$r.delete(f),u!==n&&u.parentNode?.removeChild(u)}});return Ci.set(l,u),l}var Ci=new WeakMap;function wi(e,t){let n=Ci.get(e);return n?(Ci.delete(e),n(t)):Promise.resolve()}var Ti=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)hr(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(hr(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(V(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();_r(r,t),t.append(I()),this.#n.set(e,{effect:r,fragment:t})}else V(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),pr(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(V(n.effect),this.#n.delete(e))};ensure(e,t){var n=N,r=Wn();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=I();i.append(a),this.#n.set(e,{effect:B(()=>t(a)),fragment:i})}else this.#t.set(e,B(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else T&&(this.anchor=D),this.#a(n)}};function Ei(e,t,n=!1){var r;T&&(r=D,k());var i=new Ti(e),a=n?ne:0;function o(e,t){if(T){var n=Ze(r);if(e!==parseInt(n.substring(1))){var a=Xe();O(a),i.anchor=a,E(!1),i.ensure(e,t),E(!0);return}}i.ensure(e,t)}or(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}var Di=Symbol(`NaN`);function Oi(e,t,n){T&&k();var r=new Ti(e),i=!mt();or(()=>{var e=t();e!==e&&(e=Di),i&&typeof e==`object`&&e&&(e={}),r.ensure(e,n)})}function ki(e,t){return t}function Ai(e,t,n){for(var r=[],i=t.length,o,s=t.length,c=0;c<i;c++){let n=t[c];pr(n,()=>{if(o){if(o.pending.delete(n),o.done.add(n),o.pending.size===0){var t=e.outrogroups;ji(e,a(o.done)),t.delete(o),t.size===0&&(e.outrogroups=null)}}else--s},!1)}if(s===0){var l=r.length===0&&n!==null;if(l){var u=n,d=u.parentNode;Un(d),d.append(u),e.items.clear()}ji(e,t,!l)}else o={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(o)}function ji(e,t,n=!0){var r;if(e.pending.size>0){r=new Set;for(let t of e.pending.values())for(let n of t)r.add(e.items.get(n).e)}for(var i=0;i<t.length;i++){var a=t[i];r?.has(a)?(a.f|=oe,_r(a,document.createDocumentFragment())):V(t[i],n)}}var Mi;function Ni(e,t,r,i,o,s=null){var c=e,l=new Map;if(t&4){var u=e;c=T?O(L(u)):u.appendChild(I())}T&&k();var d=null,f=Jt(()=>{var e=r();return n(e)?e:e==null?[]:a(e)}),p,m=new Map,h=!0;function g(e){v.effect.f&16384||(v.pending.delete(e),v.fallback=d,Fi(v,p,c,t,i),d!==null&&(p.length===0?d.f&33554432?(d.f^=oe,Li(d,null,c)):hr(d):pr(d,()=>{d=null})))}function _(e){v.pending.delete(e)}var v={effect:or(()=>{p=Z(f);var e=p.length;let n=!1;T&&Ze(c)===`[!`!=(e===0)&&(c=Xe(),O(c),E(!1),n=!0);for(var a=new Set,u=N,v=Wn(),y=0;y<e;y+=1){T&&D.nodeType===8&&D.data===`]`&&(c=D,n=!0,E(!1));var b=p[y],x=i(b,y),S=h?null:l.get(x);S?(S.v&&Dn(S.v,b),S.i&&Dn(S.i,y),v&&u.unskip_effect(S.e)):(S=Ii(l,h?c:Mi??=I(),b,x,y,o,t,r),h||(S.e.f|=oe),l.set(x,S)),a.add(x)}if(e===0&&s&&!d&&(h?d=B(()=>s(c)):(d=B(()=>s(Mi??=I())),d.f|=oe)),e>a.size&&Te(``,``,``),T&&e>0&&O(Xe()),!h)if(m.set(u,a),v){for(let[e,t]of l)a.has(e)||u.skip_effect(t.e);u.oncommit(g),u.ondiscard(_)}else g(u);n&&E(!0),Z(f)}),flags:t,items:l,pending:m,outrogroups:null,fallback:d};h=!1,T&&(c=D)}function Pi(e){for(;e!==null&&!(e.f&32);)e=e.next;return e}function Fi(e,t,n,r,i){var o=!!(r&8),s=t.length,c=e.items,l=Pi(e.effect.first),u,d=null,f,p=[],m=[],h,g,_,v;if(o)for(v=0;v<s;v+=1)h=t[v],g=i(h,v),_=c.get(g).e,_.f&33554432||(_.nodes?.a?.measure(),(f??=new Set).add(_));for(v=0;v<s;v+=1){if Line truncated
\r\f\xA0\v`];function oa(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||aa.includes(r[o-1]))&&(s===r.length||aa.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function sa(e,t=!1){var n=t?` !important;`:`;`,r=``;for(var i of Object.keys(e)){var a=e[i];a!=null&&a!==``&&(r+=` `+i+`: `+a+n)}return r}function ca(e){return e[0]!==`-`||e[1]!==`-`?e.toLowerCase():e}function la(e,t){if(t){var n=``,r,i;if(Array.isArray(t)?(r=t[0],i=t[1]):r=t,e){e=String(e).replaceAll(/\s*\/\*.*?\*\/\s*/g,``).trim();var a=!1,o=0,s=!1,c=[];r&&c.push(...Object.keys(r).map(ca)),i&&c.push(...Object.keys(i).map(ca));var l=0,u=-1;let t=e.length;for(var d=0;d<t;d++){var f=e[d];if(s?f===`/`&&e[d-1]===`*`&&(s=!1):a?a===f&&(a=!1):f===`/`&&e[d+1]===`*`?s=!0:f===`"`||f===`'`?a=f:f===`(`?o++:f===`)`&&o--,!s&&a===!1&&o===0){if(f===`:`&&u===-1)u=d;else if(f===`;`||d===t-1){if(u!==-1){var p=ca(e.substring(l,u).trim());if(!c.includes(p)){f!==`;`&&d++;var m=e.substring(l,d).trim();n+=` `+m+`;`}}l=d+1,u=-1}}}}return r&&(n+=sa(r)),i&&(n+=sa(i,!0)),n=n.trim(),n===``?null:n}return e==null?null:String(e)}function ua(e,t,n,r,i,a){var o=e[he];if(T||o!==n||o===void 0){var s=oa(n,r,a);(!T||s!==e.getAttribute(`class`))&&(s==null?e.removeAttribute(`class`):t?e.className=s:e.setAttribute(`class`,s)),e[he]=n}else if(a&&i!==a)for(var c in a){var l=!!a[c];(i==null||l!==!!i[c])&&e.classList.toggle(c,l)}return a}function da(e,t={},n,r){for(var i in n){var a=n[i];t[i]!==a&&(n[i]==null?e.style.removeProperty(i):e.style.setProperty(i,a,r))}}function fa(e,t,n,r){var i=e[ge];if(T||i!==t){var a=la(t,r);(!T||a!==e.getAttribute(`style`))&&(a==null?e.removeAttribute(`style`):e.style.cssText=a),e[ge]=t}else r&&(Array.isArray(r)?(da(e,n?.[0],r[0]),da(e,n?.[1],r[1],`important`)):da(e,n,r));return r}function pa(e,t,r=!1){if(e.multiple){if(t==null)return;if(!n(t))return Ke();for(var i of e.options)i.selected=t.includes(ga(i));return}for(i of e.options)if(Nn(ga(i),t)){i.selected=!0;return}(!r||t!==void 0)&&(e.selectedIndex=-1)}function ma(e){var t=new MutationObserver(()=>{`__value`in e&&pa(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[`value`]}),Zn(()=>{t.disconnect()})}function ha(e,t,n=t){var r=new WeakSet,i=!0;Ft(e,`change`,t=>{var i=t?`[selected]`:`:checked`,a;if(e.multiple)a=[].map.call(e.querySelectorAll(i),ga);else{var o=e.querySelector(i)??e.querySelector(`option:not([disabled])`);a=o&&ga(o)}n(a),e.__value=a,N!==null&&r.add(N)}),nr(()=>{var a=t();if(e===document.activeElement){var o=N;if(r.has(o))return}if(pa(e,a,i),i&&a===void 0){var s=e.querySelector(`:checked`);s!==null&&(a=ga(s),n(a))}e.__value=a,i=!1}),ma(e)}function ga(e){return`__value`in e?e.__value:e.value}var _a=Symbol(`class`),va=Symbol(`style`),ya=Symbol(`is custom element`),ba=Symbol(`is html`),xa=be?`link`:`LINK`,Sa=be?`input`:`INPUT`,Ca=be?`option`:`OPTION`,wa=be?`select`:`SELECT`,Ta=be?`progress`:`PROGRESS`;function Ea(e){if(T){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute(`value`)){var n=e.value;ka(e,`value`,null),e.value=n}if(e.hasAttribute(`checked`)){var r=e.checked;ka(e,`checked`,null),e.checked=r}}};e[ve]=n,j(n),Nt()}}function Da(e,t){var n=Ma(e);n.value!==(n.value=t??void 0)&&(e.value!==t||t===0&&e.nodeName===Ta)&&(e.value=t??``)}function Oa(e,t){t?e.hasAttribute(`selected`)||e.setAttribute(`selected`,``):e.removeAttribute(`selected`)}function ka(e,t,n,r){var i=Ma(e);T&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===xa)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[fe]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&Pa(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Aa(e,t,n,r,i=!1,a=!1){if(T&&i&&e.nodeName===Sa){var o=e;(o.type===`checkbox`?`defaultChecked`:`defaultValue`)in n||Ea(o)}var s=Ma(e),c=s[ya],l=!s[ba];let u=T&&c;u&&E(!1);var d=t||{},f=e.nodeName===Ca;for(var p in t)p in n||(n[p]=null);n.class?n.class=ia(n.class):(r||n[_a])&&(n.class=null),n[va]&&(n.style??=null);var m=Pa(e);if(e.nodeName===Sa&&`type`in n&&(`value`in n||`__value`in n)){var h=n.type;(h!==d.type||h===void 0&&e.hasAttribute(`type`))&&(d.type=h,ka(e,`type`,h,a))}for(let i in n){let o=n[i];if(f&&i===`value`&&o==null){e.value=e.__value=``,d[i]=o;continue}if(i===`class`){ua(e,e.namespaceURI===`http://www.w3.org/1999/xhtml`,o,r,t?.[_a],n[_a]),d[i]=o,d[_a]=n[_a];continue}if(i===`style`){fa(e,o,t?.[va],n[va]),d[i]=o,d[va]=n[va];continue}var g=d[i];if(!(o===g&&!(o===void 0&&e.hasAttribute(i)))){d[i]=o;var _=i[0]+i[1];if(_!==`$$`)if(_===`on`){let t={},n=`$$`+i,r=i.slice(2);var v=Ur(r);if(Vr(r)&&(r=r.slice(0,-7),t.capture=!0),!v&&g){if(o!=null)continue;e.removeEventListener(r,d[n],t),d[n]=null}if(v)ri(r,e,o),ii([r]);else if(o!=null){function a(e){d[i].call(this,e)}d[n]=ti(r,e,a,t)}}else if(i===`style`)ka(e,i,o);else if(i===`autofocus`)At(e,!!o);else if(!c&&(i===`__value`||i===`value`&&o!=null)) Line truncated
@@ -0,0 +1 @@
var e=()=>{document.documentElement.classList.toggle(`dark`,localStorage.theme===`dark`||!(`theme`in localStorage)&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)},t=e=>{e==`light`?localStorage.setItem(`theme`,`light`):e==`dark`?localStorage.setItem(`theme`,`dark`):localStorage.removeItem(`theme`)};export{t as n,e as t};
@@ -1 +0,0 @@
import{C as e,T as t,V as n,W as r,n as i,t as a,w as o,z as s}from"./DxmhE1z1.js";var c=class{constructor(e,t){this.status=e,this.body=typeof t==`string`?{message:t}:t||{message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}},l=class{constructor(e,t){try{new Headers({location:t})}catch{throw Error(`Invalid redirect location ${JSON.stringify(t)}: this string contains characters that cannot be used in HTTP headers`)}this.status=e,this.location=t}},u=class extends Error{constructor(e,t,n){super(n),this.status=e,this.text=t}};new URL(`sveltekit-internal://`);function d(e,t){return e===`/`||t===`ignore`?e:t===`never`?e.endsWith(`/`)?e.slice(0,-1):e:t===`always`&&!e.endsWith(`/`)?e+`/`:e}function f(e){return e.split(`%25`).map(decodeURI).join(`%25`)}function p(e){for(let t in e)e[t]=decodeURIComponent(e[t]);return e}function m({href:e}){return e.split(`#`)[0]}function h(e,t,n,r=!1){let i=new URL(e);Object.defineProperty(i,"searchParams",{value:new Proxy(i.searchParams,{get(e,r){if(r===`get`||r===`getAll`||r===`has`)return(t,...i)=>(n(t),e[r](t,...i));t();let i=Reflect.get(e,r);return typeof i==`function`?i.bind(e):i}}),enumerable:!0,configurable:!0});let a=[`href`,`pathname`,`search`,`toString`,`toJSON`];r&&a.push(`hash`);for(let n of a)Object.defineProperty(i,n,{get(){return t(),e[n]},enumerable:!0,configurable:!0});return i}function g(){}function _(...e){let t=5381;for(let n of e)if(typeof n==`string`){let e=n.length;for(;e;)t=t*33^n.charCodeAt(--e)}else if(ArrayBuffer.isView(n)){let e=new Uint8Array(n.buffer,n.byteOffset,n.byteLength),r=e.length;for(;r;)t=t*33^e[--r]}else throw TypeError(`value must be a string or TypedArray`);return(t>>>0).toString(36)}new TextEncoder;function v(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}var ee=window.fetch;window.fetch=(e,t)=>((e instanceof Request?e.method:t?.method||`GET`)!==`GET`&&y.delete(x(e)),ee(e,t));var y=new Map;function b(e,t){let n=x(e,t),r=document.querySelector(n);if(r?.textContent){r.remove();let{body:e,...t}=JSON.parse(r.textContent);r.getAttribute(`data-b64`)!==null&&(e=v(e));let i=r.getAttribute(`data-ttl`);return i&&y.set(n,{body:e,init:t,ttl:1e3*Number(i)}),Promise.resolve(new Response(e,t))}return window.fetch(e,t)}function te(e,t,n){if(y.size>0){let t=x(e,n),r=y.get(t);if(r){if(performance.now()<r.ttl&&[`default`,`force-cache`,`only-if-cached`,void 0].includes(n?.cache))return new Response(r.body,r.init);y.delete(t)}}return window.fetch(t,n)}function x(e,t){let n=`script[data-sveltekit-fetched][data-url=${JSON.stringify(e instanceof Request?e.url:e)}]`;if(t?.headers||t?.body){let e=[];t.headers&&e.push([...new Headers(t.headers)].join(`,`)),t.body&&(typeof t.body==`string`||ArrayBuffer.isView(t.body))&&e.push(t.body),n+=`[data-hash="${_(...e)}"]`}return n}var ne=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/,re=/^\/\((?:[^)]+)\)$/;function ie(e){let t=[];return{pattern:e===`/`||re.test(e)?/^\/$/:RegExp(`^${oe(e).map(e=>{let n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(e);if(n)return t.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),`(?:/([^]*))?`;let r=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(e);if(r)return t.push({name:r[1],matcher:r[2],optional:!0,rest:!1,chained:!0}),`(?:/([^/]+))?`;if(!e)return;let i=e.split(/\[(.+?)\](?!\])/);return`/`+i.map((e,n)=>{if(n%2){if(e.startsWith(`x+`))return ce(String.fromCharCode(parseInt(e.slice(2),16)));if(e.startsWith(`u+`))return ce(String.fromCharCode(...e.slice(2).split(`-`).map(e=>parseInt(e,16))));let[,r,a,o,s]=ne.exec(e);return t.push({name:o,matcher:s,optional:!!r,rest:!!a,chained:a?n===1&&i[0]===``:!1}),a?`([^]*?)`:r?`([^/]*)?`:`([^/]+?)`}return ce(e)}).join(``)}).join(``)}/?$`),params:t}}function ae(e){return e!==``&&!/^\([^)]+\)$/.test(e)}function oe(e){return e.slice(1).split(`/`).filter(ae)}function se(e,t,n){let r={},i=e.slice(1),a=i.filter(e=>e!==void 0),o=0;for(let e=0;e<t.length;e+=1){let s=t[e],c=i[e-o];if(s.chained&&s.rest&&o&&(c=i.slice(e-o,e+1).filter(e=>e).join(`/`),o=0),c===void 0)if(s.rest)c=``;else continue;if(!s.matcher||n[s.matcher](c)){r[s.name]=c;let n=t[e+1],l=i[e+1];n&&!n.rest&&n.optional&&l&&s.chained&&(o=0),!n&&!l&&Object.keys(r).length===a.length&&(o=0);continue}if(s.optional&&s.chained){o++;continue}return}if(!o)return r}function ce(e){return e.normalize().replace(/[[\]]/g,`\\$&`).replace(/%/g,`%25`).replace(/\//g,`%2[Ff]`).replace(/\?/g,`%3[Ff]`).replace(/#/g,`%23`).replace(/[.*+?^${}()|\\]/g,`\\$&`)}function le({nodes:e,server_loads:t,dictionary:n,matchers:r}){let i=new Set(t);return Object.entries(n).map(([t,[n,i,s]])=>{let{pattern:c,params:l}=ie(t),u={id:t,exec:e=>{let t=c.exec(e);if(t)return se(t,l,r)},errors:[1,...s||[]].map(t=>e[t]),layouts:[0,...i||[]].map(o),leaf:a(n)};return u.errors.length=u.layouts.length=Math.max(u.errors.length,u.layouts.length),u});function a(t){let n=t<0;return n&&(t=~t),[n,e[t]]}function o(t){return t===void 0?t:[i.has(t),e[t]]}}function ue(e,t=JSON.parse){try{return t(sessionStorage[e] Line truncated
@@ -0,0 +1,6 @@
import{$ as e,A as t,B as n,D as r,E as i,F as a,H as o,I as s,K as c,L as l,M as u,N as d,P as f,Q as p,R as m,T as h,U as g,X as _,Z as v,_ as y,a as b,at as x,b as ee,c as te,dt as ne,et as S,f as re,ft as ie,h as ae,it as oe,j as C,k as w,m as se,n as ce,nt as T,p as le,q as E,r as ue,u as de,v as fe,vt as D}from"./DGrvnoUE.js";import"./BZuNOv8h.js";import"./xihTtKlq.js";import"./B6JECYQD.js";var pe=Math.min,O=Math.max,me=Math.round,he=Math.floor,k=e=>({x:e,y:e}),A={left:`right`,right:`left`,bottom:`top`,top:`bottom`};function j(e,t,n){return O(e,pe(t,n))}function ge(e,t){return typeof e==`function`?e(t):e}function M(e){return e.split(`-`)[0]}function N(e){return e.split(`-`)[1]}function P(e){return e===`x`?`y`:`x`}function F(e){return e===`y`?`height`:`width`}function I(e){let t=e[0];return t===`t`||t===`b`?`y`:`x`}function _e(e){return P(I(e))}function ve(e,t,n){n===void 0&&(n=!1);let r=N(e),i=_e(e),a=F(i),o=i===`x`?r===(n?`end`:`start`)?`right`:`left`:r===`start`?`bottom`:`top`;return t.reference[a]>t.floating[a]&&(o=Ee(o)),[o,Ee(o)]}function ye(e){let t=Ee(e);return[be(e),t,be(t)]}function be(e){return e.includes(`start`)?e.replace(`start`,`end`):e.replace(`end`,`start`)}var xe=[`left`,`right`],Se=[`right`,`left`],Ce=[`top`,`bottom`],L=[`bottom`,`top`];function we(e,t,n){switch(e){case`top`:case`bottom`:return n?t?Se:xe:t?xe:Se;case`left`:case`right`:return t?Ce:L;default:return[]}}function Te(e,t,n,r){let i=N(e),a=we(M(e),n===`start`,r);return i&&(a=a.map(e=>e+`-`+i),t&&(a=a.concat(a.map(be)))),a}function Ee(e){let t=M(e);return A[t]+e.slice(t.length)}function De(e){return{top:e.top??0,right:e.right??0,bottom:e.bottom??0,left:e.left??0}}function Oe(e){return typeof e==`number`?{top:e,right:e,bottom:e,left:e}:De(e)}function ke(e){let{x:t,y:n,width:r,height:i}=e;return{width:r,height:i,top:n,left:t,right:t+r,bottom:n+i,x:t,y:n}}function Ae(e,t,n){let{reference:r,floating:i}=e,a=I(t),o=_e(t),s=F(o),c=M(t),l=a===`y`,u=r.x+r.width/2-i.width/2,d=r.y+r.height/2-i.height/2,f=r[s]/2-i[s]/2,p;switch(c){case`top`:p={x:u,y:r.y-i.height};break;case`bottom`:p={x:u,y:r.y+r.height};break;case`right`:p={x:r.x+r.width,y:d};break;case`left`:p={x:r.x-i.width,y:d};break;default:p={x:r.x,y:r.y}}let m=N(t);return m&&(p[o]+=f*(m===`end`?1:-1)*(n&&l?-1:1)),p}async function je(e,t){t===void 0&&(t={});let{x:n,y:r,platform:i,rects:a,elements:o,strategy:s}=e,{boundary:c=`clippingAncestors`,rootBoundary:l=`viewport`,elementContext:u=`floating`,altBoundary:d=!1,padding:f=0}=ge(t,e),p=Oe(f),m=o[d?u===`floating`?`reference`:`floating`:u],h=ke(await i.getClippingRect({element:await(i.isElement==null?void 0:i.isElement(m))??!0?m:m.contextElement||await(i.getDocumentElement==null?void 0:i.getDocumentElement(o.floating)),boundary:c,rootBoundary:l,strategy:s})),g=u===`floating`?{x:n,y:r,width:a.floating.width,height:a.floating.height}:a.reference,_=await(i.getOffsetParent==null?void 0:i.getOffsetParent(o.floating)),v=await(i.isElement==null?void 0:i.isElement(_))&&await(i.getScale==null?void 0:i.getScale(_))||{x:1,y:1},y=ke(i.convertOffsetParentRelativeRectToViewportRelativeRect?await i.convertOffsetParentRelativeRectToViewportRelativeRect({elements:o,rect:g,offsetParent:_,strategy:s}):g);return{top:(h.top-y.top+p.top)/v.y,bottom:(y.bottom-h.bottom+p.bottom)/v.y,left:(h.left-y.left+p.left)/v.x,right:(y.right-h.right+p.right)/v.x}}var Me=50,Ne=async(e,t,n)=>{let{placement:r=`bottom`,strategy:i=`absolute`,middleware:a=[],platform:o}=n,s=o.detectOverflow?o:{...o,detectOverflow:je},c=await(o.isRTL==null?void 0:o.isRTL(t)),l=await o.getElementRects({reference:e,floating:t,strategy:i}),{x:u,y:d}=Ae(l,r,c),f=r,p=0,m={};for(let n=0;n<a.length;n++){let h=a[n];if(!h)continue;let{name:g,fn:_}=h,{x:v,y,data:b,reset:x}=await _({x:u,y:d,initialPlacement:r,placement:f,strategy:i,middlewareData:m,rects:l,platform:s,elements:{reference:e,floating:t}});u=v??u,d=y??d,m[g]={...m[g],...b},x&&p<Me&&(p++,typeof x==`object`&&(x.placement&&(f=x.placement),x.rects&&(l=x.rects===!0?await o.getElementRects({reference:e,floating:t,strategy:i}):x.rects),{x:u,y:d}=Ae(l,f,c)),n=-1)}return{x:u,y:d,placement:f,strategy:i,middlewareData:m}},Pe=function(e){return e===void 0&&(e={}),{name:`flip`,options:e,async fn(t){var n;let{placement:r,middlewareData:i,rects:a,initialPlacement:o,platform:s,elements:c}=t,{mainAxis:l=!0,crossAxis:u=!0,fallbackPlacements:d,fallbackStrategy:f=`bestFit`,fallbackAxisSideDirection:p=`none`,flipAlignment:m=!0,...h}=ge(e,t);if((n=i.arrow)!=null&&n.alignmentOffset)return{};let g=M(r),_=I(o),v=M(o)===o,y=await(s.isRTL==null?void 0:s.isRTL(c.floating)),b=d||(v||!m?[Ee(o)]:ye(o)),x=p!==`none`;!d&&x&&b.push(...Te(o,m,p,y));let ee=[o,...b],te=await s.detectOverflow(t,h),ne=[],S=i.flip?.overflows||[];if(l&&ne.push(te[g]),u){let e=ve(r,a,y);ne.push(te[e[0]],te[e[1]])}if(S=[...S,{placement:r,overflows:ne}],!ne.every(e=>e<=0)){let e=(i.flip?.index||0)+1,t=ee[e];if(t&&(u!==`alignment`||_===I(t)||S.every(e=>I(e.placement)!==_||e.overflows[0]>0)))return{daLine truncated
3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0
1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502
0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0
0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"></path></svg>`);function Nt(e){var t=Mt();C(e,t)}var Q=f(`<svg width="100%" height="100%" viewBox="-2 -2 50 50" focusable="false" aria-hidden="true" role="presentation"><path fill="currentColor" d="M34.923,37.251L24,26.328L13.077,37.251L9.436,33.61l10.923-10.923L9.436,11.765l3.641-3.641L24,19.047L34.923,8.124
l3.641,3.641L27.641,22.688L38.564,33.61L34.923,37.251z"></path></svg>`);function Pt(e){var t=Q();C(e,t)}var Ft=f(`<svg class="loading" viewBox="25 25 50 50"><circle class="circle_path" cx="50" cy="50" r="20" fill="none" stroke="currentColor" stroke-width="5" stroke-miterlimit="10"></circle></svg>`);function It(e){var t=Ft();C(e,t)}var Lt=d(`<div class="list-item" tabindex="-1" role="none"><div><!></div></div>`),Rt=d(`<div class="empty">No options</div>`),zt=d(`<div role="none"><!> <!> <!></div>`),Bt=d(`<span id="aria-selection"> </span> <span id="aria-context"> </span>`,1),Vt=d(`<div class="multi-item-clear"><!></div>`),Ht=d(`<div role="none"><span class="multi-item-text"><!></span> <!></div>`),Ut=d(`<div><!></div>`),Wt=d(`<div class="icon loading" aria-hidden="true"><!></div>`),Gt=d(`<button type="button" class="icon clear-select"><!></button>`),Kt=d(`<div class="icon chevron" aria-hidden="true"><!></div>`),qt=d(`<input type="hidden"/>`),Jt=d(`<select class="required" required="" tabindex="-1" aria-hidden="true"></select>`),Yt=d(`<div role="none"><!> <span aria-live="polite" aria-atomic="false" aria-relevant="additions text" class="a11y-text"><!></span> <div class="prepend"><!></div> <div class="value-container"><!> <input/></div> <div class="indicators"><!> <!> <!></div> <!> <!></div>`);function Xt(o,s){ie(s,!0);let d,f=b(s,`valueMode`,3,`item`),pe=b(s,`filter`,3,At),O=b(s,`getItems`,3,jt),me=b(s,`id`,3,null),he=b(s,`name`,3,null),k=b(s,`container`,15,null),A=b(s,`input`,15,null),j=b(s,`multiple`,3,!1),ge=b(s,`multiFullItemClearable`,3,!1),M=b(s,`disabled`,3,!1),N=b(s,`focused`,15,!1),P=b(s,`value`,15),F=b(s,`filterText`,15,``),I=b(s,`placeholder`,3,`Please select`),_e=b(s,`placeholderAlwaysShow`,3,!1),ve=b(s,`items`,31,()=>S([])),ye=b(s,`label`,3,`label`),be=b(s,`itemFilter`,3,(e,t,n)=>`${e}`.toLowerCase().includes(t.toLowerCase())),xe=b(s,`groupBy`,3,void 0),Se=b(s,`groupFilter`,3,e=>e),Ce=b(s,`groupHeaderSelectable`,3,!1),L=b(s,`itemId`,3,`value`),we=b(s,`loadOptions`,3,void 0),Te=b(s,`containerStyles`,3,``),Ee=b(s,`hasError`,3,!1),De=b(s,`filterSelectedItems`,3,!0),Oe=b(s,`required`,3,!1),ke=b(s,`closeListOnChange`,3,!0),Ae=b(s,`clearFilterTextOnBlur`,3,!0),je=b(s,`createGroupHeaderItem`,3,(e,t)=>({value:e,[ye()]:e})),Me=b(s,`searchable`,3,!0),Ne=b(s,`inputStyles`,3,``),Pe=b(s,`clearable`,3,!0),Fe=b(s,`loading`,15,!1),R=b(s,`listOpen`,15,!1),Ie=b(s,`debounce`,3,(e,t=1)=>{clearTimeout(d),d=setTimeout(e,t)}),Le=b(s,`debounceWait`,3,300),Re=b(s,`hideEmptyState`,3,!1),z=b(s,`inputAttributes`,19,()=>({})),B=b(s,`listAutoWidth`,3,!0),V=b(s,`showChevron`,3,!1),ze=b(s,`listOffset`,3,5),H=b(s,`hoverItemIndex`,15,0),U=b(s,`floatingConfig`,19,()=>({})),Be=b(s,`class`,3,``),Ve=b(s,`ariaValues`,3,e=>`Option ${e}, selected.`),He=b(s,`ariaListOpen`,3,(e,t)=>`You are currently focused on option ${e}. There are ${t} results available.`),Ue=b(s,`ariaFocused`,3,()=>`Select is focused, type to refine list, press down to open the menu.`);function We(){return n(Z)}let W=oe(void 0),G,Ge,Ke,qe,K=oe(null),q={},J=x(()=>{if(z()||!Me()){let e=Object.assign({autocapitalize:`none`,autocomplete:`off`,autocorrect:`off`,spellcheck:!1,tabindex:0,type:`text`,"aria-autocomplete":`list`},z());me()&&(e.id=me()),Me()||(e.readonly=!0),JSON.stringify(e)!==JSON.stringify(q)&&(q=e)}return q});function Y(e){return e==null?e:typeof e==`object`?e[L()]:e}function Je(e){return Array.isArray(e)&&e.length>0&&typeof e[0]!=`object`}let Ye=x(()=>f()===`id`||Je(ve()));function Xe(e){return n(Ye)?Y(e):e}function Ze(e){if(e==null)return e;if(typeof e==`object`)return e[ye()];let t=ve()?.find(t=>t[L()]===e);return t?t[ye()]:e}function Qe(){return P()==null?null:n(Ye)?j()&&Array.isArray(P())?JSON.stringify(P()):`${P()}`:JSON.stringify(P())}function $e(e){return e.map((e,t)=>({index:t,value:e,label:`${e}`}))}function et(e){if(!xe())return e;let t=[],n={};e.forEach(e=>{let r=xe()(e)??``;t.includes(r)||(t.push(r),n[r]=[],r&&n[r].push(Object.assign(je()(r,e),{id:r,groupHeader:!0,selectable:Ce()}))),n[r].push(Object.assign({groupItem:!!r},e))});let r=[];return Se()(t).forEach(e=>{n[e]&&r.push(...n[e])}),r}function tt(){if(j()){JSON.stringify(P())!==JSON.stringify(G)&&(!P()||_t())&&s.oninput?.(P());return}P()?(!G||JSON.stringify(Y(P()))!==JSON.stringify(Y(G)))&&s.oninput?.(P()):G&&s.oninput?.(P())}function nt(){Ke=!0,P()&&(Array.isArray(P())||P([P()]))}function rt(){Ke=!1,P()&&P(void 0)}function X(){it(n(Z).findIndex(e=>e[L()]===Y(P())),!0)}function it(e=0,t){let r=e<0?0:e;H()!==r&&H(r),!t&&xe()&&n(Z)[H()]&&!n(Z)[H()].selectable&&tn(1)}function at(){!we()&&F().length===0||(we()?Ie()(async function(){Fe(!0);let e=await O()({onerror:s.onerror,onloaded:s.onloaded,loadOptions:we(),convertStringItemsToObjects:$e,filterText:F()});e?(Fe(e.loading),R(R()?e.listOpen:F().length>0),N(R()&&e.focused),ve(xe()?et(e.filteredItems):e.filteredItems)):(Fe(!1),N(!0),R(!0))},Le()):(R()||R(!0),j()&&T(W,void 0)))}let ot=x(()=>j()?Array.isArray(P())&&P().length>0:!!P()),st=x(()=>n(Line truncated
@@ -0,0 +1,2 @@
import{et as e}from"./DGrvnoUE.js";import{t}from"./BZuNOv8h.js";import"./CY9OlB4q.js";var n=e({user:null,status:`loading`}),r=()=>{localStorage.removeItem(`accessToken`),n.status=`unauthenticated`},i=()=>localStorage.getItem(`accessToken`),a=e=>{localStorage.setItem(`accessToken`,e)},o=`/api`,s=class extends Error{status;body;constructor(e,t){super(`API Error ${e}`),this.status=e,this.body=t}};async function c(e,n={}){let{auth:a=!0,headers:c,...l}=n,d={"Content-Type":`application/json`,...c},f=new Headers(d);if(a){let e=i();e&&f.append(`Authorization`,`Bearer ${e}`)}let p=await fetch(`${o}${e}`,{...l,headers:f});if(p.status===401&&(r(),t(`/login`)),!p.ok)throw new s(p.status,await u(p));if(p.status!==204)return p.json()}async function*l(e,n={}){let{auth:a=!0,headers:s,...c}=n,l={"Content-Type":`application/json`,...s},u=new Headers(l);if(a){let e=i();e&&u.append(`Authorization`,`Bearer ${e}`)}let d=await fetch(`${o}${e}`,{...c,headers:u});if(d.status===401&&(r(),t(`/login`)),!d.ok||!d.body)throw Error(`HTTP ${d.status}`);let f=d.body.pipeThrough(new TextDecoderStream).getReader(),p=``;try{for(;;){let{value:e,done:t}=await f.read();if(t)break;p+=e;let n=p.split(`
`);p=n.pop()??``;for(let e of n)e.trim()&&(yield JSON.parse(e))}p.trim()&&(yield JSON.parse(p))}finally{f.releaseLock()}}async function u(e){try{return await e.json()}catch{return null}}var d={get:(e,t)=>c(e,{...t,method:`GET`}),post:(e,t,n)=>c(e,{...n,method:`POST`,body:JSON.stringify(t)}),postStream:(e,t,n)=>l(e,{...n,method:`POST`,body:JSON.stringify(t)}),put:(e,t,n)=>c(e,{...n,method:`PUT`,body:JSON.stringify(t)}),delete:(e,t)=>c(e,{...t,method:`DELETE`})};export{a as i,n,r,d as t};
@@ -1,3 +0,0 @@
var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],enumerable:!0});return n||e(r,Symbol.toStringTag,{value:`Module`}),r},n=Array.isArray,r=Array.prototype.indexOf,i=Array.prototype.includes,a=Array.from,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyDescriptors,l=Object.prototype,u=Array.prototype,d=Object.getPrototypeOf,f=Object.isExtensible,p=()=>{};function m(e){for(var t=0;t<e.length;t++)e[t]()}function h(){var e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}var g=1024,_=2048,v=4096,y=8192,ee=16384,te=32768,ne=1<<25,re=65536,ie=1<<18,ae=1<<19,oe=1<<20,se=65536,ce=1<<21,le=1<<22,ue=1<<23,de=Symbol(`$state`),fe=Symbol(`legacy props`),pe=Symbol(``),me=Symbol(`proxy path`),he=Symbol(`attributes`),ge=Symbol(`class`),_e=Symbol(`style`),ve=Symbol(`text`),ye=Symbol(`form reset`),be=new class extends Error{name=`StaleReactionError`;message="The reaction that called `getAbortSignal()` was re-run or destroyed"},xe=!!globalThis.document?.contentType&&globalThis.document.contentType.includes(`xml`);function Se(e){throw Error(`https://svelte.dev/e/experimental_async_required`)}function Ce(e){throw Error(`https://svelte.dev/e/lifecycle_outside_component`)}function we(){throw Error(`https://svelte.dev/e/missing_context`)}function Te(){throw Error(`https://svelte.dev/e/async_derived_orphan`)}function Ee(e){throw Error(`https://svelte.dev/e/effect_in_teardown`)}function De(){throw Error(`https://svelte.dev/e/effect_in_unowned_derived`)}function Oe(e){throw Error(`https://svelte.dev/e/effect_orphan`)}function ke(){throw Error(`https://svelte.dev/e/effect_update_depth_exceeded`)}function Ae(){throw Error(`https://svelte.dev/e/fork_discarded`)}function je(){throw Error(`https://svelte.dev/e/fork_timing`)}function Me(){throw Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`)}function Ne(){throw Error(`https://svelte.dev/e/hydration_failed`)}function Pe(e){throw Error(`https://svelte.dev/e/lifecycle_legacy_only`)}function Fe(e){throw Error(`https://svelte.dev/e/props_invalid_value`)}function Ie(){throw Error(`https://svelte.dev/e/state_descriptors_fixed`)}function Le(){throw Error(`https://svelte.dev/e/state_prototype_fixed`)}function Re(){throw Error(`https://svelte.dev/e/state_unsafe_mutation`)}function ze(){throw Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`)}var Be={},b=Symbol(`uninitialized`),Ve=`http://www.w3.org/1999/xhtml`;function He(){console.warn(`https://svelte.dev/e/derived_inert`)}function Ue(e){console.warn(`https://svelte.dev/e/hydratable_missing_but_expected`)}function We(e){console.warn(`https://svelte.dev/e/hydration_mismatch`)}function Ge(){console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`)}var x=!1;function S(e){x=e}var C;function w(e){if(e===null)throw We(),Be;return C=e}function Ke(){return w(I(C))}function qe(e){if(x){if(I(C)!==null)throw We(),Be;C=e}}function Je(e=1){if(x){for(var t=e,n=C;t--;)n=I(n);C=n}}function Ye(e=!0){for(var t=0,n=C;;){if(n.nodeType===8){var r=n.data;if(r===`]`){if(t===0)return n;--t}else(r===`[`||r===`[!`||r[0]===`[`&&!isNaN(Number(r.slice(1))))&&(t+=1)}var i=I(n);e&&n.remove(),n=i}}function Xe(e){if(!e||e.nodeType!==8)throw We(),Be;return e.data}function Ze(e){return e===this.v}function Qe(e,t){return e==e?e!==t||typeof e==`object`&&!!e||typeof e==`function`:t==t}function $e(e){return!Qe(e,this.v)}function et(e,t){return e.label=t,tt(e.v,t),e}function tt(e,t){return e?.[me]?.(t),e}function nt(e){return typeof e==`symbol`?`Symbol(${e.description})`:typeof e==`function`?`<function>`:typeof e==`object`&&e?`<object>`:String(e)}var T=null;function rt(e){T=e}function it(){let e={};return[()=>(st(e)||we(),at(e)),t=>ot(e,t)]}function at(e){return ft(`getContext`).get(e)}function ot(e,t){return ft(`setContext`).set(e,t),t}function st(e){return ft(`hasContext`).has(e)}function ct(){return ft(`getAllContexts`)}function lt(e,t=!1,n){T={p:T,i:!1,c:null,e:null,s:e,x:null,r:W,l:null}}function ut(e){var t=T,n=t.e;if(n!==null){t.e=null;for(var r of n)Un(r)}return e!==void 0&&(t.x=e),t.i=!0,T=t.p,e??{}}function dt(){return!0}function ft(e){return T===null&&Ce(e),T.c??=new Map(pt(T)||void 0)}function pt(e){let t=e.p;for(;t!==null;){let e=t.c;if(e!==null)return e;t=t.p}return null}var mt=[];function ht(){var e=mt;mt=[],m(e)}function E(e){if(mt.length===0&&!Qt){var t=mt;queueMicrotask(()=>{t===mt&&ht()})}mt.push(e)}function gt(){for(;mt.length>0;)ht()}function _t(e){var t=W;if(t===null)return V.f|=ue,e;if(!(t.f&32768)&&!(t.f&4))throw e;D(e,t)}function D(e,t){if(!(t!==null&&t.f&16384)){for(;t!==null;){if(t.f&128){if(!(t.f&32768))throw e;try{t.b.error(e);return}catch(t){e=t}}t=t.parent}throw e}}var vt=~(_|v|g);function O(e,t){e.f=e.f&vt|t}function yt(e){e.f&512||e.deps===null?O(e,g):O(e,v)}function bt(e){if(e!==null)for(let t of e)!(t.f&2)||!(t.f&65536)||(t.f^=se,bt(t.deps))}function xt(e,t,n){e.f&2048?t.add(e):e.f&4096&&n.add(e),bt(e.deps),O(e,g)}var St=[];function Ct(e,t=p){letLine truncated
`).some(e=>e.startsWith(`https://svelte.dev/e/`)))throw r;return r!==Be&&console.warn(`Failed to hydrate: `,r),t.recover===!1&&Ne(),An(),Pn(n),S(!1),Gr(e,t)}finally{S(r),w(i)}}var qr=new Map;function Jr(e,{target:t,anchor:n,props:r={},events:i,context:o,intro:s=!0,transformError:c}){An();var l=void 0,u=Gn(()=>{var s=n??t.appendChild(P());Nt(s,{pending:()=>{}},t=>{lt({});var n=T;if(o&&(n.c=o),i&&(r.$$events=i),x&&$(t,null),l=e(t,r)||{},x&&(W.nodes.end=C,C===null||C.nodeType!==8||C.data!==`]`))throw We(),Be;ut()},c);var u=new Set,d=e=>{for(var n=0;n<e.length;n++){var r=e[n];if(!u.has(r)){u.add(r);var i=Dr(r);for(let e of[t,document]){var a=qr.get(e);a===void 0&&(a=new Map,qr.set(e,a));var o=a.get(r);o===void 0?(e.addEventListener(r,Ir,{passive:i}),a.set(r,1)):a.set(r,o+1)}}}};return d(a(kr)),Ar.add(d),()=>{for(var e of u)for(let n of[t,document]){var r=qr.get(n),i=r.get(e);--i==0?(n.removeEventListener(e,Ir),r.delete(e),r.size===0&&qr.delete(n)):r.set(e,i)}Ar.delete(d),s!==n&&s.parentNode?.removeChild(s)}});return Yr.set(l,u),l}var Yr=new WeakMap;function Xr(e,t){let n=Yr.get(e);return n?(Yr.delete(e),n(t)):Promise.resolve()}var Zr=class{anchor;#e=new Map;#t=new Map;#n=new Map;#r=new Set;#i=!0;constructor(e,t=!0){this.anchor=e,this.#i=t}#a=e=>{if(this.#e.has(e)){var t=this.#e.get(e),n=this.#t.get(t);if(n)ir(n),this.#r.delete(t);else{var r=this.#n.get(t);r&&(ir(r.effect),this.#t.set(t,r.effect),this.#n.delete(t),r.fragment.lastChild.remove(),this.anchor.before(r.fragment),n=r.effect)}for(let[t,n]of this.#e){if(this.#e.delete(t),t===e)break;let r=this.#n.get(n);r&&(z(r.effect),this.#n.delete(n))}for(let[e,r]of this.#t){if(e===t||this.#r.has(e))continue;let i=()=>{if(Array.from(this.#e.values()).includes(e)){var t=document.createDocumentFragment();or(r,t),t.append(P()),this.#n.set(e,{effect:r,fragment:t})}else z(r);this.#r.delete(e),this.#t.delete(e)};this.#i||!n?(this.#r.add(e),nr(r,i,!1)):i()}}};#o=e=>{this.#e.delete(e);let t=Array.from(this.#e.values());for(let[e,n]of this.#n)t.includes(e)||(z(n.effect),this.#n.delete(e))};ensure(e,t){var n=k,r=Fn();if(t&&!this.#t.has(e)&&!this.#n.has(e))if(r){var i=document.createDocumentFragment(),a=P();i.append(a),this.#n.set(e,{effect:R(()=>t(a)),fragment:i})}else this.#t.set(e,R(()=>t(this.anchor)));if(this.#e.set(n,e),r){for(let[t,r]of this.#t)t===e?n.unskip_effect(r):n.skip_effect(r);for(let[t,r]of this.#n)t===e?n.unskip_effect(r.effect):n.skip_effect(r.effect);n.oncommit(this.#a),n.ondiscard(this.#o)}else x&&(this.anchor=C),this.#a(n)}};function Qr(e,t,n=!1){var r;x&&(r=C,Ke());var i=new Zr(e),a=n?re:0;function o(e,t){if(x){var n=Xe(r);if(e!==parseInt(n.substring(1))){var a=Ye();w(a),i.anchor=a,S(!1),i.ensure(e,t),S(!0);return}}i.ensure(e,t)}Xn(()=>{var e=!1;t((t,n=0)=>{e=!0,o(n,t)}),e||o(-1,null)},a)}function $r(e,t,...n){var r=new Zr(e);Xn(()=>{let e=t()??null;r.ensure(e,e&&(t=>e(t,...n)))},re)}function ei(e){return(t,...n)=>{var r=e(...n),i;x?(i=C,Ke()):(i=F(zr(r.render().trim())),t.before(i));let a=r.setup?.(i);$(i,i),typeof a==`function`&&Vn(a)}}function ti(e,t,n){var r;x&&(r=C,Ke());var i=new Zr(e);Xn(()=>{var e=t()??null;if(x&&Xe(r)===`[`!=(e!==null)){var a=Ye();w(a),i.anchor=a,S(!1),i.ensure(e,e&&(t=>n(t,e))),S(!0);return}i.ensure(e,e&&(t=>n(t,e)))},re)}function ni(e,t){let n=null,r=x;var i;if(x){n=C;for(var a=F(document.head);a!==null&&(a.nodeType!==8||a.data!==e);)a=I(a);if(a===null)S(!1);else{var o=I(a);a.remove(),w(o)}}x||(i=document.head.appendChild(P()));try{Xn(()=>{var e=R(()=>t(i));e.f|=ie})}finally{r&&(S(!0),w(n))}}function ri(e){var t,n,r=``;if(typeof e==`string`||typeof e==`number`)r+=e;else if(typeof e==`object`)if(Array.isArray(e)){var i=e.length;for(t=0;t<i;t++)e[t]&&(n=ri(e[t]))&&(r&&(r+=` `),r+=n)}else for(n in e)e[n]&&(r&&(r+=` `),r+=n);return r}function ii(){for(var e,t,n=0,r=``,i=arguments.length;n<i;n++)(e=arguments[n])&&(t=ri(e))&&(r&&(r+=` `),r+=t);return r}function ai(e){return typeof e==`object`?ii(e):e??``}var oi=[...`
\r\f\xA0\v`];function si(e,t,n){var r=e==null?``:``+e;if(t&&(r=r?r+` `+t:t),n){for(var i of Object.keys(n))if(n[i])r=r?r+` `+i:i;else if(r.length)for(var a=i.length,o=0;(o=r.indexOf(i,o))>=0;){var s=o+a;(o===0||oi.includes(r[o-1]))&&(s===r.length||oi.includes(r[s]))?r=(o===0?``:r.substring(0,o))+r.substring(s+1):o=s}}return r===``?null:r}function ci(e,t,n,r,i,a){var o=e[ge];if(x||o!==n||o===void 0){var s=si(n,r,a);(!x||s!==e.getAttribute(`class`))&&(s==null?e.removeAttribute(`class`):t?e.className=s:e.setAttribute(`class`,s)),e[ge]=n}else if(a&&i!==a)for(var c in a){var l=!!a[c];(i==null||l!==!!i[c])&&e.classList.toggle(c,l)}return a}var li=Symbol(`is custom element`),ui=Symbol(`is html`),di=xe?`link`:`LINK`;function fi(e){if(x){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute(`value`)){var n=e.value;pi(e,`value`,null),e.value=n}if(e.hasAttribute(`checked`)){var r=e.checked;pi(e,`checked`,null),e.checked=r}}};e[ye]=n,E(n),Ot()}}function pi(e,t,n,r){var i=mi(e);x&&(i[t]=e.getAttribute(t),t===`src`||t===`srcset`||t===`href`&&e.nodeName===di)||i[t]!==(i[t]=n)&&(t===`loading`&&(e[pe]=n),n==null?e.removeAttribute(t):typeof n!=`string`&&gi(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function mi(e){return e[he]??={[li]:e.nodeName.includes(`-`),[ui]:e.namespaceURI===Ve}}var hi=new Map;function gi(e){var t=e.getAttribute(`is`)||e.nodeName,n=hi.get(t);if(n)return n;hi.set(t,n=[]);for(var r,i=e,a=Element.prototype;a!==i;){for(var o in r=c(i),r)r[o].set&&o!==`innerHTML`&&o!==`textContent`&&o!==`innerText`&&n.push(o);i=d(i)}return n}function _i(e,t,n=t){var r=new WeakSet;At(e,`input`,async i=>{var a=i?e.defaultValue:e.value;if(a=vi(e)?yi(a):a,n(a),k!==null&&r.add(k),await Sr(),a!==(a=t())){var o=e.selectionStart,s=e.selectionEnd,c=e.value.length;if(e.value=a??``,s!==null){var l=e.value.length;o===s&&s===c&&l>c?(e.selectionStart=l,e.selectionEnd=l):(e.selectionStart=o,e.selectionEnd=Math.min(s,l))}}}),(x&&e.defaultValue!==e.value||Q(t)==null&&e.value)&&(n(vi(e)?yi(e.value):e.value),k!==null&&r.add(k)),Jn(()=>{var n=t();if(e===document.activeElement){var i=k;if(r.has(i))return}vi(e)&&n===yi(e.value)||e.type===`date`&&!n&&!e.value||n!==e.value&&(e.value=n??``)})}function vi(e){var t=e.type;return t===`number`||t===`range`}function yi(e){return e===``?null:+e}function bi(e,t){return e===t||e?.[de]===t}function xi(e={},t,n,r){var i=T.r,a=W;return Kn(()=>{var o,s;return Jn(()=>{o=s,s=r?.()||[],Q(()=>{bi(n(...s),e)||(t(e,...s),o&&bi(n(...o),e)&&t(null,...o))})}),()=>{let r=a;for(;r!==i&&r.parent!==null&&r.parent.f&33554432;)r=r.parent;let o=()=>{s&&bi(n(...s),e)&&t(null,...s)},c=r.teardown;r.teardown=()=>{o(),c?.()}}}),e}function Si(e,t,n,r){var i=!0,a=!!(n&8),o=!!(n&16),c=r,l=!0,u=void 0,d=()=>o&&i?(u??=zt(r),Z(u)):(l&&(l=!1,c=o?Q(r):r),c);let f;if(a){var p=de in e||fe in e;f=s(e,t)?.set??(p&&t in e?n=>e[t]=n:void 0)}var m,h=!1;a?[m,h]=Tt(()=>e[t]):m=e[t],m===void 0&&r!==void 0&&(m=d(),f&&(i&&Fe(t),f(m)));var g=i?()=>{var n=e[t];return n===void 0?d():(l=!0,n)}:()=>{var n=e[t];return n!==void 0&&(c=void 0),n===void 0?c:n};if(i&&!(n&4))return g;if(f){var _=e.$$legacy;return(function(e,t){return arguments.length>0?((!i||!t||_||h)&&f(t?g():e),e):g()})}var v=!1,y=(n&1?zt:Ut)(()=>(v=!1,g()));a&&Z(y);var ee=W;return(function(e,t){if(arguments.length>0){let n=t?Z(y):i&&a?wn(e):e;return N(y,n),v=!0,c!==void 0&&(c=n),e}return B&&v||ee.f&16384?y.v:Z(y)})}function Ci(e){return class extends wi{constructor(t){super({component:e,...t})}}}var wi=class{#e;#t;constructor(e){var t=new Map,n=(e,n)=>{var r=yn(n,!1,!1);return t.set(e,r),r};let r=new Proxy({...e.props||{},$$events:{}},{get(e,r){return Z(t.get(r)??n(r,Reflect.get(e,r)))},has(e,r){return r===fe||(Z(t.get(r)??n(r,Reflect.get(e,r))),Reflect.has(e,r))},set(e,r,i){return N(t.get(r)??n(r,i),i),Reflect.set(e,r,i)}});this.#t=(e.hydrate?Kr:Gr)(e.component,{target:e.target,anchor:e.anchor,props:r,context:e.context,intro:e.intro??!1,recover:e.recover,transformError:e.transformError}),(!e?.props?.$$host||e.sync===!1)&&on(),this.#e=r.$$events;for(let e of Object.keys(this.#t))e!==`$set`&&e!==`$destroy`&&e!==`$on`&&o(this,e,{get(){return this.#t[e]},set(t){this.#t[e]=t},enumerable:!0});this.#t.$set=e=>{Object.assign(r,e)},this.#t.$destroy=()=>{Xr(this.#t)}}$set(e){this.#t.$set(e)}$on(e,t){this.#e[e]=this.#e[e]||[];let n=(...e)=>t.call(this,...e);return this.#e[e].push(n),()=>{this.#e[e]=this.#e[e].filter(e=>e!==n)}}$destroy(){this.#t.$destroy()}};function Ti(e,t){if(Se(`hydratable`),x){let t=window.__svelte?.h;if(t?.has(e))return t.get(e);Ue(e)}return t()}var Ei=t({afterUpdate:()=>Ni,beforeUpdate:()=>Mi,createContext:()=>it,createEventDispatcher:()=>ji,createRawSnippet:()=>ei,flushSync:()=>on,fork:()=>pn,getAbortSignal:()=>Di,getAllContexts:()=>ct,getContext:()=>at,hasContext:()=>st,hydratable:()=>Ti,hydrate:()=>Kr,mount:()=>Gr,onDestroy:()=>ki,onMount:()=>Oi,setContext:()=>ot,settled:()=>Cr,tick:()=>Sr,unmount:()=>Xr,untrack:()=>Q});function Di(){return V===null&&Me(),(V.ac??=new AbortControlle Line truncated
@@ -1 +0,0 @@
import{n as e}from"./puscbPt7.js";var t={register:t=>e.post(`/auth/register`,t),status:()=>e.get(`/auth/status`),login:t=>e.post(`/auth/login`,t)};export{t};
@@ -0,0 +1 @@
import"./DGrvnoUE.js";import{i as e,o as t}from"./BZuNOv8h.js";var n={get data(){return t.data},get error(){return t.error},get form(){return t.form},get params(){return t.params},get route(){return t.route},get state(){return t.state},get status(){return t.status},get url(){return t.url}};e.updated.check;var r=n;export{r as t};
@@ -1 +0,0 @@
import{L as e}from"./DxmhE1z1.js";import{t}from"./DZTG_XPZ.js";var n=e({user:null,status:`loading`}),r=()=>{localStorage.removeItem(`accessToken`),n.status=`unauthenticated`},i=()=>localStorage.getItem(`accessToken`),a=e=>{localStorage.setItem(`accessToken`,e)},o=`/api`,s=class extends Error{status;body;constructor(e,t){super(`API Error ${e}`),this.status=e,this.body=t}};async function c(e,n={}){let{auth:a=!0,headers:c,...u}=n,d={"Content-Type":`application/json`,...c},f=new Headers(d);if(a){let e=i();e&&f.append(`Authorization`,`Bearer ${e}`)}let p=await fetch(`${o}${e}`,{...u,headers:f});if(p.status===401&&(r(),t(`/login`)),!p.ok)throw new s(p.status,await l(p));if(p.status!==204)return p.json()}async function l(e){try{return await e.json()}catch{return null}}var u={get:(e,t)=>c(e,{...t,method:`GET`}),post:(e,t,n)=>c(e,{...n,method:`POST`,body:JSON.stringify(t)}),put:(e,t,n)=>c(e,{...n,method:`PUT`,body:JSON.stringify(t)}),delete:(e,t)=>c(e,{...t,method:`DELETE`})},d=`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAXoAAAA1CAYAAABP2SCdAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAClxJREFUeJzt3W2MXFUZB/D/c+6d7WK7c6e73btbWgSUlshrlQ8YAoKQIIkhBk2lO01rEz6YUrTxJTRqsKyaygfxLUgUTKi4c7dQEU2IiIIaRAOUtxZTlZgUQdqdu9vuzvR9Z+Y8fmi7vXdmdu/c05md5fL8PvWePec8596982x67pxziZkhhBAiuVS7ByCEEKK17FP/WDg0ch2T6pipMpOeKGT7X2z9sIRozPxtY2enKvqSYBlZ+o3xVf1vtmlIQsw5U4leK/UYgO6Zq6sXAVzZ2iEJ0ThL841MeChUyNbXANzTnhEJMffI1I0QQiScJHohhEg4SfRCCJFwU3P0RPg5M+ZPHYM6GXxbe4YlWiE9nL+ZWJ0bLFOTqaHxdZmJdo1JCNF6U4l+YsDdFPxB1/aRXlVSkugThDTdDuKbgmWVeZWnAUiiFyLBZOpGCCESThK9EEIknCR6IYRIODu6ikgKTXiQgGeCZWQf9ds1HiHE7JBE/x5yMOv+ut1jEELMPpm6EUKIhJNEL4QQCSeJXgghEq6tc/SO598H0Pvjt+RXCln3bqOYudHvgHBZ/JZ6TyHbt9EkZtrzv0qgj8VvyeOFrPs5s5j52wjqU1H1SmW1/sjanndMYlTLDPufYSaj8arJ1NqoFbrpR8aWU4W/Fywj8DnV9VhjreONXhUZU1W+aLKdcWZo7COs+O647U7ExObxVb2vzlSHtu/uSJd6f2XSf92YqPxgPNv/57jtMl5hIWPyFyYxNXiryTMhAijtjf7WJGbd/pjvm1jt/iFYlh7ObyFWoa2trZS95sDKhYW4/XcNj12rmL8SikkYmhjofTSqrZPL/xikzosbE8DOQrb3rjgN2vswlul6EH/IoOU885C4isAfj90OtMs0JoGuAPhmg6Z545ikLgFHx7RTuNM0Rp2oywzPE6X5pc7I3suVbhBF93/inoq+r8oq1oflFLbYbeTa1qM1fhJdK22ZXse6MaGM/miUdKnTVmbjIMZzJu0wCMKy5p07CE/UFtHVAF8TLCsdmzTKKYr1UgCh8TJjR0NDI7qOwZfGjck4vVVNo2TqRgghEk4SvRBCJJwkeiGESLi2ztGT1p/Uth17boy4vMLx/H+FysAPT2T7tgTLnGH/m2Bkg2UKdJcm6/a4MS0qHWuknpMb3QDiL4THy/dqZQ/GjUkVXY7b5hRd1t8l2/5ZqD9UHgDjmunaxJHJ5W9gotB8M4MeA1kmz1xwGD2jUXWKpaOvdc3rCvWvtL6FibdUVf0+k/VgVH8TPeN7ADfmSIGzyvqvR2zb6DyJ9WbH838ULCsrdf3hVYv2ztiQ6Z+s1KfNYlY2Op7/9WCZRfjsgQE39NzJ8fI7AOo6dWwrHGDD3yeovLb6M6qg11c/FHZy/pMgnH/qOH0BaeOYXFlHwKboirPP8UZ/CIR3jgWwkcn6b9y+FPRl1dcWQK6Qdb89XZu2JvqJNX17TNo5nn8ugAuDZQz01VRk9FfXA+n9xYHe6ovUPMSLasamcKg40NO6mHUcXNPnAwhtb+Dk/MOg5vSvFXURV11bsNXK8+R15x0DEOo/7Y3uqz4lIhotNDSOHqNxjKzpO1w9jkY5nt+NqvujQ5dSkQ2Jj5teW8fz51fHrBCfVSfIcgDpqUPGSDFrFjOT822mqpj69B+R0yHxQQDLTh8zm55netj3wSYtW4+Jl1R/XizCvgMG55rJ5ZeAKPzZO5HrpiVTN0IIkXCS6IUQIuEk0QshRMK1e2XsPwBc1Kr+i1l3AwZxR7Csa5n/R8fznzbobmch6344qlIh6w7SIEIPRdIX+EOO5+cMYo4Usu7ZBu1arjjg/oYGYQXLnOWjdzqer036qxAWHxpwjReIzSbH8z8B4EnD5k16SiLe7SqM1xzPYJdwin8LtXtlrAJxy258Bhibw49n0h4RED8mx/jfD29GKNk5hjExx//HVX2emWEAponMNrh724WIwK27b8V7xqzdQ3M6kQghhDhzkuiFECLhZm3qJv3wOz2pDnpfqFDZY8z4X9y+COgEsMhkHAT4jPgxAdrfs21vaKfE0iQfKa5dsj+6Le83ikmoiTlpqWMHV/ZHLi5qBwYXza4tkDrOfT3b9k59n/zo5DzdrF01zwRth9Vd2Rt6TkI6tUCT2XnSifs2cgM3MTPa+mZnd2dHb6iM7UxLYz6AVHd6b/j76pTqBht/eT/PQOmMBwaAQOMz/XzWEr1KpR4sa9wSLNOsLz64un933L5OPgz7vck4CtneVSbtFg75l5dhvxUsI4sfBXBrdEz3DiD8ULgRC7aP9pdL9r5gmaXxLIBr4/Y1GwoD7v0A7jdp63j+iNb21KK3lF0pAGjpB7cRmcrIOWVthxf2ET9VzLo12yM3wvH8pwDc2IyxvZelOxfcUNa6ZmfKVnIWjF1W1vZL4VLzFVoW4cbq1cmtIlM3QgiRcJLohRAi4STRCyFEwk07R5/CvMkySn8KlhHwTiaXvyGqUyL9n/Hs4tCubMzYxYATLLNAl2Zy+cVxB02kVnADc2PpobELFVWWxu2/HrbgMlP4ehBer66X+WX+fCh8oBkxFWM+q3BMADub0fcU1h/N5PJLgkUTq/ueiWqWHt63SGl1edPGoegFZiw4dUiEQw01Y96rCeFrxNpu5D7t6FAv+Ct7Z4xTYnVUoap/wluN9F8XoXvOrpki/CX0O1A4ZHqeDDXJ4PB1s6j2SwSEvzPj7dOH4MZyDC3RVb8XAs5BcIO06Vu/zODQQ1CycWUmlz8yUysm6ufamH0ALo6MCLzOQHewrMJ8USaX752uTRzatt4u3rrojel+Pm2iP/n+xNAFd7blr2RNz0eHVd8AENo6tt47Xp3c6G6zVwk29gCElN7IoPXx+68TkbGrmHUjExtbtBbA3c2ISYR8IevOuCvdGccAP8S1a5UayETW1Ux4vFnjqNi0+NDK3pG47U6+DzT8TlDPvweEyNXPpeO8AhF/OE+u1g1/DoZHb+IG+n+3KQy4oXcMzx8aW2wrPfP2ydNg5k3FrBuZsAsD7rrgMQ1CpZf5lQYi/K6Y7Qv1nx72v0yMe6Nj9n6puszxfJ8JUUn3lWLWvaKq3WoAQ9Ex3W9Vl2U8fxcTYr9KsB4q6/sBbJju5zJ1I4QQCSeJXgghEk4SvRBCJFysBVMKnNegn0bVY9DLjXXI28CI/TC2HoJ6rs5InjXa6q0ORdzgSkjaAXDkNWoEA4Vm9HMaPQHwm83oyapgj1ZoynkCwLxJa8YHYbEQngdHj+24tsZMute68pYi1bRz16nKwVDB+NIyFvih/gn8Ngwx0zNEfDhYpsCRO4V2kjpchjY6T6Xwqkk7bAbDa+C+Iqr5IsSZIMZWJtS+ASsUkmpf+8f6DVTfC0wv1dSrH/QRMP0txjCn7wr87MyhzJfvCiHEnFHvYSwBn5/Iug+0a0xzhUzdCCFEwkmiF0KIhJNEL4QQCSeJXgghEk4SvRBCJJwkeiGESDhJ9EIIkXCS6IUQIuFm7VWCQgjRSqrMj3PK+newzMLkrLyqb66TlbFCCJFw/weRJ1LNZA9BxAAAAABJRU5ErkJggg==`;export{a,r as i,u as n,n as r,d as t};
@@ -0,0 +1 @@
import{t as e}from"./DtfiK5fE.js";var t={me:()=>e.get(`/user/me`),update:(t,n)=>e.put(`/user/${t}`,n),listAddresses:t=>e.get(`/user/${t}/address`),createAddress:(t,n)=>e.post(`/user/${t}/address`,n),updateAddress:(t,n)=>e.put(`/user/${t}/address`,n),deleteAddress:(t,n)=>e.delete(`/user/${t}/address/${n}`)};export{t};
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BLV5Ey1y.js","../chunks/DGrvnoUE.js","../chunks/xihTtKlq.js","../chunks/DTCALkcP.js","../assets/0.C0TpjoIE.css","../nodes/1.COgG0Fy8.js","../chunks/ffLBjIeN.js","../chunks/BZuNOv8h.js","../nodes/2.wASuVt48.js","../chunks/CY9OlB4q.js","../chunks/DtfiK5fE.js","../chunks/zsRwiIdn.js","../chunks/Cyrg28w2.js","../chunks/BRXBpvt8.js","../nodes/3.CHETzYMH.js","../nodes/4.DkQEi_Qd.js","../chunks/BYWIgY43.js","../chunks/BYYkuYgk.js","../nodes/5.BgmoPtaF.js","../chunks/B6JECYQD.js","../assets/5.nNRn7XR0.css","../nodes/6.B8E4TBNF.js","../chunks/Bht2nTIZ.js","../nodes/7.CJI6is1J.js","../nodes/8.Ds1ONmZq.js","../nodes/9.CYl-UmMc.js","../chunks/BeIYuf23.js","../chunks/6mSXxE57.js","../chunks/DdGy_ePN.js","../nodes/10.D6tOeXzG.js","../nodes/11.DtFjRc5K.js","../nodes/12.D1XIMXHX.js","../nodes/13.C-WGkE4P.js","../chunks/D3MDK1zR.js","../nodes/14.Zp7NcB4T.js","../nodes/15.DBuFCR43.js","../nodes/16.Bwbj6qfw.js","../nodes/17.BV0v_EXc.js","../nodes/18.e37B7XcZ.js","../nodes/19.CdvrW-UN.js","../chunks/BedKI9iq.js","../nodes/20.CPbrVrB7.js"])))=>i.map(i=>d[i]);
import{$ as e,A as t,B as n,F as r,H as i,J as a,K as o,M as s,N as c,Q as l,Z as u,a as d,at as f,c as p,dt as m,ft as h,i as g,it as _,j as v,k as y,nt as b,q as x,r as S,vt as C,w}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=c(`<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>`),j=c(`<!> <!>`,1);function M(c,g){h(g,!0);let T=d(g,`components`,23,()=>[]),E=d(g,`data_0`,3,null),D=d(g,`data_1`,3,null),O=d(g,`data_2`,3,null);a(()=>g.stores.page.set(g.page)),x(()=>{g.stores,g.page,g.constructors,T(),g.form,E(),D(),O(),g.stores.page.notify()});let k=_(!1),M=_(!1),N=_(null);S(()=>{let e=g.stores.page.subscribe(()=>{n(k)&&(b(M,!0),i().then(()=>{b(N,document.title||`untitled page`,!0)}))});return b(k,!0),e});let P=f(()=>g.constructors[2]);var F=j(),I=l(F),L=e=>{let t=f(()=>g.constructors[0]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return E()},get form(){return g.form},get params(){return g.page.params},children:(e,t)=>{var r=s(),i=l(r),a=e=>{let t=f(()=>g.constructors[1]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return D()},get form(){return g.form},get params(){return g.page.params},children:(e,t)=>{var r=s(),i=l(r);w(i,()=>n(P),(e,t)=>{p(t(e,{get data(){return O()},get form(){return g.form},get params(){return g.page.params}}),e=>T()[2]=e,()=>T()?.[2])}),v(e,r)},$$slots:{default:!0}}),e=>T()[1]=e,()=>T()?.[1])}),v(e,r)},o=e=>{let t=f(()=>g.constructors[1]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return D()},get form(){return g.form},get params(){return g.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),v(e,r)};y(i,e=>{g.constructors[2]?e(a):e(o,-1)}),v(e,r)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),v(e,r)},R=e=>{let t=f(()=>g.constructors[0]);var r=s(),i=l(r);w(i,()=>n(t),(e,t)=>{p(t(e,{get data(){return E()},get form(){return g.form},get params(){return g.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),v(e,r)};y(I,e=>{g.constructors[1]?e(L):e(R,-1)});var z=e(I,2),B=e=>{var i=A(),a=u(i),s=e=>{var i=r();o(()=>t(i,n(N))),v(e,i)};y(a,e=>{n(M)&&e(s)}),C(i),v(e,i)};y(z,e=>{n(k)&&e(B)}),v(c,F),m()}var N=g(M),P=[()=>O(()=>import(`../nodes/0.BLV5Ey1y.js`),__vite__mapDeps([0,1,2,3,4]),import.meta.url),()=>O(()=>import(`../nodes/1.COgG0Fy8.js`),__vite__mapDeps([5,1,2,6,7]),import.meta.url),()=>O(()=>import(`../nodes/2.wASuVt48.js`),__vite__mapDeps([8,1,7,2,3,6,9,10,11,12,13]),import.meta.url),()=>O(()=>import(`../nodes/3.CHETzYMH.js`),__vite__mapDeps([14,1,7,2,9,12]),import.meta.url),()=>O(()=>import(`../nodes/4.DkQEi_Qd.js`),__vite__mapDeps([15,1,7,2,9,16,17,10]),import.meta.url),()=>O(()=>import(`../nodes/5.BgmoPtaF.js`),__vite__mapDeps([18,1,2,6,7,16,17,10,9,19,20]),import.meta.url),()=>O(()=>import(`../nodes/6.B8E4TBNF.js`),__vite__mapDeps([21,1,7,2,9,17,10,22]),import.meta.url),()=>O(()=>import(`../nodes/7.CJI6is1J.js`),__vite__mapDeps([23,1,2]),import.meta.url),()=>O(()=>import(`../nodes/8.Ds1ONmZq.js`),__vite__mapDeps([24,1,2]),import.meta.url),()=>O(()=>import(`../nodes/9.CYl-UmMc.js`),__vite__mapDeps([25,1,2,10,7,9,12,26,27,28,19]),import.meta.url),()=>O(()=>import(`../nodes/10.D6tOeXzG.js`),__vite__mapDeps([29,1,2]),import.meta.url),()=>O(()=>import(`../nodes/11.DtFjRc5K.js`),__vite__mapDeps([30,1,2]),import.meta.url),()=>O(()=>import(`../nodes/12.D1XIMXHX.js`),__vite__mapDeps([31,1,2]),import.meta.url),()=>O(()=>import(`../nodes/13.C-WGkE4P.js`),__vite__mapDeps([32,1,2,26,10,7,9,27,28,19,33]),import.meta.url),()=>O(()=>import(`../nodes/14.Zp7NcB4T.js`),__vite__mapDeps([34,1,2]),import.meta.url),()=>O(()=>import(`../nodes/15.DBuFCR43.js`),__vite__mapDeps([35,1,2,10,7,9,27]),import.meta.url),()Line truncated
@@ -1,2 +0,0 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.Bz_yxCPk.js","../chunks/DxmhE1z1.js","../chunks/xihTtKlq.js","../assets/0.tR7PeoVK.css","../nodes/1.BXecr-g5.js","../chunks/BJ9updZG.js","../chunks/DZTG_XPZ.js","../nodes/2.Du7hfnuf.js","../chunks/puscbPt7.js","../nodes/3.BTi5lIWZ.js","../nodes/4.CGofJn_s.js","../nodes/5.DeDtZvF3.js","../nodes/6.Pd88j_py.js","../chunks/BT9KdPme.js","../nodes/7.Dq6C-eoP.js","../nodes/8.BW4HeEmf.js","../nodes/9.BK5CBBXk.js","../nodes/10.3_h0i6QI.js","../nodes/11.Q5ro3K8Z.js","../nodes/12.CqhEZfs2.js","../nodes/13.CyYsvdDt.js","../nodes/14.DZ-KMWs7.js","../nodes/15.CzF_DBRn.js","../nodes/16.DRUykhSC.js","../nodes/17.KofPYOuZ.js","../chunks/Nqp1gr6g.js","../nodes/18.3mUWXX_j.js"])))=>i.map(i=>d[i]);
import{A as e,C as t,F as n,H as r,I as i,K as a,P as o,Q as s,T as c,V as l,_ as u,a as d,f,g as p,h as m,i as h,j as g,k as _,m as v,n as y,q as b,r as x,v as S,y as C,z as w}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=S(`<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>`),j=S(`<!> <!>`,1);function M(x,S){b(S,!0);let T=h(S,`components`,23,()=>[]),E=h(S,`data_0`,3,null),D=h(S,`data_1`,3,null),O=h(S,`data_2`,3,null);g(()=>S.stores.page.set(S.page)),e(()=>{S.stores,S.page,S.constructors,T(),S.form,E(),D(),O(),S.stores.page.notify()});let k=l(!1),M=l(!1),N=l(null);y(()=>{let e=S.stores.page.subscribe(()=>{t(k)&&(w(M,!0),c().then(()=>{w(N,document.title||`untitled page`,!0)}))});return w(k,!0),e});let P=r(()=>S.constructors[2]);var F=j(),I=n(F),L=e=>{let i=r(()=>S.constructors[0]);var a=u(),o=n(a);f(o,()=>t(i),(e,i)=>{d(i(e,{get data(){return E()},get form(){return S.form},get params(){return S.page.params},children:(e,i)=>{var a=u(),o=n(a),s=e=>{let i=r(()=>S.constructors[1]);var a=u(),o=n(a);f(o,()=>t(i),(e,r)=>{d(r(e,{get data(){return D()},get form(){return S.form},get params(){return S.page.params},children:(e,r)=>{var i=u(),a=n(i);f(a,()=>t(P),(e,t)=>{d(t(e,{get data(){return O()},get form(){return S.form},get params(){return S.page.params}}),e=>T()[2]=e,()=>T()?.[2])}),p(e,i)},$$slots:{default:!0}}),e=>T()[1]=e,()=>T()?.[1])}),p(e,a)},c=e=>{let i=r(()=>S.constructors[1]);var a=u(),o=n(a);f(o,()=>t(i),(e,t)=>{d(t(e,{get data(){return D()},get form(){return S.form},get params(){return S.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),p(e,a)};v(o,e=>{S.constructors[2]?e(s):e(c,-1)}),p(e,a)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),p(e,a)},R=e=>{let i=r(()=>S.constructors[0]);var a=u(),o=n(a);f(o,()=>t(i),(e,t)=>{d(t(e,{get data(){return E()},get form(){return S.form},get params(){return S.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),p(e,a)};v(I,e=>{S.constructors[1]?e(L):e(R,-1)});var z=i(I,2),B=e=>{var n=A(),r=o(n),i=e=>{var n=C();_(()=>m(n,t(N))),p(e,n)};v(r,e=>{t(M)&&e(i)}),s(n),p(e,n)};v(z,e=>{t(k)&&e(B)}),p(x,F),a()}var N=x(M),P=[()=>O(()=>import(`../nodes/0.Bz_yxCPk.js`),__vite__mapDeps([0,1,2,3]),import.meta.url),()=>O(()=>import(`../nodes/1.BXecr-g5.js`),__vite__mapDeps([4,1,2,5,6]),import.meta.url),()=>O(()=>import(`../nodes/2.Du7hfnuf.js`),__vite__mapDeps([7,1,6,2,5,8]),import.meta.url),()=>O(()=>import(`../nodes/3.BTi5lIWZ.js`),__vite__mapDeps([9,1,2]),import.meta.url),()=>O(()=>import(`../nodes/4.CGofJn_s.js`),__vite__mapDeps([10,1,2]),import.meta.url),()=>O(()=>import(`../nodes/5.DeDtZvF3.js`),__vite__mapDeps([11,1,2]),import.meta.url),()=>O(()=>import(`../nodes/6.Pd88j_py.js`),__vite__mapDeps([12,1,2,13]),import.meta.url),()=>O(()=>import(`../nodes/7.Dq6C-eoP.js`),__vite__mapDeps([14,1,2]),import.meta.url),()=>O(()=>import(`../nodes/8.BW4HeEmf.js`),__vite__mapDeps([15,1,2]),import.meta.url),()=>O(()=>import(`../nodes/9.BK5CBBXk.js`),__vite__mapDeps([16,1,2]),import.meta.url),()=>O(()=>import(`../nodes/10.3_h0i6QI.js`),__vite__mapDeps([17,1,2]),import.meta.url),()=>O(()=>import(`../nodes/11.Q5ro3K8Z.js`),__vite__mapDeps([18,1,2]),import.meta.url),()=>O(()=>import(`../nodes/12.CqhEZfs2.js`),__vite__mapDeps([19,1,2]),import.meta.url),()=>O(()=>import(`../nodes/13.CyYsvdDt.js`),__vite__mapDeps([20,1,2]),import.meta.url),()=>O(()=>import(`../nodes/14.DZ-KMWs7.js`),__vite__mapDeps([21,1,2]),import.meta.url),()=>O(()=>import(`../nodes/15.CzF_DBRn.js`),__vite__mapDeps([22,1,2]),import.meta.url),()=>O(()=>import(`../nodes/16.DRUykhSC.js`),__vite__mapDeps([23,1,2]),import.meta.url),()=>O(()=>import(`../nodes/17.KofPYOuZ.js`)Line truncated
@@ -0,0 +1 @@
import{c as e,r as t}from"../chunks/BZuNOv8h.js";export{e as load_css,t as start};
@@ -1 +0,0 @@
import{n as e,s as t}from"../chunks/DZTG_XPZ.js";export{t as load_css,e as start};
@@ -0,0 +1 @@
import{G as e,K as t,M as n,N as r,Q as i,T as a,Y as o,_t as s,bt as c,dt as l,ft as u,j as d,m as f,r as p,x as m}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";import{t as h}from"../chunks/DTCALkcP.js";var g=c({prerender:()=>!1,ssr:()=>!1}),_=`data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMTNtbSIKICAgaGVpZ2h0PSIxM21tIgogICB2aWV3Qm94PSIwIDAgMTMgMTMiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIGlua3NjYXBlOnZlcnNpb249IjEuNC40IChkY2FmM2U3ZDllLCAyMDI2LTA1LTA1KSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZmF2aWNvbi5zdmciCiAgIGlua3NjYXBlOmV4cG9ydC1maWxlbmFtZT0iLi4vYmFja2VuZC93ZWIvc3JjL2xpYi9hc3NldHMvbG9nby10eHQucG5nIgogICBpbmtzY2FwZTpleHBvcnQteGRwaT0iOTYiCiAgIGlua3NjYXBlOmV4cG9ydC15ZHBpPSI5NiIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzEiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6em9vbT0iMS40NDMxODE4IgogICAgIGlua3NjYXBlOmN4PSIyODcuOTA1NTIiCiAgICAgaW5rc2NhcGU6Y3k9IjIxNC44MDMxNSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAxMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImxheWVyMSIgLz4KICA8ZGVmcwogICAgIGlkPSJkZWZzMSIgLz4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJDYWxxdWUgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjE0LjExMTFweDtmb250LWZhbWlseTpwaXhlbGFydDstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOnBpeGVsYXJ0O3dyaXRpbmctbW9kZTpsci10YjtkaXJlY3Rpb246bHRyO2ZpbGw6IzM3YWJjODtzdHJva2Utd2lkdGg6MC4yNjQ1ODMiCiAgICAgICB4PSIyLjc0OTk5OTUiCiAgICAgICB5PSIxMS41NDk5OTciCiAgICAgICBpZD0idGV4dDEiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW4xIgogICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LXNpemU6MTQuMTExMXB4O2ZvbnQtZmFtaWx5OidHb2h1Rm9udCAxMSBOZXJkIEZvbnQnOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246J0dvaHVGb250IDExIE5lcmQgRm9udCc7ZmlsbDojMGVhNWU5O2ZpbGwtb3BhY2l0eToxO3N0cm9rZS13aWR0aDowLjI2NDU4MyIKICAgICAgICAgeD0iMi43NDk5OTk1IgogICAgICAgICB5PSIxMS41NDk5OTciPmw8L3RzcGFuPjwvdGV4dD4KICA8L2c+Cjwvc3ZnPgo=`,v=r(`<link rel="icon"/> <link rel="stylesheet" href="/assets/fontello/css/fontello.css"/>`,1);function y(r,c){u(c,!0),p(()=>{h()});var g=n();m(`12qhfyh`,n=>{var r=v(),a=i(r);s(2),t(()=>f(a,`href`,_)),e(()=>{o.title=`Lasssistanoque`}),d(n,r)});var y=i(g);a(y,()=>c.children),d(r,g),l()}export{y as component,g as universal};
@@ -1 +0,0 @@
import{$ as e,F as t,M as n,O as r,Z as i,_ as a,c as o,d as s,g as c,k as l,p as u,v as d}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var f=e({prerender:()=>!1,ssr:()=>!1}),p=`data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMTNtbSIKICAgaGVpZ2h0PSIxM21tIgogICB2aWV3Qm94PSIwIDAgMTMgMTMiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIGlua3NjYXBlOnZlcnNpb249IjEuNC40IChkY2FmM2U3ZDllLCAyMDI2LTA1LTA1KSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZmF2aWNvbi5zdmciCiAgIGlua3NjYXBlOmV4cG9ydC1maWxlbmFtZT0iLi4vYmFja2VuZC93ZWIvc3JjL2xpYi9hc3NldHMvbG9nby10eHQucG5nIgogICBpbmtzY2FwZTpleHBvcnQteGRwaT0iOTYiCiAgIGlua3NjYXBlOmV4cG9ydC15ZHBpPSI5NiIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaWQ9Im5hbWVkdmlldzEiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIgogICAgIGJvcmRlcmNvbG9yPSIjMDAwMDAwIgogICAgIGJvcmRlcm9wYWNpdHk9IjAuMjUiCiAgICAgaW5rc2NhcGU6c2hvd3BhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6cGFnZW9wYWNpdHk9IjAuMCIKICAgICBpbmtzY2FwZTpwYWdlY2hlY2tlcmJvYXJkPSIwIgogICAgIGlua3NjYXBlOmRlc2tjb2xvcj0iI2QxZDFkMSIKICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0ibW0iCiAgICAgaW5rc2NhcGU6em9vbT0iMS40NDMxODE4IgogICAgIGlua3NjYXBlOmN4PSIyODcuOTA1NTIiCiAgICAgaW5rc2NhcGU6Y3k9IjIxNC44MDMxNSIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjI1NjAiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iMTAxMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iMCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9ImxheWVyMSIgLz4KICA8ZGVmcwogICAgIGlkPSJkZWZzMSIgLz4KICA8ZwogICAgIGlua3NjYXBlOmxhYmVsPSJDYWxxdWUgMSIKICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIgogICAgIGlkPSJsYXllcjEiPgogICAgPHRleHQKICAgICAgIHhtbDpzcGFjZT0icHJlc2VydmUiCiAgICAgICBzdHlsZT0iZm9udC1zaXplOjE0LjExMTFweDtmb250LWZhbWlseTpwaXhlbGFydDstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOnBpeGVsYXJ0O3dyaXRpbmctbW9kZTpsci10YjtkaXJlY3Rpb246bHRyO2ZpbGw6IzM3YWJjODtzdHJva2Utd2lkdGg6MC4yNjQ1ODMiCiAgICAgICB4PSIyLjc0OTk5OTUiCiAgICAgICB5PSIxMS41NDk5OTciCiAgICAgICBpZD0idGV4dDEiPjx0c3BhbgogICAgICAgICBzb2RpcG9kaTpyb2xlPSJsaW5lIgogICAgICAgICBpZD0idHNwYW4xIgogICAgICAgICBzdHlsZT0iZm9udC1zdHlsZTpub3JtYWw7Zm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zdHJldGNoOm5vcm1hbDtmb250LXNpemU6MTQuMTExMXB4O2ZvbnQtZmFtaWx5OidHb2h1Rm9udCAxMSBOZXJkIEZvbnQnOy1pbmtzY2FwZS1mb250LXNwZWNpZmljYXRpb246J0dvaHVGb250IDExIE5lcmQgRm9udCc7ZmlsbDojMGVhNWU5O2ZpbGwtb3BhY2l0eToxO3N0cm9rZS13aWR0aDowLjI2NDU4MyIKICAgICAgICAgeD0iMi43NDk5OTk1IgogICAgICAgICB5PSIxMS41NDk5OTciPmw8L3RzcGFuPjwvdGV4dD4KICA8L2c+Cjwvc3ZnPgo=`,m=d(`<link rel="icon"/> <link rel="stylesheet" href="/assets/fontello/css/fontello.css"/>`,1);function h(e,d){var f=a();s(`12qhfyh`,e=>{var a=m(),s=t(a);i(2),l(()=>o(s,`href`,p)),r(()=>{n.title=`Lasssistanoque`}),c(e,a)});var h=t(f);u(h,()=>d.children),c(e,f)}export{h as component,f as universal};
@@ -1 +0,0 @@
import{F as e,I as t,K as n,P as r,Q as i,g as a,h as o,k as s,q as c,v as l}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";import{t as u}from"../chunks/BJ9updZG.js";var d=l(`<h1> </h1> <p> </p>`,1);function f(l,f){c(f,!0);var p=d(),m=e(p),h=r(m,!0);i(m);var g=t(m,2),_=r(g,!0);i(g),s(()=>{o(h,u.status),o(_,u.error?.message)}),a(l,p),n()}export{f as component};
@@ -0,0 +1 @@
import{$ as e,A as t,K as n,N as r,Q as i,Z as a,dt as o,ft as s,j as c,vt as l}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";import{t as u}from"../chunks/ffLBjIeN.js";var d=r(`<h1> </h1> <p> </p>`,1);function f(r,f){s(f,!0);var p=d(),m=i(p),h=a(m,!0);l(m);var g=e(m,2),_=a(g,!0);l(g),n(()=>{t(h,u.status),t(_,u.error?.message)}),c(r,p),o()}export{f as component};
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Base de connaissance`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
@@ -0,0 +1 @@
import{N as e,bt as t,j as n}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var r=t({load:()=>i});function i(){return{title:`Paramétrage : Canaux de communication`,back:`/settings`}}var a=e(`<div>Modèles</div>`);function o(e){var t=a();n(e,t)}export{o as component,r as universal};
@@ -0,0 +1 @@
import{N as e,bt as t,j as n}from"../chunks/DGrvnoUE.js";import"../chunks/xihTtKlq.js";var r=t({load:()=>i});function i(){return{title:`Paramétrage : Base de connaissance`,back:`/settings`}}var a=e(`<div>Modèles</div>`);function o(e){var t=a();n(e,t)}export{o as component,r as universal};
@@ -1 +0,0 @@
import{$ as e,g as t,v as n}from"../chunks/DxmhE1z1.js";import"../chunks/xihTtKlq.js";var r=e({load:()=>i});function i(){return{title:`Paramétrage : Mémoire`,back:`/settings`}}var a=n(`<div>Modèles</div>`);function o(e){var n=a();t(e,n)}export{o as component,r as universal};
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more