add teams
EBO <eric.bouhana@softalys.com> committé le 2026-05-08 14:57
d4d6828cb65fae19b6a70ce6dc6d44a4e9c355fd
1 parent(s)
27 fichiers modifiés
+705
-2649
D
.specify/memory/constitution.md
+0
-150
@@ -1,150 +0,0 @@
| 1 | -<!-- | |
| 2 | -Sync Impact Report: | |
| 3 | -- Version change: (none) → 0.5.2 (initial placement) | |
| 4 | -- Source: Rustwarden/constitution.md — constitution complète importée pour le nouveau projet | |
| 5 | -- Modified principles: N/A (placement initial) | |
| 6 | -- Added sections: Constitution complète (Core Principles I–V, Technical Constraints, Development Workflow, Governance) | |
| 7 | -- Removed sections: None | |
| 8 | -- Templates requiring updates: | |
| 9 | - ✅ plan-template.md (Constitution Check / gates — aligné, pas de modification) | |
| 10 | - ✅ spec-template.md (scope/requirements — aligné, pas de modification) | |
| 11 | - ✅ tasks-template.md (catégorisation tâches — aligné, pas de modification) | |
| 12 | - ✅ .cursor/commands/*.md (référence .specify/memory/constitution.md — valide) | |
| 13 | -- Follow-up TODOs: None | |
| 14 | ---> | |
| 15 | - | |
| 16 | -# Rustwarden Constitution | |
| 17 | - | |
| 18 | -## Core Principles | |
| 19 | - | |
| 20 | -### I. Qualité de Code | |
| 21 | - | |
| 22 | -Toutes les fonctions publiques DOIVENT être documentées avec `///` (doc comments Rust). Le code DOIT respecter les bonnes pratiques Rust (clippy warnings = 0). Les structures de données DOIVENT utiliser `#[derive(Debug, Clone)]` où approprié. La gestion d'erreurs DOIT utiliser `Result<T, E>` avec des types d'erreur explicites. | |
| 23 | - | |
| 24 | -**Rationale**: La documentation et la qualité du code facilitent la maintenance, la collaboration et réduisent les bugs. Rust offre des outils puissants (clippy, rustfmt) qui doivent être utilisés. | |
| 25 | - | |
| 26 | -### II. Architecture Axum et SeaORM | |
| 27 | - | |
| 28 | -Toutes les routes HTTP DOIVENT être définies avec Axum uniquement (`axum::Router`). Toutes les interactions avec la base de données DOIVENT passer par SeaORM (aucun raw SQL autorisé). Les migrations DOIVENT être gérées via le système de migrations SeaORM (`migrations/src/`). Le serveur DOIT utiliser Tokio comme runtime async (requis par Axum). | |
| 29 | - | |
| 30 | -**Documentation SeaORM** (amendement v0.4.1): Toutes les interactions avec la base de données DOIVENT être implémentées en référence à la [documentation officielle SeaORM](https://www.sea-ql.org/SeaORM/docs/index/). Avant d'implémenter une nouvelle fonctionnalité de base de données (requêtes, relations, transactions, etc.), la documentation SeaORM DOIT être consultée pour garantir l'utilisation des bonnes pratiques et des patterns recommandés. Les exemples de code et les patterns documentés dans la documentation SeaORM DOIVENT être privilégiés par rapport aux approches personnalisées. | |
| 31 | - | |
| 32 | -**Implémentation obligatoire des handlers serveur** (amendement v0.5.0): TOUJOURS implémenter les handlers serveur pour que l'API soit fonctionnelle. Les handlers stub (retournant `501 Not Implemented`) sont INTERDITS dans le code de production. Chaque route définie DOIT avoir un handler complet qui : | |
| 33 | -- Injecte l'état nécessaire (`State<DatabaseConnection>`, `Extension<Claims>`, etc.) | |
| 34 | -- Utilise les services métier appropriés | |
| 35 | -- Retourne des réponses fonctionnelles avec données réelles | |
| 36 | -- Gère les erreurs de manière appropriée | |
| 37 | - | |
| 38 | -Les handlers stub sont uniquement autorisés temporairement pendant le développement incrémental, mais DOIVENT être remplacés par des implémentations complètes avant que la feature soit considérée comme terminée. Toute documentation d'API (OpenAPI, guides, exemples) qui référence un endpoint DOIT correspondre à un handler serveur fonctionnel. | |
| 39 | - | |
| 40 | -**Rationale**: Axum et SeaORM sont les frameworks standard pour Rust, offrant sécurité, performance et maintenabilité. L'utilisation exclusive de ces outils garantit la cohérence de l'architecture. La documentation officielle SeaORM contient les meilleures pratiques, les patterns recommandés et les solutions aux problèmes courants. En s'y référant systématiquement, on évite les erreurs d'implémentation, on améliore la maintenabilité et on garantit la compatibilité avec les futures versions de SeaORM. L'implémentation complète des handlers serveur est essentielle pour que l'API soit réellement utilisable par les clients. Des handlers stub créent une fausse impression de fonctionnalité et empêchent les tests d'intégration réels. Cette règle garantit que toute documentation d'API correspond à une implémentation fonctionnelle, évitant les frustrations des développeurs clients. | |
| 41 | - | |
| 42 | -### III. Développement Incrémental | |
| 43 | - | |
| 44 | -Chaque fonctionnalité DOIT être complète et testée avant de passer à la suivante. L'architecture DOIT être modulaire (structure de dossiers claire, séparation des responsabilités). Les user stories DOIVENT être indépendantes et testables individuellement. | |
| 45 | - | |
| 46 | -**Rationale**: Le développement incrémental permet de livrer de la valeur rapidement, facilite les tests et réduit la complexité. Chaque étape doit être validée avant de continuer. | |
| 47 | - | |
| 48 | -### IV. Sécurité par Défaut | |
| 49 | - | |
| 50 | -Tous les secrets DOIVENT être stockés dans des variables d'environnement, JAMAIS dans le code. La validation et la sanitization des entrées utilisateur DOIVENT être effectuées systématiquement. L'authentification et l'autorisation RBAC DOIVENT être implémentées pour toutes les routes protégées. | |
| 51 | - | |
| 52 | -**JWT Authentication Standard** (amendement v0.3.1): Le système DOIT extraire et vérifier les JWT tokens depuis le header `Authorization` au format `Bearer <token>`. Cette extraction DOIT être la méthode principale pour l'authentification API. Les cookies HttpOnly peuvent être utilisés comme méthode alternative pour les clients web, mais le header Authorization a priorité pour permettre aux clients API de surcharger l'authentification par cookie. | |
| 53 | - | |
| 54 | -**Conformité ANSSI Rust** (amendement v0.4.0): Le code DOIT respecter les recommandations du [Guide ANSSI pour le développement sécurisé en Rust](https://anssi-fr.github.io/rust-guide/). Spécifiquement : | |
| 55 | -- Le code DOIT compiler avec `#![deny(warnings)]` activé (ou au minimum `#![warn(warnings)]`) | |
| 56 | -- Toute utilisation de `unsafe` DOIT être justifiée et documentée avec un commentaire `// SAFETY:` expliquant pourquoi `unsafe` est nécessaire et quels invariants doivent être respectés | |
| 57 | -- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (sauf dans les tests unitaires). Toutes les erreurs DOIVENT être gérées explicitement avec `Result<T, E>` et l'opérateur `?` | |
| 58 | -- L'utilisation de `std::mem::uninitialized` est INTERDITE (dépréciée). L'utilisation de `std::mem::MaybeUninit` DOIT être justifiée pour chaque cas d'usage | |
| 59 | -- Les outils `cargo-audit` et `cargo-outdated` DOIVENT être exécutés régulièrement. Les vulnérabilités détectées DOIVENT être corrigées avant chaque release | |
| 60 | -- Toutes les considérations de sécurité, invariants et garanties DOIVENT être documentées | |
| 61 | - | |
| 62 | -Voir `docs/ANSSI_COMPLIANCE_REPORT.md` pour le rapport de conformité détaillé et les actions prioritaires. | |
| 63 | - | |
| 64 | -**Rationale**: La sécurité est critique pour un système cloud. Les secrets dans le code sont une vulnérabilité majeure. La validation des entrées prévient les attaques par injection. L'authentification JWT via header Authorization est le standard de l'industrie pour les APIs REST et permet une authentification stateless sécurisée. Les guidelines ANSSI représentent les meilleures pratiques pour le développement sécurisé en Rust. Leur respect garantit que les garanties de sécurité offertes par Rust ne sont pas invalidées par une utilisation incorrecte du langage. | |
| 65 | - | |
| 66 | -### V. Tests et Validation Continue | |
| 67 | - | |
| 68 | -Tous les composants DOIVENT avoir des tests unitaires et d'intégration. La couverture de tests DOIT être > 80% (objectif > 90% pour les composants critiques). Les tests DOIVENT être exécutés avant chaque commit (`cargo test`). Les tests DOIVENT suivre les bonnes pratiques Rust (structure `#[cfg(test)]`, tests d'intégration dans `tests/integration/`). | |
| 69 | - | |
| 70 | -**Rationale**: Les tests garantissent la qualité et la fiabilité du code. Une couverture élevée réduit les régressions et facilite les refactorings. L'exécution automatique des tests avant commit maintient la qualité du codebase. | |
| 71 | - | |
| 72 | -## Technical Constraints | |
| 73 | - | |
| 74 | -### Framework Requirements | |
| 75 | - | |
| 76 | -- **Rust Version**: Rust 1.91.1+ (Edition 2024) - **IMPORTANT**: Version minimale requise pour Axum/SeaORM | |
| 77 | -- **Web Framework**: Axum 0.8.7+ (dernière version stable) | |
| 78 | -- **ORM**: SeaORM 2.0.x+ (requis pour toutes les interactions DB) | |
| 79 | -- **Runtime**: Tokio 1.x (requis par Axum) | |
| 80 | -- **Database**: PostgreSQL 18+ (recommandé) | |
| 81 | - | |
| 82 | -### Development Environment | |
| 83 | - | |
| 84 | -**Docker pour Base de Données** (amendement v0.5.2): En développement, la base de données PostgreSQL DOIT être lancée via Docker depuis le dossier `database/` à la racine du projet. Le développeur DOIT utiliser le fichier `database/docker-compose.yml` pour démarrer la base de données. La base de données est lancée et gérée par le développeur (pas automatiquement par le système). Cette configuration garantit un environnement de développement cohérent et isolé, facilite le partage de configuration entre développeurs, et permet un reset rapide de la base de données pour les tests. | |
| 85 | - | |
| 86 | -**Rationale**: L'utilisation de Docker pour PostgreSQL en développement standardise l'environnement de développement, évite les conflits de versions entre développeurs, et simplifie la configuration initiale. Le développeur garde le contrôle sur le démarrage/arrêt de la base de données, permettant une meilleure gestion des ressources et une flexibilité dans le workflow de développement. | |
| 87 | - | |
| 88 | -### Code Quality Requirements | |
| 89 | - | |
| 90 | -- Toutes les fonctions publiques DOIVENT avoir une documentation `///` | |
| 91 | -- `cargo clippy -- -D warnings` DOIT passer sans erreur | |
| 92 | -- `cargo fmt` DOIT être appliqué avant chaque commit | |
| 93 | -- Les erreurs DOIVENT utiliser des types explicites (`Result<T, AppError>`) | |
| 94 | -- `#![deny(warnings)]` DOIT être activé dans `src/lib.rs` ou `src/main.rs` (conformité ANSSI) | |
| 95 | -- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (conformité ANSSI) | |
| 96 | -- Toute utilisation de `unsafe` DOIT être documentée avec `// SAFETY:` (conformité ANSSI) | |
| 97 | - | |
| 98 | -### Testing Requirements | |
| 99 | - | |
| 100 | -- Tests unitaires dans le même fichier avec `#[cfg(test)]` ou dans `tests/unit/` | |
| 101 | -- Tests d'intégration dans `tests/integration/` | |
| 102 | -- Tests de contrat dans `tests/contract/` | |
| 103 | -- Couverture > 80% (objectif > 90% pour auth, security, core services) | |
| 104 | - | |
| 105 | -### Security Requirements | |
| 106 | - | |
| 107 | -- Pas de secrets hardcodés (variables d'environnement uniquement) | |
| 108 | -- Validation de toutes les entrées utilisateur | |
| 109 | -- Hashing des mots de passe (bcrypt, jamais en clair) | |
| 110 | -- JWT tokens: extraction depuis header `Authorization: Bearer <token>` (priorité 1) | |
| 111 | -- Cookies HttpOnly pour les clients web (fallback, priorité 2) | |
| 112 | -- Rate limiting sur les endpoints d'authentification | |
| 113 | -- Logging de tous les événements d'authentification (succès et échecs) | |
| 114 | -- Conformité aux guidelines ANSSI Rust (voir `docs/ANSSI_COMPLIANCE_REPORT.md`) | |
| 115 | -- Code `unsafe` minimisé et documenté avec `// SAFETY:` | |
| 116 | -- Gestion d'erreurs explicite (pas de panics en production) | |
| 117 | - | |
| 118 | -## Development Workflow | |
| 119 | - | |
| 120 | -### Code Review Process | |
| 121 | - | |
| 122 | -Toutes les pull requests DOIVENT être revues avant merge. Les reviewers DOIVENT vérifier la conformité avec cette constitution. Les violations constitutionnelles DOIVENT être corrigées avant merge. | |
| 123 | - | |
| 124 | -### Quality Gates | |
| 125 | - | |
| 126 | -- ✅ `cargo build` passe sans erreur | |
| 127 | -- ✅ `cargo test` passe (tous les tests) | |
| 128 | -- ✅ `cargo clippy -- -D warnings` passe | |
| 129 | -- ✅ `cargo fmt --check` passe | |
| 130 | -- ✅ Couverture de tests > 80% | |
| 131 | -- ✅ Documentation complète (`cargo doc` génère sans warnings) | |
| 132 | -- ✅ `cargo audit` ne signale pas de vulnérabilités (avant release, conformité ANSSI) | |
| 133 | -- ✅ `cargo outdated` a été vérifié (avant release, conformité ANSSI) | |
| 134 | -- ✅ Aucun `unwrap()`/`expect()` dans le code de production (sauf tests, conformité ANSSI) | |
| 135 | -- ✅ Tous les blocs `unsafe` sont documentés avec `// SAFETY:` (conformité ANSSI) | |
| 136 | - | |
| 137 | -### Testing Strategy | |
| 138 | - | |
| 139 | -1. **TDD recommandé**: Écrire les tests avant l'implémentation pour les nouvelles fonctionnalités | |
| 140 | -2. **Tests unitaires**: Pour chaque fonction publique, tester les cas normaux et d'erreur | |
| 141 | -3. **Tests d'intégration**: Pour chaque endpoint API, tester les scénarios complets | |
| 142 | -4. **Tests de contrat**: Pour chaque endpoint public, valider le format de réponse | |
| 143 | - | |
| 144 | -## Governance | |
| 145 | - | |
| 146 | -Cette constitution prévaut sur toutes les autres pratiques du projet. Les amendements nécessitent une documentation, une approbation et un plan de migration si nécessaire. | |
| 147 | - | |
| 148 | -**Version**: 0.5.2 | **Ratified**: 2025-12-09 | **Last Amended**: 2025-03-05 | |
| 149 | - | |
| 150 | -**Compliance**: Toutes les PRs et reviews DOIVENT vérifier la conformité avec cette constitution. La complexité DOIT être justifiée. Utiliser les templates dans `.specify/templates/` pour la documentation des features. |
D
.specify/scripts/bash/check-prerequisites.sh
+0
-166
@@ -1,166 +0,0 @@
| 1 | -#!/usr/bin/env bash | |
| 2 | - | |
| 3 | -# Consolidated prerequisite checking script | |
| 4 | -# | |
| 5 | -# This script provides unified prerequisite checking for Spec-Driven Development workflow. | |
| 6 | -# It replaces the functionality previously spread across multiple scripts. | |
| 7 | -# | |
| 8 | -# Usage: ./check-prerequisites.sh [OPTIONS] | |
| 9 | -# | |
| 10 | -# OPTIONS: | |
| 11 | -# --json Output in JSON format | |
| 12 | -# --require-tasks Require tasks.md to exist (for implementation phase) | |
| 13 | -# --include-tasks Include tasks.md in AVAILABLE_DOCS list | |
| 14 | -# --paths-only Only output path variables (no validation) | |
| 15 | -# --help, -h Show help message | |
| 16 | -# | |
| 17 | -# OUTPUTS: | |
| 18 | -# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]} | |
| 19 | -# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md | |
| 20 | -# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc. | |
| 21 | - | |
| 22 | -set -e | |
| 23 | - | |
| 24 | -# Parse command line arguments | |
| 25 | -JSON_MODE=false | |
| 26 | -REQUIRE_TASKS=false | |
| 27 | -INCLUDE_TASKS=false | |
| 28 | -PATHS_ONLY=false | |
| 29 | - | |
| 30 | -for arg in "$@"; do | |
| 31 | - case "$arg" in | |
| 32 | - --json) | |
| 33 | - JSON_MODE=true | |
| 34 | - ;; | |
| 35 | - --require-tasks) | |
| 36 | - REQUIRE_TASKS=true | |
| 37 | - ;; | |
| 38 | - --include-tasks) | |
| 39 | - INCLUDE_TASKS=true | |
| 40 | - ;; | |
| 41 | - --paths-only) | |
| 42 | - PATHS_ONLY=true | |
| 43 | - ;; | |
| 44 | - --help|-h) | |
| 45 | - cat << 'EOF' | |
| 46 | -Usage: check-prerequisites.sh [OPTIONS] | |
| 47 | - | |
| 48 | -Consolidated prerequisite checking for Spec-Driven Development workflow. | |
| 49 | - | |
| 50 | -OPTIONS: | |
| 51 | - --json Output in JSON format | |
| 52 | - --require-tasks Require tasks.md to exist (for implementation phase) | |
| 53 | - --include-tasks Include tasks.md in AVAILABLE_DOCS list | |
| 54 | - --paths-only Only output path variables (no prerequisite validation) | |
| 55 | - --help, -h Show this help message | |
| 56 | - | |
| 57 | -EXAMPLES: | |
| 58 | - # Check task prerequisites (plan.md required) | |
| 59 | - ./check-prerequisites.sh --json | |
| 60 | - | |
| 61 | - # Check implementation prerequisites (plan.md + tasks.md required) | |
| 62 | - ./check-prerequisites.sh --json --require-tasks --include-tasks | |
| 63 | - | |
| 64 | - # Get feature paths only (no validation) | |
| 65 | - ./check-prerequisites.sh --paths-only | |
| 66 | - | |
| 67 | -EOF | |
| 68 | - exit 0 | |
| 69 | - ;; | |
| 70 | - *) | |
| 71 | - echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2 | |
| 72 | - exit 1 | |
| 73 | - ;; | |
| 74 | - esac | |
| 75 | -done | |
| 76 | - | |
| 77 | -# Source common functions | |
| 78 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 79 | -source "$SCRIPT_DIR/common.sh" | |
| 80 | - | |
| 81 | -# Get feature paths and validate branch | |
| 82 | -eval $(get_feature_paths) | |
| 83 | -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 | |
| 84 | - | |
| 85 | -# If paths-only mode, output paths and exit (support JSON + paths-only combined) | |
| 86 | -if $PATHS_ONLY; then | |
| 87 | - if $JSON_MODE; then | |
| 88 | - # Minimal JSON paths payload (no validation performed) | |
| 89 | - printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \ | |
| 90 | - "$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS" | |
| 91 | - else | |
| 92 | - echo "REPO_ROOT: $REPO_ROOT" | |
| 93 | - echo "BRANCH: $CURRENT_BRANCH" | |
| 94 | - echo "FEATURE_DIR: $FEATURE_DIR" | |
| 95 | - echo "FEATURE_SPEC: $FEATURE_SPEC" | |
| 96 | - echo "IMPL_PLAN: $IMPL_PLAN" | |
| 97 | - echo "TASKS: $TASKS" | |
| 98 | - fi | |
| 99 | - exit 0 | |
| 100 | -fi | |
| 101 | - | |
| 102 | -# Validate required directories and files | |
| 103 | -if [[ ! -d "$FEATURE_DIR" ]]; then | |
| 104 | - echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 | |
| 105 | - echo "Run /speckit.specify first to create the feature structure." >&2 | |
| 106 | - exit 1 | |
| 107 | -fi | |
| 108 | - | |
| 109 | -if [[ ! -f "$IMPL_PLAN" ]]; then | |
| 110 | - echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 | |
| 111 | - echo "Run /speckit.plan first to create the implementation plan." >&2 | |
| 112 | - exit 1 | |
| 113 | -fi | |
| 114 | - | |
| 115 | -# Check for tasks.md if required | |
| 116 | -if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then | |
| 117 | - echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 | |
| 118 | - echo "Run /speckit.tasks first to create the task list." >&2 | |
| 119 | - exit 1 | |
| 120 | -fi | |
| 121 | - | |
| 122 | -# Build list of available documents | |
| 123 | -docs=() | |
| 124 | - | |
| 125 | -# Always check these optional docs | |
| 126 | -[[ -f "$RESEARCH" ]] && docs+=("research.md") | |
| 127 | -[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") | |
| 128 | - | |
| 129 | -# Check contracts directory (only if it exists and has files) | |
| 130 | -if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then | |
| 131 | - docs+=("contracts/") | |
| 132 | -fi | |
| 133 | - | |
| 134 | -[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") | |
| 135 | - | |
| 136 | -# Include tasks.md if requested and it exists | |
| 137 | -if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then | |
| 138 | - docs+=("tasks.md") | |
| 139 | -fi | |
| 140 | - | |
| 141 | -# Output results | |
| 142 | -if $JSON_MODE; then | |
| 143 | - # Build JSON array of documents | |
| 144 | - if [[ ${#docs[@]} -eq 0 ]]; then | |
| 145 | - json_docs="[]" | |
| 146 | - else | |
| 147 | - json_docs=$(printf '"%s",' "${docs[@]}") | |
| 148 | - json_docs="[${json_docs%,}]" | |
| 149 | - fi | |
| 150 | - | |
| 151 | - printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" | |
| 152 | -else | |
| 153 | - # Text output | |
| 154 | - echo "FEATURE_DIR:$FEATURE_DIR" | |
| 155 | - echo "AVAILABLE_DOCS:" | |
| 156 | - | |
| 157 | - # Show status of each potential document | |
| 158 | - check_file "$RESEARCH" "research.md" | |
| 159 | - check_file "$DATA_MODEL" "data-model.md" | |
| 160 | - check_dir "$CONTRACTS_DIR" "contracts/" | |
| 161 | - check_file "$QUICKSTART" "quickstart.md" | |
| 162 | - | |
| 163 | - if $INCLUDE_TASKS; then | |
| 164 | - check_file "$TASKS" "tasks.md" | |
| 165 | - fi | |
| 166 | -fi |
D
.specify/scripts/bash/common.sh
+0
-156
@@ -1,156 +0,0 @@
| 1 | -#!/usr/bin/env bash | |
| 2 | -# Common functions and variables for all scripts | |
| 3 | - | |
| 4 | -# Get repository root, with fallback for non-git repositories | |
| 5 | -get_repo_root() { | |
| 6 | - if git rev-parse --show-toplevel >/dev/null 2>&1; then | |
| 7 | - git rev-parse --show-toplevel | |
| 8 | - else | |
| 9 | - # Fall back to script location for non-git repos | |
| 10 | - local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 11 | - (cd "$script_dir/../../.." && pwd) | |
| 12 | - fi | |
| 13 | -} | |
| 14 | - | |
| 15 | -# Get current branch, with fallback for non-git repositories | |
| 16 | -get_current_branch() { | |
| 17 | - # First check if SPECIFY_FEATURE environment variable is set | |
| 18 | - if [[ -n "${SPECIFY_FEATURE:-}" ]]; then | |
| 19 | - echo "$SPECIFY_FEATURE" | |
| 20 | - return | |
| 21 | - fi | |
| 22 | - | |
| 23 | - # Then check git if available | |
| 24 | - if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then | |
| 25 | - git rev-parse --abbrev-ref HEAD | |
| 26 | - return | |
| 27 | - fi | |
| 28 | - | |
| 29 | - # For non-git repos, try to find the latest feature directory | |
| 30 | - local repo_root=$(get_repo_root) | |
| 31 | - local specs_dir="$repo_root/specs" | |
| 32 | - | |
| 33 | - if [[ -d "$specs_dir" ]]; then | |
| 34 | - local latest_feature="" | |
| 35 | - local highest=0 | |
| 36 | - | |
| 37 | - for dir in "$specs_dir"/*; do | |
| 38 | - if [[ -d "$dir" ]]; then | |
| 39 | - local dirname=$(basename "$dir") | |
| 40 | - if [[ "$dirname" =~ ^([0-9]{3})- ]]; then | |
| 41 | - local number=${BASH_REMATCH[1]} | |
| 42 | - number=$((10#$number)) | |
| 43 | - if [[ "$number" -gt "$highest" ]]; then | |
| 44 | - highest=$number | |
| 45 | - latest_feature=$dirname | |
| 46 | - fi | |
| 47 | - fi | |
| 48 | - fi | |
| 49 | - done | |
| 50 | - | |
| 51 | - if [[ -n "$latest_feature" ]]; then | |
| 52 | - echo "$latest_feature" | |
| 53 | - return | |
| 54 | - fi | |
| 55 | - fi | |
| 56 | - | |
| 57 | - echo "main" # Final fallback | |
| 58 | -} | |
| 59 | - | |
| 60 | -# Check if we have git available | |
| 61 | -has_git() { | |
| 62 | - git rev-parse --show-toplevel >/dev/null 2>&1 | |
| 63 | -} | |
| 64 | - | |
| 65 | -check_feature_branch() { | |
| 66 | - local branch="$1" | |
| 67 | - local has_git_repo="$2" | |
| 68 | - | |
| 69 | - # For non-git repos, we can't enforce branch naming but still provide output | |
| 70 | - if [[ "$has_git_repo" != "true" ]]; then | |
| 71 | - echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2 | |
| 72 | - return 0 | |
| 73 | - fi | |
| 74 | - | |
| 75 | - if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then | |
| 76 | - echo "ERROR: Not on a feature branch. Current branch: $branch" >&2 | |
| 77 | - echo "Feature branches should be named like: 001-feature-name" >&2 | |
| 78 | - return 1 | |
| 79 | - fi | |
| 80 | - | |
| 81 | - return 0 | |
| 82 | -} | |
| 83 | - | |
| 84 | -get_feature_dir() { echo "$1/specs/$2"; } | |
| 85 | - | |
| 86 | -# Find feature directory by numeric prefix instead of exact branch match | |
| 87 | -# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature) | |
| 88 | -find_feature_dir_by_prefix() { | |
| 89 | - local repo_root="$1" | |
| 90 | - local branch_name="$2" | |
| 91 | - local specs_dir="$repo_root/specs" | |
| 92 | - | |
| 93 | - # Extract numeric prefix from branch (e.g., "004" from "004-whatever") | |
| 94 | - if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then | |
| 95 | - # If branch doesn't have numeric prefix, fall back to exact match | |
| 96 | - echo "$specs_dir/$branch_name" | |
| 97 | - return | |
| 98 | - fi | |
| 99 | - | |
| 100 | - local prefix="${BASH_REMATCH[1]}" | |
| 101 | - | |
| 102 | - # Search for directories in specs/ that start with this prefix | |
| 103 | - local matches=() | |
| 104 | - if [[ -d "$specs_dir" ]]; then | |
| 105 | - for dir in "$specs_dir"/"$prefix"-*; do | |
| 106 | - if [[ -d "$dir" ]]; then | |
| 107 | - matches+=("$(basename "$dir")") | |
| 108 | - fi | |
| 109 | - done | |
| 110 | - fi | |
| 111 | - | |
| 112 | - # Handle results | |
| 113 | - if [[ ${#matches[@]} -eq 0 ]]; then | |
| 114 | - # No match found - return the branch name path (will fail later with clear error) | |
| 115 | - echo "$specs_dir/$branch_name" | |
| 116 | - elif [[ ${#matches[@]} -eq 1 ]]; then | |
| 117 | - # Exactly one match - perfect! | |
| 118 | - echo "$specs_dir/${matches[0]}" | |
| 119 | - else | |
| 120 | - # Multiple matches - this shouldn't happen with proper naming convention | |
| 121 | - echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2 | |
| 122 | - echo "Please ensure only one spec directory exists per numeric prefix." >&2 | |
| 123 | - echo "$specs_dir/$branch_name" # Return something to avoid breaking the script | |
| 124 | - fi | |
| 125 | -} | |
| 126 | - | |
| 127 | -get_feature_paths() { | |
| 128 | - local repo_root=$(get_repo_root) | |
| 129 | - local current_branch=$(get_current_branch) | |
| 130 | - local has_git_repo="false" | |
| 131 | - | |
| 132 | - if has_git; then | |
| 133 | - has_git_repo="true" | |
| 134 | - fi | |
| 135 | - | |
| 136 | - # Use prefix-based lookup to support multiple branches per spec | |
| 137 | - local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch") | |
| 138 | - | |
| 139 | - cat <<EOF | |
| 140 | -REPO_ROOT='$repo_root' | |
| 141 | -CURRENT_BRANCH='$current_branch' | |
| 142 | -HAS_GIT='$has_git_repo' | |
| 143 | -FEATURE_DIR='$feature_dir' | |
| 144 | -FEATURE_SPEC='$feature_dir/spec.md' | |
| 145 | -IMPL_PLAN='$feature_dir/plan.md' | |
| 146 | -TASKS='$feature_dir/tasks.md' | |
| 147 | -RESEARCH='$feature_dir/research.md' | |
| 148 | -DATA_MODEL='$feature_dir/data-model.md' | |
| 149 | -QUICKSTART='$feature_dir/quickstart.md' | |
| 150 | -CONTRACTS_DIR='$feature_dir/contracts' | |
| 151 | -EOF | |
| 152 | -} | |
| 153 | - | |
| 154 | -check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; } | |
| 155 | -check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; } | |
| 156 | - |
D
.specify/scripts/bash/create-new-feature.sh
+0
-313
@@ -1,313 +0,0 @@
| 1 | -#!/usr/bin/env bash | |
| 2 | - | |
| 3 | -set -e | |
| 4 | - | |
| 5 | -JSON_MODE=false | |
| 6 | -SHORT_NAME="" | |
| 7 | -BRANCH_NUMBER="" | |
| 8 | -ARGS=() | |
| 9 | -i=1 | |
| 10 | -while [ $i -le $# ]; do | |
| 11 | - arg="${!i}" | |
| 12 | - case "$arg" in | |
| 13 | - --json) | |
| 14 | - JSON_MODE=true | |
| 15 | - ;; | |
| 16 | - --short-name) | |
| 17 | - if [ $((i + 1)) -gt $# ]; then | |
| 18 | - echo 'Error: --short-name requires a value' >&2 | |
| 19 | - exit 1 | |
| 20 | - fi | |
| 21 | - i=$((i + 1)) | |
| 22 | - next_arg="${!i}" | |
| 23 | - # Check if the next argument is another option (starts with --) | |
| 24 | - if [[ "$next_arg" == --* ]]; then | |
| 25 | - echo 'Error: --short-name requires a value' >&2 | |
| 26 | - exit 1 | |
| 27 | - fi | |
| 28 | - SHORT_NAME="$next_arg" | |
| 29 | - ;; | |
| 30 | - --number) | |
| 31 | - if [ $((i + 1)) -gt $# ]; then | |
| 32 | - echo 'Error: --number requires a value' >&2 | |
| 33 | - exit 1 | |
| 34 | - fi | |
| 35 | - i=$((i + 1)) | |
| 36 | - next_arg="${!i}" | |
| 37 | - if [[ "$next_arg" == --* ]]; then | |
| 38 | - echo 'Error: --number requires a value' >&2 | |
| 39 | - exit 1 | |
| 40 | - fi | |
| 41 | - BRANCH_NUMBER="$next_arg" | |
| 42 | - ;; | |
| 43 | - --help|-h) | |
| 44 | - echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" | |
| 45 | - echo "" | |
| 46 | - echo "Options:" | |
| 47 | - echo " --json Output in JSON format" | |
| 48 | - echo " --short-name <name> Provide a custom short name (2-4 words) for the branch" | |
| 49 | - echo " --number N Specify branch number manually (overrides auto-detection)" | |
| 50 | - echo " --help, -h Show this help message" | |
| 51 | - echo "" | |
| 52 | - echo "Examples:" | |
| 53 | - echo " $0 'Add user authentication system' --short-name 'user-auth'" | |
| 54 | - echo " $0 'Implement OAuth2 integration for API' --number 5" | |
| 55 | - exit 0 | |
| 56 | - ;; | |
| 57 | - *) | |
| 58 | - ARGS+=("$arg") | |
| 59 | - ;; | |
| 60 | - esac | |
| 61 | - i=$((i + 1)) | |
| 62 | -done | |
| 63 | - | |
| 64 | -FEATURE_DESCRIPTION="${ARGS[*]}" | |
| 65 | -if [ -z "$FEATURE_DESCRIPTION" ]; then | |
| 66 | - echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2 | |
| 67 | - exit 1 | |
| 68 | -fi | |
| 69 | - | |
| 70 | -# Trim whitespace and validate description is not empty (e.g., user passed only whitespace) | |
| 71 | -FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs) | |
| 72 | -if [ -z "$FEATURE_DESCRIPTION" ]; then | |
| 73 | - echo "Error: Feature description cannot be empty or contain only whitespace" >&2 | |
| 74 | - exit 1 | |
| 75 | -fi | |
| 76 | - | |
| 77 | -# Function to find the repository root by searching for existing project markers | |
| 78 | -find_repo_root() { | |
| 79 | - local dir="$1" | |
| 80 | - while [ "$dir" != "/" ]; do | |
| 81 | - if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then | |
| 82 | - echo "$dir" | |
| 83 | - return 0 | |
| 84 | - fi | |
| 85 | - dir="$(dirname "$dir")" | |
| 86 | - done | |
| 87 | - return 1 | |
| 88 | -} | |
| 89 | - | |
| 90 | -# Function to get highest number from specs directory | |
| 91 | -get_highest_from_specs() { | |
| 92 | - local specs_dir="$1" | |
| 93 | - local highest=0 | |
| 94 | - | |
| 95 | - if [ -d "$specs_dir" ]; then | |
| 96 | - for dir in "$specs_dir"/*; do | |
| 97 | - [ -d "$dir" ] || continue | |
| 98 | - dirname=$(basename "$dir") | |
| 99 | - number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") | |
| 100 | - number=$((10#$number)) | |
| 101 | - if [ "$number" -gt "$highest" ]; then | |
| 102 | - highest=$number | |
| 103 | - fi | |
| 104 | - done | |
| 105 | - fi | |
| 106 | - | |
| 107 | - echo "$highest" | |
| 108 | -} | |
| 109 | - | |
| 110 | -# Function to get highest number from git branches | |
| 111 | -get_highest_from_branches() { | |
| 112 | - local highest=0 | |
| 113 | - | |
| 114 | - # Get all branches (local and remote) | |
| 115 | - branches=$(git branch -a 2>/dev/null || echo "") | |
| 116 | - | |
| 117 | - if [ -n "$branches" ]; then | |
| 118 | - while IFS= read -r branch; do | |
| 119 | - # Clean branch name: remove leading markers and remote prefixes | |
| 120 | - clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||') | |
| 121 | - | |
| 122 | - # Extract feature number if branch matches pattern ###-* | |
| 123 | - if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then | |
| 124 | - number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0") | |
| 125 | - number=$((10#$number)) | |
| 126 | - if [ "$number" -gt "$highest" ]; then | |
| 127 | - highest=$number | |
| 128 | - fi | |
| 129 | - fi | |
| 130 | - done <<< "$branches" | |
| 131 | - fi | |
| 132 | - | |
| 133 | - echo "$highest" | |
| 134 | -} | |
| 135 | - | |
| 136 | -# Function to check existing branches (local and remote) and return next available number | |
| 137 | -check_existing_branches() { | |
| 138 | - local specs_dir="$1" | |
| 139 | - | |
| 140 | - # Fetch all remotes to get latest branch info (suppress errors if no remotes) | |
| 141 | - git fetch --all --prune 2>/dev/null || true | |
| 142 | - | |
| 143 | - # Get highest number from ALL branches (not just matching short name) | |
| 144 | - local highest_branch=$(get_highest_from_branches) | |
| 145 | - | |
| 146 | - # Get highest number from ALL specs (not just matching short name) | |
| 147 | - local highest_spec=$(get_highest_from_specs "$specs_dir") | |
| 148 | - | |
| 149 | - # Take the maximum of both | |
| 150 | - local max_num=$highest_branch | |
| 151 | - if [ "$highest_spec" -gt "$max_num" ]; then | |
| 152 | - max_num=$highest_spec | |
| 153 | - fi | |
| 154 | - | |
| 155 | - # Return next number | |
| 156 | - echo $((max_num + 1)) | |
| 157 | -} | |
| 158 | - | |
| 159 | -# Function to clean and format a branch name | |
| 160 | -clean_branch_name() { | |
| 161 | - local name="$1" | |
| 162 | - echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//' | |
| 163 | -} | |
| 164 | - | |
| 165 | -# Resolve repository root. Prefer git information when available, but fall back | |
| 166 | -# to searching for repository markers so the workflow still functions in repositories that | |
| 167 | -# were initialised with --no-git. | |
| 168 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 169 | - | |
| 170 | -if git rev-parse --show-toplevel >/dev/null 2>&1; then | |
| 171 | - REPO_ROOT=$(git rev-parse --show-toplevel) | |
| 172 | - HAS_GIT=true | |
| 173 | -else | |
| 174 | - REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")" | |
| 175 | - if [ -z "$REPO_ROOT" ]; then | |
| 176 | - echo "Error: Could not determine repository root. Please run this script from within the repository." >&2 | |
| 177 | - exit 1 | |
| 178 | - fi | |
| 179 | - HAS_GIT=false | |
| 180 | -fi | |
| 181 | - | |
| 182 | -cd "$REPO_ROOT" | |
| 183 | - | |
| 184 | -SPECS_DIR="$REPO_ROOT/specs" | |
| 185 | -mkdir -p "$SPECS_DIR" | |
| 186 | - | |
| 187 | -# Function to generate branch name with stop word filtering and length filtering | |
| 188 | -generate_branch_name() { | |
| 189 | - local description="$1" | |
| 190 | - | |
| 191 | - # Common stop words to filter out | |
| 192 | - local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$" | |
| 193 | - | |
| 194 | - # Convert to lowercase and split into words | |
| 195 | - local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g') | |
| 196 | - | |
| 197 | - # Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original) | |
| 198 | - local meaningful_words=() | |
| 199 | - for word in $clean_name; do | |
| 200 | - # Skip empty words | |
| 201 | - [ -z "$word" ] && continue | |
| 202 | - | |
| 203 | - # Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms) | |
| 204 | - if ! echo "$word" | grep -qiE "$stop_words"; then | |
| 205 | - if [ ${#word} -ge 3 ]; then | |
| 206 | - meaningful_words+=("$word") | |
| 207 | - elif echo "$description" | grep -q "\b${word^^}\b"; then | |
| 208 | - # Keep short words if they appear as uppercase in original (likely acronyms) | |
| 209 | - meaningful_words+=("$word") | |
| 210 | - fi | |
| 211 | - fi | |
| 212 | - done | |
| 213 | - | |
| 214 | - # If we have meaningful words, use first 3-4 of them | |
| 215 | - if [ ${#meaningful_words[@]} -gt 0 ]; then | |
| 216 | - local max_words=3 | |
| 217 | - if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi | |
| 218 | - | |
| 219 | - local result="" | |
| 220 | - local count=0 | |
| 221 | - for word in "${meaningful_words[@]}"; do | |
| 222 | - if [ $count -ge $max_words ]; then break; fi | |
| 223 | - if [ -n "$result" ]; then result="$result-"; fi | |
| 224 | - result="$result$word" | |
| 225 | - count=$((count + 1)) | |
| 226 | - done | |
| 227 | - echo "$result" | |
| 228 | - else | |
| 229 | - # Fallback to original logic if no meaningful words found | |
| 230 | - local cleaned=$(clean_branch_name "$description") | |
| 231 | - echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//' | |
| 232 | - fi | |
| 233 | -} | |
| 234 | - | |
| 235 | -# Generate branch name | |
| 236 | -if [ -n "$SHORT_NAME" ]; then | |
| 237 | - # Use provided short name, just clean it up | |
| 238 | - BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME") | |
| 239 | -else | |
| 240 | - # Generate from description with smart filtering | |
| 241 | - BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION") | |
| 242 | -fi | |
| 243 | - | |
| 244 | -# Determine branch number | |
| 245 | -if [ -z "$BRANCH_NUMBER" ]; then | |
| 246 | - if [ "$HAS_GIT" = true ]; then | |
| 247 | - # Check existing branches on remotes | |
| 248 | - BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR") | |
| 249 | - else | |
| 250 | - # Fall back to local directory check | |
| 251 | - HIGHEST=$(get_highest_from_specs "$SPECS_DIR") | |
| 252 | - BRANCH_NUMBER=$((HIGHEST + 1)) | |
| 253 | - fi | |
| 254 | -fi | |
| 255 | - | |
| 256 | -# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal) | |
| 257 | -FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))") | |
| 258 | -BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}" | |
| 259 | - | |
| 260 | -# GitHub enforces a 244-byte limit on branch names | |
| 261 | -# Validate and truncate if necessary | |
| 262 | -MAX_BRANCH_LENGTH=244 | |
| 263 | -if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then | |
| 264 | - # Calculate how much we need to trim from suffix | |
| 265 | - # Account for: feature number (3) + hyphen (1) = 4 chars | |
| 266 | - MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4)) | |
| 267 | - | |
| 268 | - # Truncate suffix at word boundary if possible | |
| 269 | - TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH) | |
| 270 | - # Remove trailing hyphen if truncation created one | |
| 271 | - TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//') | |
| 272 | - | |
| 273 | - ORIGINAL_BRANCH_NAME="$BRANCH_NAME" | |
| 274 | - BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}" | |
| 275 | - | |
| 276 | - >&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit" | |
| 277 | - >&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)" | |
| 278 | - >&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)" | |
| 279 | -fi | |
| 280 | - | |
| 281 | -if [ "$HAS_GIT" = true ]; then | |
| 282 | - if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then | |
| 283 | - # Check if branch already exists | |
| 284 | - if git branch --list "$BRANCH_NAME" | grep -q .; then | |
| 285 | - >&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number." | |
| 286 | - exit 1 | |
| 287 | - else | |
| 288 | - >&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again." | |
| 289 | - exit 1 | |
| 290 | - fi | |
| 291 | - fi | |
| 292 | -else | |
| 293 | - >&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME" | |
| 294 | -fi | |
| 295 | - | |
| 296 | -FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" | |
| 297 | -mkdir -p "$FEATURE_DIR" | |
| 298 | - | |
| 299 | -TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md" | |
| 300 | -SPEC_FILE="$FEATURE_DIR/spec.md" | |
| 301 | -if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi | |
| 302 | - | |
| 303 | -# Set the SPECIFY_FEATURE environment variable for the current session | |
| 304 | -export SPECIFY_FEATURE="$BRANCH_NAME" | |
| 305 | - | |
| 306 | -if $JSON_MODE; then | |
| 307 | - printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" | |
| 308 | -else | |
| 309 | - echo "BRANCH_NAME: $BRANCH_NAME" | |
| 310 | - echo "SPEC_FILE: $SPEC_FILE" | |
| 311 | - echo "FEATURE_NUM: $FEATURE_NUM" | |
| 312 | - echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME" | |
| 313 | -fi |
D
.specify/scripts/bash/setup-plan.sh
+0
-61
@@ -1,61 +0,0 @@
| 1 | -#!/usr/bin/env bash | |
| 2 | - | |
| 3 | -set -e | |
| 4 | - | |
| 5 | -# Parse command line arguments | |
| 6 | -JSON_MODE=false | |
| 7 | -ARGS=() | |
| 8 | - | |
| 9 | -for arg in "$@"; do | |
| 10 | - case "$arg" in | |
| 11 | - --json) | |
| 12 | - JSON_MODE=true | |
| 13 | - ;; | |
| 14 | - --help|-h) | |
| 15 | - echo "Usage: $0 [--json]" | |
| 16 | - echo " --json Output results in JSON format" | |
| 17 | - echo " --help Show this help message" | |
| 18 | - exit 0 | |
| 19 | - ;; | |
| 20 | - *) | |
| 21 | - ARGS+=("$arg") | |
| 22 | - ;; | |
| 23 | - esac | |
| 24 | -done | |
| 25 | - | |
| 26 | -# Get script directory and load common functions | |
| 27 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 28 | -source "$SCRIPT_DIR/common.sh" | |
| 29 | - | |
| 30 | -# Get all paths and variables from common functions | |
| 31 | -eval $(get_feature_paths) | |
| 32 | - | |
| 33 | -# Check if we're on a proper feature branch (only for git repos) | |
| 34 | -check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1 | |
| 35 | - | |
| 36 | -# Ensure the feature directory exists | |
| 37 | -mkdir -p "$FEATURE_DIR" | |
| 38 | - | |
| 39 | -# Copy plan template if it exists | |
| 40 | -TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md" | |
| 41 | -if [[ -f "$TEMPLATE" ]]; then | |
| 42 | - cp "$TEMPLATE" "$IMPL_PLAN" | |
| 43 | - echo "Copied plan template to $IMPL_PLAN" | |
| 44 | -else | |
| 45 | - echo "Warning: Plan template not found at $TEMPLATE" | |
| 46 | - # Create a basic plan file if template doesn't exist | |
| 47 | - touch "$IMPL_PLAN" | |
| 48 | -fi | |
| 49 | - | |
| 50 | -# Output results | |
| 51 | -if $JSON_MODE; then | |
| 52 | - printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \ | |
| 53 | - "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT" | |
| 54 | -else | |
| 55 | - echo "FEATURE_SPEC: $FEATURE_SPEC" | |
| 56 | - echo "IMPL_PLAN: $IMPL_PLAN" | |
| 57 | - echo "SPECS_DIR: $FEATURE_DIR" | |
| 58 | - echo "BRANCH: $CURRENT_BRANCH" | |
| 59 | - echo "HAS_GIT: $HAS_GIT" | |
| 60 | -fi | |
| 61 | - |
D
.specify/scripts/bash/update-agent-context.sh
+0
-829
@@ -1,829 +0,0 @@
| 1 | -#!/usr/bin/env bash | |
| 2 | - | |
| 3 | -# Update agent context files with information from plan.md | |
| 4 | -# | |
| 5 | -# This script maintains AI agent context files by parsing feature specifications | |
| 6 | -# and updating agent-specific configuration files with project information. | |
| 7 | -# | |
| 8 | -# MAIN FUNCTIONS: | |
| 9 | -# 1. Environment Validation | |
| 10 | -# - Verifies git repository structure and branch information | |
| 11 | -# - Checks for required plan.md files and templates | |
| 12 | -# - Validates file permissions and accessibility | |
| 13 | -# | |
| 14 | -# 2. Plan Data Extraction | |
| 15 | -# - Parses plan.md files to extract project metadata | |
| 16 | -# - Identifies language/version, frameworks, databases, and project types | |
| 17 | -# - Handles missing or incomplete specification data gracefully | |
| 18 | -# | |
| 19 | -# 3. Agent File Management | |
| 20 | -# - Creates new agent context files from templates when needed | |
| 21 | -# - Updates existing agent files with new project information | |
| 22 | -# - Preserves manual additions and custom configurations | |
| 23 | -# - Supports multiple AI agent formats and directory structures | |
| 24 | -# | |
| 25 | -# 4. Content Generation | |
| 26 | -# - Generates language-specific build/test commands | |
| 27 | -# - Creates appropriate project directory structures | |
| 28 | -# - Updates technology stacks and recent changes sections | |
| 29 | -# - Maintains consistent formatting and timestamps | |
| 30 | -# | |
| 31 | -# 5. Multi-Agent Support | |
| 32 | -# - Handles agent-specific file paths and naming conventions | |
| 33 | -# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity | |
| 34 | -# - Can update single agents or all existing agent files | |
| 35 | -# - Creates default Claude file if no agent files exist | |
| 36 | -# | |
| 37 | -# Usage: ./update-agent-context.sh [agent_type] | |
| 38 | -# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli | |
| 39 | -# Leave empty to update all existing agent files | |
| 40 | - | |
| 41 | -set -e | |
| 42 | - | |
| 43 | -# Enable strict error handling | |
| 44 | -set -u | |
| 45 | -set -o pipefail | |
| 46 | - | |
| 47 | -#============================================================================== | |
| 48 | -# Configuration and Global Variables | |
| 49 | -#============================================================================== | |
| 50 | - | |
| 51 | -# Get script directory and load common functions | |
| 52 | -SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| 53 | -source "$SCRIPT_DIR/common.sh" | |
| 54 | - | |
| 55 | -# Get all paths and variables from common functions | |
| 56 | -eval $(get_feature_paths) | |
| 57 | - | |
| 58 | -NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code | |
| 59 | -AGENT_TYPE="${1:-}" | |
| 60 | - | |
| 61 | -# Agent-specific file paths | |
| 62 | -CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" | |
| 63 | -GEMINI_FILE="$REPO_ROOT/GEMINI.md" | |
| 64 | -COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md" | |
| 65 | -CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc" | |
| 66 | -QWEN_FILE="$REPO_ROOT/QWEN.md" | |
| 67 | -AGENTS_FILE="$REPO_ROOT/AGENTS.md" | |
| 68 | -WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md" | |
| 69 | -KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md" | |
| 70 | -AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md" | |
| 71 | -ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md" | |
| 72 | -CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md" | |
| 73 | -QODER_FILE="$REPO_ROOT/QODER.md" | |
| 74 | -AMP_FILE="$REPO_ROOT/AGENTS.md" | |
| 75 | -SHAI_FILE="$REPO_ROOT/SHAI.md" | |
| 76 | -KIRO_FILE="$REPO_ROOT/AGENTS.md" | |
| 77 | -AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md" | |
| 78 | -BOB_FILE="$REPO_ROOT/AGENTS.md" | |
| 79 | - | |
| 80 | -# Template file | |
| 81 | -TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md" | |
| 82 | - | |
| 83 | -# Global variables for parsed plan data | |
| 84 | -NEW_LANG="" | |
| 85 | -NEW_FRAMEWORK="" | |
| 86 | -NEW_DB="" | |
| 87 | -NEW_PROJECT_TYPE="" | |
| 88 | - | |
| 89 | -#============================================================================== | |
| 90 | -# Utility Functions | |
| 91 | -#============================================================================== | |
| 92 | - | |
| 93 | -log_info() { | |
| 94 | - echo "INFO: $1" | |
| 95 | -} | |
| 96 | - | |
| 97 | -log_success() { | |
| 98 | - echo "✓ $1" | |
| 99 | -} | |
| 100 | - | |
| 101 | -log_error() { | |
| 102 | - echo "ERROR: $1" >&2 | |
| 103 | -} | |
| 104 | - | |
| 105 | -log_warning() { | |
| 106 | - echo "WARNING: $1" >&2 | |
| 107 | -} | |
| 108 | - | |
| 109 | -# Cleanup function for temporary files | |
| 110 | -cleanup() { | |
| 111 | - local exit_code=$? | |
| 112 | - rm -f /tmp/agent_update_*_$$ | |
| 113 | - rm -f /tmp/manual_additions_$$ | |
| 114 | - exit $exit_code | |
| 115 | -} | |
| 116 | - | |
| 117 | -# Set up cleanup trap | |
| 118 | -trap cleanup EXIT INT TERM | |
| 119 | - | |
| 120 | -#============================================================================== | |
| 121 | -# Validation Functions | |
| 122 | -#============================================================================== | |
| 123 | - | |
| 124 | -validate_environment() { | |
| 125 | - # Check if we have a current branch/feature (git or non-git) | |
| 126 | - if [[ -z "$CURRENT_BRANCH" ]]; then | |
| 127 | - log_error "Unable to determine current feature" | |
| 128 | - if [[ "$HAS_GIT" == "true" ]]; then | |
| 129 | - log_info "Make sure you're on a feature branch" | |
| 130 | - else | |
| 131 | - log_info "Set SPECIFY_FEATURE environment variable or create a feature first" | |
| 132 | - fi | |
| 133 | - exit 1 | |
| 134 | - fi | |
| 135 | - | |
| 136 | - # Check if plan.md exists | |
| 137 | - if [[ ! -f "$NEW_PLAN" ]]; then | |
| 138 | - log_error "No plan.md found at $NEW_PLAN" | |
| 139 | - log_info "Make sure you're working on a feature with a corresponding spec directory" | |
| 140 | - if [[ "$HAS_GIT" != "true" ]]; then | |
| 141 | - log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first" | |
| 142 | - fi | |
| 143 | - exit 1 | |
| 144 | - fi | |
| 145 | - | |
| 146 | - # Check if template exists (needed for new files) | |
| 147 | - if [[ ! -f "$TEMPLATE_FILE" ]]; then | |
| 148 | - log_warning "Template file not found at $TEMPLATE_FILE" | |
| 149 | - log_warning "Creating new agent files will fail" | |
| 150 | - fi | |
| 151 | -} | |
| 152 | - | |
| 153 | -#============================================================================== | |
| 154 | -# Plan Parsing Functions | |
| 155 | -#============================================================================== | |
| 156 | - | |
| 157 | -extract_plan_field() { | |
| 158 | - local field_pattern="$1" | |
| 159 | - local plan_file="$2" | |
| 160 | - | |
| 161 | - grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \ | |
| 162 | - head -1 | \ | |
| 163 | - sed "s|^\*\*${field_pattern}\*\*: ||" | \ | |
| 164 | - sed 's/^[ \t]*//;s/[ \t]*$//' | \ | |
| 165 | - grep -v "NEEDS CLARIFICATION" | \ | |
| 166 | - grep -v "^N/A$" || echo "" | |
| 167 | -} | |
| 168 | - | |
| 169 | -parse_plan_data() { | |
| 170 | - local plan_file="$1" | |
| 171 | - | |
| 172 | - if [[ ! -f "$plan_file" ]]; then | |
| 173 | - log_error "Plan file not found: $plan_file" | |
| 174 | - return 1 | |
| 175 | - fi | |
| 176 | - | |
| 177 | - if [[ ! -r "$plan_file" ]]; then | |
| 178 | - log_error "Plan file is not readable: $plan_file" | |
| 179 | - return 1 | |
| 180 | - fi | |
| 181 | - | |
| 182 | - log_info "Parsing plan data from $plan_file" | |
| 183 | - | |
| 184 | - NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file") | |
| 185 | - NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file") | |
| 186 | - NEW_DB=$(extract_plan_field "Storage" "$plan_file") | |
| 187 | - NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file") | |
| 188 | - | |
| 189 | - # Log what we found | |
| 190 | - if [[ -n "$NEW_LANG" ]]; then | |
| 191 | - log_info "Found language: $NEW_LANG" | |
| 192 | - else | |
| 193 | - log_warning "No language information found in plan" | |
| 194 | - fi | |
| 195 | - | |
| 196 | - if [[ -n "$NEW_FRAMEWORK" ]]; then | |
| 197 | - log_info "Found framework: $NEW_FRAMEWORK" | |
| 198 | - fi | |
| 199 | - | |
| 200 | - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then | |
| 201 | - log_info "Found database: $NEW_DB" | |
| 202 | - fi | |
| 203 | - | |
| 204 | - if [[ -n "$NEW_PROJECT_TYPE" ]]; then | |
| 205 | - log_info "Found project type: $NEW_PROJECT_TYPE" | |
| 206 | - fi | |
| 207 | -} | |
| 208 | - | |
| 209 | -format_technology_stack() { | |
| 210 | - local lang="$1" | |
| 211 | - local framework="$2" | |
| 212 | - local parts=() | |
| 213 | - | |
| 214 | - # Add non-empty parts | |
| 215 | - [[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang") | |
| 216 | - [[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework") | |
| 217 | - | |
| 218 | - # Join with proper formatting | |
| 219 | - if [[ ${#parts[@]} -eq 0 ]]; then | |
| 220 | - echo "" | |
| 221 | - elif [[ ${#parts[@]} -eq 1 ]]; then | |
| 222 | - echo "${parts[0]}" | |
| 223 | - else | |
| 224 | - # Join multiple parts with " + " | |
| 225 | - local result="${parts[0]}" | |
| 226 | - for ((i=1; i<${#parts[@]}; i++)); do | |
| 227 | - result="$result + ${parts[i]}" | |
| 228 | - done | |
| 229 | - echo "$result" | |
| 230 | - fi | |
| 231 | -} | |
| 232 | - | |
| 233 | -#============================================================================== | |
| 234 | -# Template and Content Generation Functions | |
| 235 | -#============================================================================== | |
| 236 | - | |
| 237 | -get_project_structure() { | |
| 238 | - local project_type="$1" | |
| 239 | - | |
| 240 | - if [[ "$project_type" == *"web"* ]]; then | |
| 241 | - echo "backend/\\nfrontend/\\ntests/" | |
| 242 | - else | |
| 243 | - echo "src/\\ntests/" | |
| 244 | - fi | |
| 245 | -} | |
| 246 | - | |
| 247 | -get_commands_for_language() { | |
| 248 | - local lang="$1" | |
| 249 | - | |
| 250 | - case "$lang" in | |
| 251 | - *"Python"*) | |
| 252 | - echo "cd src && pytest && ruff check ." | |
| 253 | - ;; | |
| 254 | - *"Rust"*) | |
| 255 | - echo "cargo test && cargo clippy" | |
| 256 | - ;; | |
| 257 | - *"JavaScript"*|*"TypeScript"*) | |
| 258 | - echo "npm test \\&\\& npm run lint" | |
| 259 | - ;; | |
| 260 | - *) | |
| 261 | - echo "# Add commands for $lang" | |
| 262 | - ;; | |
| 263 | - esac | |
| 264 | -} | |
| 265 | - | |
| 266 | -get_language_conventions() { | |
| 267 | - local lang="$1" | |
| 268 | - echo "$lang: Follow standard conventions" | |
| 269 | -} | |
| 270 | - | |
| 271 | -create_new_agent_file() { | |
| 272 | - local target_file="$1" | |
| 273 | - local temp_file="$2" | |
| 274 | - local project_name="$3" | |
| 275 | - local current_date="$4" | |
| 276 | - | |
| 277 | - if [[ ! -f "$TEMPLATE_FILE" ]]; then | |
| 278 | - log_error "Template not found at $TEMPLATE_FILE" | |
| 279 | - return 1 | |
| 280 | - fi | |
| 281 | - | |
| 282 | - if [[ ! -r "$TEMPLATE_FILE" ]]; then | |
| 283 | - log_error "Template file is not readable: $TEMPLATE_FILE" | |
| 284 | - return 1 | |
| 285 | - fi | |
| 286 | - | |
| 287 | - log_info "Creating new agent context file from template..." | |
| 288 | - | |
| 289 | - if ! cp "$TEMPLATE_FILE" "$temp_file"; then | |
| 290 | - log_error "Failed to copy template file" | |
| 291 | - return 1 | |
| 292 | - fi | |
| 293 | - | |
| 294 | - # Replace template placeholders | |
| 295 | - local project_structure | |
| 296 | - project_structure=$(get_project_structure "$NEW_PROJECT_TYPE") | |
| 297 | - | |
| 298 | - local commands | |
| 299 | - commands=$(get_commands_for_language "$NEW_LANG") | |
| 300 | - | |
| 301 | - local language_conventions | |
| 302 | - language_conventions=$(get_language_conventions "$NEW_LANG") | |
| 303 | - | |
| 304 | - # Perform substitutions with error checking using safer approach | |
| 305 | - # Escape special characters for sed by using a different delimiter or escaping | |
| 306 | - local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g') | |
| 307 | - local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g') | |
| 308 | - local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g') | |
| 309 | - | |
| 310 | - # Build technology stack and recent change strings conditionally | |
| 311 | - local tech_stack | |
| 312 | - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then | |
| 313 | - tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)" | |
| 314 | - elif [[ -n "$escaped_lang" ]]; then | |
| 315 | - tech_stack="- $escaped_lang ($escaped_branch)" | |
| 316 | - elif [[ -n "$escaped_framework" ]]; then | |
| 317 | - tech_stack="- $escaped_framework ($escaped_branch)" | |
| 318 | - else | |
| 319 | - tech_stack="- ($escaped_branch)" | |
| 320 | - fi | |
| 321 | - | |
| 322 | - local recent_change | |
| 323 | - if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then | |
| 324 | - recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework" | |
| 325 | - elif [[ -n "$escaped_lang" ]]; then | |
| 326 | - recent_change="- $escaped_branch: Added $escaped_lang" | |
| 327 | - elif [[ -n "$escaped_framework" ]]; then | |
| 328 | - recent_change="- $escaped_branch: Added $escaped_framework" | |
| 329 | - else | |
| 330 | - recent_change="- $escaped_branch: Added" | |
| 331 | - fi | |
| 332 | - | |
| 333 | - local substitutions=( | |
| 334 | - "s|\[PROJECT NAME\]|$project_name|" | |
| 335 | - "s|\[DATE\]|$current_date|" | |
| 336 | - "s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|" | |
| 337 | - "s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g" | |
| 338 | - "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|" | |
| 339 | - "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|" | |
| 340 | - "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|" | |
| 341 | - ) | |
| 342 | - | |
| 343 | - for substitution in "${substitutions[@]}"; do | |
| 344 | - if ! sed -i.bak -e "$substitution" "$temp_file"; then | |
| 345 | - log_error "Failed to perform substitution: $substitution" | |
| 346 | - rm -f "$temp_file" "$temp_file.bak" | |
| 347 | - return 1 | |
| 348 | - fi | |
| 349 | - done | |
| 350 | - | |
| 351 | - # Convert \n sequences to actual newlines | |
| 352 | - newline=$(printf '\n') | |
| 353 | - sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" | |
| 354 | - | |
| 355 | - # Clean up backup files | |
| 356 | - rm -f "$temp_file.bak" "$temp_file.bak2" | |
| 357 | - | |
| 358 | - # Prepend Cursor frontmatter for .mdc files so rules are auto-included | |
| 359 | - if [[ "$target_file" == *.mdc ]]; then | |
| 360 | - local frontmatter_file | |
| 361 | - frontmatter_file=$(mktemp) || return 1 | |
| 362 | - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" | |
| 363 | - cat "$temp_file" >> "$frontmatter_file" | |
| 364 | - mv "$frontmatter_file" "$temp_file" | |
| 365 | - fi | |
| 366 | - | |
| 367 | - return 0 | |
| 368 | -} | |
| 369 | - | |
| 370 | - | |
| 371 | - | |
| 372 | - | |
| 373 | -update_existing_agent_file() { | |
| 374 | - local target_file="$1" | |
| 375 | - local current_date="$2" | |
| 376 | - | |
| 377 | - log_info "Updating existing agent context file..." | |
| 378 | - | |
| 379 | - # Use a single temporary file for atomic update | |
| 380 | - local temp_file | |
| 381 | - temp_file=$(mktemp) || { | |
| 382 | - log_error "Failed to create temporary file" | |
| 383 | - return 1 | |
| 384 | - } | |
| 385 | - | |
| 386 | - # Process the file in one pass | |
| 387 | - local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK") | |
| 388 | - local new_tech_entries=() | |
| 389 | - local new_change_entry="" | |
| 390 | - | |
| 391 | - # Prepare new technology entries | |
| 392 | - if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then | |
| 393 | - new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)") | |
| 394 | - fi | |
| 395 | - | |
| 396 | - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then | |
| 397 | - new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)") | |
| 398 | - fi | |
| 399 | - | |
| 400 | - # Prepare new change entry | |
| 401 | - if [[ -n "$tech_stack" ]]; then | |
| 402 | - new_change_entry="- $CURRENT_BRANCH: Added $tech_stack" | |
| 403 | - elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then | |
| 404 | - new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB" | |
| 405 | - fi | |
| 406 | - | |
| 407 | - # Check if sections exist in the file | |
| 408 | - local has_active_technologies=0 | |
| 409 | - local has_recent_changes=0 | |
| 410 | - | |
| 411 | - if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then | |
| 412 | - has_active_technologies=1 | |
| 413 | - fi | |
| 414 | - | |
| 415 | - if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then | |
| 416 | - has_recent_changes=1 | |
| 417 | - fi | |
| 418 | - | |
| 419 | - # Process file line by line | |
| 420 | - local in_tech_section=false | |
| 421 | - local in_changes_section=false | |
| 422 | - local tech_entries_added=false | |
| 423 | - local changes_entries_added=false | |
| 424 | - local existing_changes_count=0 | |
| 425 | - local file_ended=false | |
| 426 | - | |
| 427 | - while IFS= read -r line || [[ -n "$line" ]]; do | |
| 428 | - # Handle Active Technologies section | |
| 429 | - if [[ "$line" == "## Active Technologies" ]]; then | |
| 430 | - echo "$line" >> "$temp_file" | |
| 431 | - in_tech_section=true | |
| 432 | - continue | |
| 433 | - elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then | |
| 434 | - # Add new tech entries before closing the section | |
| 435 | - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then | |
| 436 | - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" | |
| 437 | - tech_entries_added=true | |
| 438 | - fi | |
| 439 | - echo "$line" >> "$temp_file" | |
| 440 | - in_tech_section=false | |
| 441 | - continue | |
| 442 | - elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then | |
| 443 | - # Add new tech entries before empty line in tech section | |
| 444 | - if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then | |
| 445 | - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" | |
| 446 | - tech_entries_added=true | |
| 447 | - fi | |
| 448 | - echo "$line" >> "$temp_file" | |
| 449 | - continue | |
| 450 | - fi | |
| 451 | - | |
| 452 | - # Handle Recent Changes section | |
| 453 | - if [[ "$line" == "## Recent Changes" ]]; then | |
| 454 | - echo "$line" >> "$temp_file" | |
| 455 | - # Add new change entry right after the heading | |
| 456 | - if [[ -n "$new_change_entry" ]]; then | |
| 457 | - echo "$new_change_entry" >> "$temp_file" | |
| 458 | - fi | |
| 459 | - in_changes_section=true | |
| 460 | - changes_entries_added=true | |
| 461 | - continue | |
| 462 | - elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then | |
| 463 | - echo "$line" >> "$temp_file" | |
| 464 | - in_changes_section=false | |
| 465 | - continue | |
| 466 | - elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then | |
| 467 | - # Keep only first 2 existing changes | |
| 468 | - if [[ $existing_changes_count -lt 2 ]]; then | |
| 469 | - echo "$line" >> "$temp_file" | |
| 470 | - ((existing_changes_count++)) | |
| 471 | - fi | |
| 472 | - continue | |
| 473 | - fi | |
| 474 | - | |
| 475 | - # Update timestamp | |
| 476 | - if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then | |
| 477 | - echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file" | |
| 478 | - else | |
| 479 | - echo "$line" >> "$temp_file" | |
| 480 | - fi | |
| 481 | - done < "$target_file" | |
| 482 | - | |
| 483 | - # Post-loop check: if we're still in the Active Technologies section and haven't added new entries | |
| 484 | - if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then | |
| 485 | - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" | |
| 486 | - tech_entries_added=true | |
| 487 | - fi | |
| 488 | - | |
| 489 | - # If sections don't exist, add them at the end of the file | |
| 490 | - if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then | |
| 491 | - echo "" >> "$temp_file" | |
| 492 | - echo "## Active Technologies" >> "$temp_file" | |
| 493 | - printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file" | |
| 494 | - tech_entries_added=true | |
| 495 | - fi | |
| 496 | - | |
| 497 | - if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then | |
| 498 | - echo "" >> "$temp_file" | |
| 499 | - echo "## Recent Changes" >> "$temp_file" | |
| 500 | - echo "$new_change_entry" >> "$temp_file" | |
| 501 | - changes_entries_added=true | |
| 502 | - fi | |
| 503 | - | |
| 504 | - # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion | |
| 505 | - if [[ "$target_file" == *.mdc ]]; then | |
| 506 | - if ! head -1 "$temp_file" | grep -q '^---'; then | |
| 507 | - local frontmatter_file | |
| 508 | - frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; } | |
| 509 | - printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" | |
| 510 | - cat "$temp_file" >> "$frontmatter_file" | |
| 511 | - mv "$frontmatter_file" "$temp_file" | |
| 512 | - fi | |
| 513 | - fi | |
| 514 | - | |
| 515 | - # Move temp file to target atomically | |
| 516 | - if ! mv "$temp_file" "$target_file"; then | |
| 517 | - log_error "Failed to update target file" | |
| 518 | - rm -f "$temp_file" | |
| 519 | - return 1 | |
| 520 | - fi | |
| 521 | - | |
| 522 | - return 0 | |
| 523 | -} | |
| 524 | -#============================================================================== | |
| 525 | -# Main Agent File Update Function | |
| 526 | -#============================================================================== | |
| 527 | - | |
| 528 | -update_agent_file() { | |
| 529 | - local target_file="$1" | |
| 530 | - local agent_name="$2" | |
| 531 | - | |
| 532 | - if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then | |
| 533 | - log_error "update_agent_file requires target_file and agent_name parameters" | |
| 534 | - return 1 | |
| 535 | - fi | |
| 536 | - | |
| 537 | - log_info "Updating $agent_name context file: $target_file" | |
| 538 | - | |
| 539 | - local project_name | |
| 540 | - project_name=$(basename "$REPO_ROOT") | |
| 541 | - local current_date | |
| 542 | - current_date=$(date +%Y-%m-%d) | |
| 543 | - | |
| 544 | - # Create directory if it doesn't exist | |
| 545 | - local target_dir | |
| 546 | - target_dir=$(dirname "$target_file") | |
| 547 | - if [[ ! -d "$target_dir" ]]; then | |
| 548 | - if ! mkdir -p "$target_dir"; then | |
| 549 | - log_error "Failed to create directory: $target_dir" | |
| 550 | - return 1 | |
| 551 | - fi | |
| 552 | - fi | |
| 553 | - | |
| 554 | - if [[ ! -f "$target_file" ]]; then | |
| 555 | - # Create new file from template | |
| 556 | - local temp_file | |
| 557 | - temp_file=$(mktemp) || { | |
| 558 | - log_error "Failed to create temporary file" | |
| 559 | - return 1 | |
| 560 | - } | |
| 561 | - | |
| 562 | - if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then | |
| 563 | - if mv "$temp_file" "$target_file"; then | |
| 564 | - log_success "Created new $agent_name context file" | |
| 565 | - else | |
| 566 | - log_error "Failed to move temporary file to $target_file" | |
| 567 | - rm -f "$temp_file" | |
| 568 | - return 1 | |
| 569 | - fi | |
| 570 | - else | |
| 571 | - log_error "Failed to create new agent file" | |
| 572 | - rm -f "$temp_file" | |
| 573 | - return 1 | |
| 574 | - fi | |
| 575 | - else | |
| 576 | - # Update existing file | |
| 577 | - if [[ ! -r "$target_file" ]]; then | |
| 578 | - log_error "Cannot read existing file: $target_file" | |
| 579 | - return 1 | |
| 580 | - fi | |
| 581 | - | |
| 582 | - if [[ ! -w "$target_file" ]]; then | |
| 583 | - log_error "Cannot write to existing file: $target_file" | |
| 584 | - return 1 | |
| 585 | - fi | |
| 586 | - | |
| 587 | - if update_existing_agent_file "$target_file" "$current_date"; then | |
| 588 | - log_success "Updated existing $agent_name context file" | |
| 589 | - else | |
| 590 | - log_error "Failed to update existing agent file" | |
| 591 | - return 1 | |
| 592 | - fi | |
| 593 | - fi | |
| 594 | - | |
| 595 | - return 0 | |
| 596 | -} | |
| 597 | - | |
| 598 | -#============================================================================== | |
| 599 | -# Agent Selection and Processing | |
| 600 | -#============================================================================== | |
| 601 | - | |
| 602 | -update_specific_agent() { | |
| 603 | - local agent_type="$1" | |
| 604 | - | |
| 605 | - case "$agent_type" in | |
| 606 | - claude) | |
| 607 | - update_agent_file "$CLAUDE_FILE" "Claude Code" | |
| 608 | - ;; | |
| 609 | - gemini) | |
| 610 | - update_agent_file "$GEMINI_FILE" "Gemini CLI" | |
| 611 | - ;; | |
| 612 | - copilot) | |
| 613 | - update_agent_file "$COPILOT_FILE" "GitHub Copilot" | |
| 614 | - ;; | |
| 615 | - cursor-agent) | |
| 616 | - update_agent_file "$CURSOR_FILE" "Cursor IDE" | |
| 617 | - ;; | |
| 618 | - qwen) | |
| 619 | - update_agent_file "$QWEN_FILE" "Qwen Code" | |
| 620 | - ;; | |
| 621 | - opencode) | |
| 622 | - update_agent_file "$AGENTS_FILE" "opencode" | |
| 623 | - ;; | |
| 624 | - codex) | |
| 625 | - update_agent_file "$AGENTS_FILE" "Codex CLI" | |
| 626 | - ;; | |
| 627 | - windsurf) | |
| 628 | - update_agent_file "$WINDSURF_FILE" "Windsurf" | |
| 629 | - ;; | |
| 630 | - kilocode) | |
| 631 | - update_agent_file "$KILOCODE_FILE" "Kilo Code" | |
| 632 | - ;; | |
| 633 | - auggie) | |
| 634 | - update_agent_file "$AUGGIE_FILE" "Auggie CLI" | |
| 635 | - ;; | |
| 636 | - roo) | |
| 637 | - update_agent_file "$ROO_FILE" "Roo Code" | |
| 638 | - ;; | |
| 639 | - codebuddy) | |
| 640 | - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" | |
| 641 | - ;; | |
| 642 | - qodercli) | |
| 643 | - update_agent_file "$QODER_FILE" "Qoder CLI" | |
| 644 | - ;; | |
| 645 | - amp) | |
| 646 | - update_agent_file "$AMP_FILE" "Amp" | |
| 647 | - ;; | |
| 648 | - shai) | |
| 649 | - update_agent_file "$SHAI_FILE" "SHAI" | |
| 650 | - ;; | |
| 651 | - kiro-cli) | |
| 652 | - update_agent_file "$KIRO_FILE" "Kiro CLI" | |
| 653 | - ;; | |
| 654 | - agy) | |
| 655 | - update_agent_file "$AGY_FILE" "Antigravity" | |
| 656 | - ;; | |
| 657 | - bob) | |
| 658 | - update_agent_file "$BOB_FILE" "IBM Bob" | |
| 659 | - ;; | |
| 660 | - generic) | |
| 661 | - log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent." | |
| 662 | - ;; | |
| 663 | - *) | |
| 664 | - log_error "Unknown agent type '$agent_type'" | |
| 665 | - log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic" | |
| 666 | - exit 1 | |
| 667 | - ;; | |
| 668 | - esac | |
| 669 | -} | |
| 670 | - | |
| 671 | -update_all_existing_agents() { | |
| 672 | - local found_agent=false | |
| 673 | - | |
| 674 | - # Check each possible agent file and update if it exists | |
| 675 | - if [[ -f "$CLAUDE_FILE" ]]; then | |
| 676 | - update_agent_file "$CLAUDE_FILE" "Claude Code" | |
| 677 | - found_agent=true | |
| 678 | - fi | |
| 679 | - | |
| 680 | - if [[ -f "$GEMINI_FILE" ]]; then | |
| 681 | - update_agent_file "$GEMINI_FILE" "Gemini CLI" | |
| 682 | - found_agent=true | |
| 683 | - fi | |
| 684 | - | |
| 685 | - if [[ -f "$COPILOT_FILE" ]]; then | |
| 686 | - update_agent_file "$COPILOT_FILE" "GitHub Copilot" | |
| 687 | - found_agent=true | |
| 688 | - fi | |
| 689 | - | |
| 690 | - if [[ -f "$CURSOR_FILE" ]]; then | |
| 691 | - update_agent_file "$CURSOR_FILE" "Cursor IDE" | |
| 692 | - found_agent=true | |
| 693 | - fi | |
| 694 | - | |
| 695 | - if [[ -f "$QWEN_FILE" ]]; then | |
| 696 | - update_agent_file "$QWEN_FILE" "Qwen Code" | |
| 697 | - found_agent=true | |
| 698 | - fi | |
| 699 | - | |
| 700 | - if [[ -f "$AGENTS_FILE" ]]; then | |
| 701 | - update_agent_file "$AGENTS_FILE" "Codex/opencode" | |
| 702 | - found_agent=true | |
| 703 | - fi | |
| 704 | - | |
| 705 | - if [[ -f "$WINDSURF_FILE" ]]; then | |
| 706 | - update_agent_file "$WINDSURF_FILE" "Windsurf" | |
| 707 | - found_agent=true | |
| 708 | - fi | |
| 709 | - | |
| 710 | - if [[ -f "$KILOCODE_FILE" ]]; then | |
| 711 | - update_agent_file "$KILOCODE_FILE" "Kilo Code" | |
| 712 | - found_agent=true | |
| 713 | - fi | |
| 714 | - | |
| 715 | - if [[ -f "$AUGGIE_FILE" ]]; then | |
| 716 | - update_agent_file "$AUGGIE_FILE" "Auggie CLI" | |
| 717 | - found_agent=true | |
| 718 | - fi | |
| 719 | - | |
| 720 | - if [[ -f "$ROO_FILE" ]]; then | |
| 721 | - update_agent_file "$ROO_FILE" "Roo Code" | |
| 722 | - found_agent=true | |
| 723 | - fi | |
| 724 | - | |
| 725 | - if [[ -f "$CODEBUDDY_FILE" ]]; then | |
| 726 | - update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" | |
| 727 | - found_agent=true | |
| 728 | - fi | |
| 729 | - | |
| 730 | - if [[ -f "$SHAI_FILE" ]]; then | |
| 731 | - update_agent_file "$SHAI_FILE" "SHAI" | |
| 732 | - found_agent=true | |
| 733 | - fi | |
| 734 | - | |
| 735 | - if [[ -f "$QODER_FILE" ]]; then | |
| 736 | - update_agent_file "$QODER_FILE" "Qoder CLI" | |
| 737 | - found_agent=true | |
| 738 | - fi | |
| 739 | - | |
| 740 | - if [[ -f "$KIRO_FILE" ]]; then | |
| 741 | - update_agent_file "$KIRO_FILE" "Kiro CLI" | |
| 742 | - found_agent=true | |
| 743 | - fi | |
| 744 | - | |
| 745 | - if [[ -f "$AGY_FILE" ]]; then | |
| 746 | - update_agent_file "$AGY_FILE" "Antigravity" | |
| 747 | - found_agent=true | |
| 748 | - fi | |
| 749 | - if [[ -f "$BOB_FILE" ]]; then | |
| 750 | - update_agent_file "$BOB_FILE" "IBM Bob" | |
| 751 | - found_agent=true | |
| 752 | - fi | |
| 753 | - | |
| 754 | - # If no agent files exist, create a default Claude file | |
| 755 | - if [[ "$found_agent" == false ]]; then | |
| 756 | - log_info "No existing agent files found, creating default Claude file..." | |
| 757 | - update_agent_file "$CLAUDE_FILE" "Claude Code" | |
| 758 | - fi | |
| 759 | -} | |
| 760 | -print_summary() { | |
| 761 | - echo | |
| 762 | - log_info "Summary of changes:" | |
| 763 | - | |
| 764 | - if [[ -n "$NEW_LANG" ]]; then | |
| 765 | - echo " - Added language: $NEW_LANG" | |
| 766 | - fi | |
| 767 | - | |
| 768 | - if [[ -n "$NEW_FRAMEWORK" ]]; then | |
| 769 | - echo " - Added framework: $NEW_FRAMEWORK" | |
| 770 | - fi | |
| 771 | - | |
| 772 | - if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then | |
| 773 | - echo " - Added database: $NEW_DB" | |
| 774 | - fi | |
| 775 | - | |
| 776 | - echo | |
| 777 | - | |
| 778 | - log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]" | |
| 779 | -} | |
| 780 | - | |
| 781 | -#============================================================================== | |
| 782 | -# Main Execution | |
| 783 | -#============================================================================== | |
| 784 | - | |
| 785 | -main() { | |
| 786 | - # Validate environment before proceeding | |
| 787 | - validate_environment | |
| 788 | - | |
| 789 | - log_info "=== Updating agent context files for feature $CURRENT_BRANCH ===" | |
| 790 | - | |
| 791 | - # Parse the plan file to extract project information | |
| 792 | - if ! parse_plan_data "$NEW_PLAN"; then | |
| 793 | - log_error "Failed to parse plan data" | |
| 794 | - exit 1 | |
| 795 | - fi | |
| 796 | - | |
| 797 | - # Process based on agent type argument | |
| 798 | - local success=true | |
| 799 | - | |
| 800 | - if [[ -z "$AGENT_TYPE" ]]; then | |
| 801 | - # No specific agent provided - update all existing agent files | |
| 802 | - log_info "No agent specified, updating all existing agent files..." | |
| 803 | - if ! update_all_existing_agents; then | |
| 804 | - success=false | |
| 805 | - fi | |
| 806 | - else | |
| 807 | - # Specific agent provided - update only that agent | |
| 808 | - log_info "Updating specific agent: $AGENT_TYPE" | |
| 809 | - if ! update_specific_agent "$AGENT_TYPE"; then | |
| 810 | - success=false | |
| 811 | - fi | |
| 812 | - fi | |
| 813 | - | |
| 814 | - # Print summary | |
| 815 | - print_summary | |
| 816 | - | |
| 817 | - if [[ "$success" == true ]]; then | |
| 818 | - log_success "Agent context update completed successfully" | |
| 819 | - exit 0 | |
| 820 | - else | |
| 821 | - log_error "Agent context update completed with errors" | |
| 822 | - exit 1 | |
| 823 | - fi | |
| 824 | -} | |
| 825 | - | |
| 826 | -# Execute main function if script is run directly | |
| 827 | -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then | |
| 828 | - main "$@" | |
| 829 | -fi |
D
.specify/templates/agent-file-template.md
+0
-28
@@ -1,28 +0,0 @@
| 1 | -# [PROJECT NAME] Development Guidelines | |
| 2 | - | |
| 3 | -Auto-generated from all feature plans. Last updated: [DATE] | |
| 4 | - | |
| 5 | -## Active Technologies | |
| 6 | - | |
| 7 | -[EXTRACTED FROM ALL PLAN.MD FILES] | |
| 8 | - | |
| 9 | -## Project Structure | |
| 10 | - | |
| 11 | -```text | |
| 12 | -[ACTUAL STRUCTURE FROM PLANS] | |
| 13 | -``` | |
| 14 | - | |
| 15 | -## Commands | |
| 16 | - | |
| 17 | -[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES] | |
| 18 | - | |
| 19 | -## Code Style | |
| 20 | - | |
| 21 | -[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE] | |
| 22 | - | |
| 23 | -## Recent Changes | |
| 24 | - | |
| 25 | -[LAST 3 FEATURES AND WHAT THEY ADDED] | |
| 26 | - | |
| 27 | -<!-- MANUAL ADDITIONS START --> | |
| 28 | -<!-- MANUAL ADDITIONS END --> |
D
.specify/templates/checklist-template.md
+0
-40
@@ -1,40 +0,0 @@
| 1 | -# [CHECKLIST TYPE] Checklist: [FEATURE NAME] | |
| 2 | - | |
| 3 | -**Purpose**: [Brief description of what this checklist covers] | |
| 4 | -**Created**: [DATE] | |
| 5 | -**Feature**: [Link to spec.md or relevant documentation] | |
| 6 | - | |
| 7 | -**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. | |
| 8 | - | |
| 9 | -<!-- | |
| 10 | - ============================================================================ | |
| 11 | - IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only. | |
| 12 | - | |
| 13 | - The /speckit.checklist command MUST replace these with actual items based on: | |
| 14 | - - User's specific checklist request | |
| 15 | - - Feature requirements from spec.md | |
| 16 | - - Technical context from plan.md | |
| 17 | - - Implementation details from tasks.md | |
| 18 | - | |
| 19 | - DO NOT keep these sample items in the generated checklist file. | |
| 20 | - ============================================================================ | |
| 21 | ---> | |
| 22 | - | |
| 23 | -## [Category 1] | |
| 24 | - | |
| 25 | -- [ ] CHK001 First checklist item with clear action | |
| 26 | -- [ ] CHK002 Second checklist item | |
| 27 | -- [ ] CHK003 Third checklist item | |
| 28 | - | |
| 29 | -## [Category 2] | |
| 30 | - | |
| 31 | -- [ ] CHK004 Another category item | |
| 32 | -- [ ] CHK005 Item with specific criteria | |
| 33 | -- [ ] CHK006 Final item in this category | |
| 34 | - | |
| 35 | -## Notes | |
| 36 | - | |
| 37 | -- Check items off as completed: `[x]` | |
| 38 | -- Add comments or findings inline | |
| 39 | -- Link to relevant resources or documentation | |
| 40 | -- Items are numbered sequentially for easy reference |
D
.specify/templates/constitution-template.md
+0
-50
@@ -1,50 +0,0 @@
| 1 | -# [PROJECT_NAME] Constitution | |
| 2 | -<!-- Example: Spec Constitution, TaskFlow Constitution, etc. --> | |
| 3 | - | |
| 4 | -## Core Principles | |
| 5 | - | |
| 6 | -### [PRINCIPLE_1_NAME] | |
| 7 | -<!-- Example: I. Library-First --> | |
| 8 | -[PRINCIPLE_1_DESCRIPTION] | |
| 9 | -<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries --> | |
| 10 | - | |
| 11 | -### [PRINCIPLE_2_NAME] | |
| 12 | -<!-- Example: II. CLI Interface --> | |
| 13 | -[PRINCIPLE_2_DESCRIPTION] | |
| 14 | -<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats --> | |
| 15 | - | |
| 16 | -### [PRINCIPLE_3_NAME] | |
| 17 | -<!-- Example: III. Test-First (NON-NEGOTIABLE) --> | |
| 18 | -[PRINCIPLE_3_DESCRIPTION] | |
| 19 | -<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced --> | |
| 20 | - | |
| 21 | -### [PRINCIPLE_4_NAME] | |
| 22 | -<!-- Example: IV. Integration Testing --> | |
| 23 | -[PRINCIPLE_4_DESCRIPTION] | |
| 24 | -<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas --> | |
| 25 | - | |
| 26 | -### [PRINCIPLE_5_NAME] | |
| 27 | -<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity --> | |
| 28 | -[PRINCIPLE_5_DESCRIPTION] | |
| 29 | -<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles --> | |
| 30 | - | |
| 31 | -## [SECTION_2_NAME] | |
| 32 | -<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. --> | |
| 33 | - | |
| 34 | -[SECTION_2_CONTENT] | |
| 35 | -<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. --> | |
| 36 | - | |
| 37 | -## [SECTION_3_NAME] | |
| 38 | -<!-- Example: Development Workflow, Review Process, Quality Gates, etc. --> | |
| 39 | - | |
| 40 | -[SECTION_3_CONTENT] | |
| 41 | -<!-- Example: Code review requirements, testing gates, deployment approval process, etc. --> | |
| 42 | - | |
| 43 | -## Governance | |
| 44 | -<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan --> | |
| 45 | - | |
| 46 | -[GOVERNANCE_RULES] | |
| 47 | -<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance --> | |
| 48 | - | |
| 49 | -**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] | |
| 50 | -<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 --> |
D
.specify/templates/plan-template.md
+0
-104
@@ -1,104 +0,0 @@
| 1 | -# Implementation Plan: [FEATURE] | |
| 2 | - | |
| 3 | -**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] | |
| 4 | -**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` | |
| 5 | - | |
| 6 | -**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow. | |
| 7 | - | |
| 8 | -## Summary | |
| 9 | - | |
| 10 | -[Extract from feature spec: primary requirement + technical approach from research] | |
| 11 | - | |
| 12 | -## Technical Context | |
| 13 | - | |
| 14 | -<!-- | |
| 15 | - ACTION REQUIRED: Replace the content in this section with the technical details | |
| 16 | - for the project. The structure here is presented in advisory capacity to guide | |
| 17 | - the iteration process. | |
| 18 | ---> | |
| 19 | - | |
| 20 | -**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION] | |
| 21 | -**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] | |
| 22 | -**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] | |
| 23 | -**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] | |
| 24 | -**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] | |
| 25 | -**Project Type**: [e.g., library/cli/web-service/mobile-app/compiler/desktop-app or NEEDS CLARIFICATION] | |
| 26 | -**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] | |
| 27 | -**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION] | |
| 28 | -**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION] | |
| 29 | - | |
| 30 | -## Constitution Check | |
| 31 | - | |
| 32 | -*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | |
| 33 | - | |
| 34 | -[Gates determined based on constitution file] | |
| 35 | - | |
| 36 | -## Project Structure | |
| 37 | - | |
| 38 | -### Documentation (this feature) | |
| 39 | - | |
| 40 | -```text | |
| 41 | -specs/[###-feature]/ | |
| 42 | -├── plan.md # This file (/speckit.plan command output) | |
| 43 | -├── research.md # Phase 0 output (/speckit.plan command) | |
| 44 | -├── data-model.md # Phase 1 output (/speckit.plan command) | |
| 45 | -├── quickstart.md # Phase 1 output (/speckit.plan command) | |
| 46 | -├── contracts/ # Phase 1 output (/speckit.plan command) | |
| 47 | -└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) | |
| 48 | -``` | |
| 49 | - | |
| 50 | -### Source Code (repository root) | |
| 51 | -<!-- | |
| 52 | - ACTION REQUIRED: Replace the placeholder tree below with the concrete layout | |
| 53 | - for this feature. Delete unused options and expand the chosen structure with | |
| 54 | - real paths (e.g., apps/admin, packages/something). The delivered plan must | |
| 55 | - not include Option labels. | |
| 56 | ---> | |
| 57 | - | |
| 58 | -```text | |
| 59 | -# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT) | |
| 60 | -src/ | |
| 61 | -├── models/ | |
| 62 | -├── services/ | |
| 63 | -├── cli/ | |
| 64 | -└── lib/ | |
| 65 | - | |
| 66 | -tests/ | |
| 67 | -├── contract/ | |
| 68 | -├── integration/ | |
| 69 | -└── unit/ | |
| 70 | - | |
| 71 | -# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected) | |
| 72 | -backend/ | |
| 73 | -├── src/ | |
| 74 | -│ ├── models/ | |
| 75 | -│ ├── services/ | |
| 76 | -│ └── api/ | |
| 77 | -└── tests/ | |
| 78 | - | |
| 79 | -frontend/ | |
| 80 | -├── src/ | |
| 81 | -│ ├── components/ | |
| 82 | -│ ├── pages/ | |
| 83 | -│ └── services/ | |
| 84 | -└── tests/ | |
| 85 | - | |
| 86 | -# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected) | |
| 87 | -api/ | |
| 88 | -└── [same as backend above] | |
| 89 | - | |
| 90 | -ios/ or android/ | |
| 91 | -└── [platform-specific structure: feature modules, UI flows, platform tests] | |
| 92 | -``` | |
| 93 | - | |
| 94 | -**Structure Decision**: [Document the selected structure and reference the real | |
| 95 | -directories captured above] | |
| 96 | - | |
| 97 | -## Complexity Tracking | |
| 98 | - | |
| 99 | -> **Fill ONLY if Constitution Check has violations that must be justified** | |
| 100 | - | |
| 101 | -| Violation | Why Needed | Simpler Alternative Rejected Because | | |
| 102 | -|-----------|------------|-------------------------------------| | |
| 103 | -| [e.g., 4th project] | [current need] | [why 3 projects insufficient] | | |
| 104 | -| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | |
D
.specify/templates/spec-template.md
+0
-115
@@ -1,115 +0,0 @@
| 1 | -# Feature Specification: [FEATURE NAME] | |
| 2 | - | |
| 3 | -**Feature Branch**: `[###-feature-name]` | |
| 4 | -**Created**: [DATE] | |
| 5 | -**Status**: Draft | |
| 6 | -**Input**: User description: "$ARGUMENTS" | |
| 7 | - | |
| 8 | -## User Scenarios & Testing *(mandatory)* | |
| 9 | - | |
| 10 | -<!-- | |
| 11 | - IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance. | |
| 12 | - Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them, | |
| 13 | - you should still have a viable MVP (Minimum Viable Product) that delivers value. | |
| 14 | - | |
| 15 | - Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical. | |
| 16 | - Think of each story as a standalone slice of functionality that can be: | |
| 17 | - - Developed independently | |
| 18 | - - Tested independently | |
| 19 | - - Deployed independently | |
| 20 | - - Demonstrated to users independently | |
| 21 | ---> | |
| 22 | - | |
| 23 | -### User Story 1 - [Brief Title] (Priority: P1) | |
| 24 | - | |
| 25 | -[Describe this user journey in plain language] | |
| 26 | - | |
| 27 | -**Why this priority**: [Explain the value and why it has this priority level] | |
| 28 | - | |
| 29 | -**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"] | |
| 30 | - | |
| 31 | -**Acceptance Scenarios**: | |
| 32 | - | |
| 33 | -1. **Given** [initial state], **When** [action], **Then** [expected outcome] | |
| 34 | -2. **Given** [initial state], **When** [action], **Then** [expected outcome] | |
| 35 | - | |
| 36 | ---- | |
| 37 | - | |
| 38 | -### User Story 2 - [Brief Title] (Priority: P2) | |
| 39 | - | |
| 40 | -[Describe this user journey in plain language] | |
| 41 | - | |
| 42 | -**Why this priority**: [Explain the value and why it has this priority level] | |
| 43 | - | |
| 44 | -**Independent Test**: [Describe how this can be tested independently] | |
| 45 | - | |
| 46 | -**Acceptance Scenarios**: | |
| 47 | - | |
| 48 | -1. **Given** [initial state], **When** [action], **Then** [expected outcome] | |
| 49 | - | |
| 50 | ---- | |
| 51 | - | |
| 52 | -### User Story 3 - [Brief Title] (Priority: P3) | |
| 53 | - | |
| 54 | -[Describe this user journey in plain language] | |
| 55 | - | |
| 56 | -**Why this priority**: [Explain the value and why it has this priority level] | |
| 57 | - | |
| 58 | -**Independent Test**: [Describe how this can be tested independently] | |
| 59 | - | |
| 60 | -**Acceptance Scenarios**: | |
| 61 | - | |
| 62 | -1. **Given** [initial state], **When** [action], **Then** [expected outcome] | |
| 63 | - | |
| 64 | ---- | |
| 65 | - | |
| 66 | -[Add more user stories as needed, each with an assigned priority] | |
| 67 | - | |
| 68 | -### Edge Cases | |
| 69 | - | |
| 70 | -<!-- | |
| 71 | - ACTION REQUIRED: The content in this section represents placeholders. | |
| 72 | - Fill them out with the right edge cases. | |
| 73 | ---> | |
| 74 | - | |
| 75 | -- What happens when [boundary condition]? | |
| 76 | -- How does system handle [error scenario]? | |
| 77 | - | |
| 78 | -## Requirements *(mandatory)* | |
| 79 | - | |
| 80 | -<!-- | |
| 81 | - ACTION REQUIRED: The content in this section represents placeholders. | |
| 82 | - Fill them out with the right functional requirements. | |
| 83 | ---> | |
| 84 | - | |
| 85 | -### Functional Requirements | |
| 86 | - | |
| 87 | -- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"] | |
| 88 | -- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"] | |
| 89 | -- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"] | |
| 90 | -- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"] | |
| 91 | -- **FR-005**: System MUST [behavior, e.g., "log all security events"] | |
| 92 | - | |
| 93 | -*Example of marking unclear requirements:* | |
| 94 | - | |
| 95 | -- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?] | |
| 96 | -- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified] | |
| 97 | - | |
| 98 | -### Key Entities *(include if feature involves data)* | |
| 99 | - | |
| 100 | -- **[Entity 1]**: [What it represents, key attributes without implementation] | |
| 101 | -- **[Entity 2]**: [What it represents, relationships to other entities] | |
| 102 | - | |
| 103 | -## Success Criteria *(mandatory)* | |
| 104 | - | |
| 105 | -<!-- | |
| 106 | - ACTION REQUIRED: Define measurable success criteria. | |
| 107 | - These must be technology-agnostic and measurable. | |
| 108 | ---> | |
| 109 | - | |
| 110 | -### Measurable Outcomes | |
| 111 | - | |
| 112 | -- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"] | |
| 113 | -- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"] | |
| 114 | -- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"] | |
| 115 | -- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"] |
D
.specify/templates/tasks-template.md
+0
-251
@@ -1,251 +0,0 @@
| 1 | ---- | |
| 2 | - | |
| 3 | -description: "Task list template for feature implementation" | |
| 4 | ---- | |
| 5 | - | |
| 6 | -# Tasks: [FEATURE NAME] | |
| 7 | - | |
| 8 | -**Input**: Design documents from `/specs/[###-feature-name]/` | |
| 9 | -**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ | |
| 10 | - | |
| 11 | -**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification. | |
| 12 | - | |
| 13 | -**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. | |
| 14 | - | |
| 15 | -## Format: `[ID] [P?] [Story] Description` | |
| 16 | - | |
| 17 | -- **[P]**: Can run in parallel (different files, no dependencies) | |
| 18 | -- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) | |
| 19 | -- Include exact file paths in descriptions | |
| 20 | - | |
| 21 | -## Path Conventions | |
| 22 | - | |
| 23 | -- **Single project**: `src/`, `tests/` at repository root | |
| 24 | -- **Web app**: `backend/src/`, `frontend/src/` | |
| 25 | -- **Mobile**: `api/src/`, `ios/src/` or `android/src/` | |
| 26 | -- Paths shown below assume single project - adjust based on plan.md structure | |
| 27 | - | |
| 28 | -<!-- | |
| 29 | - ============================================================================ | |
| 30 | - IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only. | |
| 31 | - | |
| 32 | - The /speckit.tasks command MUST replace these with actual tasks based on: | |
| 33 | - - User stories from spec.md (with their priorities P1, P2, P3...) | |
| 34 | - - Feature requirements from plan.md | |
| 35 | - - Entities from data-model.md | |
| 36 | - - Endpoints from contracts/ | |
| 37 | - | |
| 38 | - Tasks MUST be organized by user story so each story can be: | |
| 39 | - - Implemented independently | |
| 40 | - - Tested independently | |
| 41 | - - Delivered as an MVP increment | |
| 42 | - | |
| 43 | - DO NOT keep these sample tasks in the generated tasks.md file. | |
| 44 | - ============================================================================ | |
| 45 | ---> | |
| 46 | - | |
| 47 | -## Phase 1: Setup (Shared Infrastructure) | |
| 48 | - | |
| 49 | -**Purpose**: Project initialization and basic structure | |
| 50 | - | |
| 51 | -- [ ] T001 Create project structure per implementation plan | |
| 52 | -- [ ] T002 Initialize [language] project with [framework] dependencies | |
| 53 | -- [ ] T003 [P] Configure linting and formatting tools | |
| 54 | - | |
| 55 | ---- | |
| 56 | - | |
| 57 | -## Phase 2: Foundational (Blocking Prerequisites) | |
| 58 | - | |
| 59 | -**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented | |
| 60 | - | |
| 61 | -**⚠️ CRITICAL**: No user story work can begin until this phase is complete | |
| 62 | - | |
| 63 | -Examples of foundational tasks (adjust based on your project): | |
| 64 | - | |
| 65 | -- [ ] T004 Setup database schema and migrations framework | |
| 66 | -- [ ] T005 [P] Implement authentication/authorization framework | |
| 67 | -- [ ] T006 [P] Setup API routing and middleware structure | |
| 68 | -- [ ] T007 Create base models/entities that all stories depend on | |
| 69 | -- [ ] T008 Configure error handling and logging infrastructure | |
| 70 | -- [ ] T009 Setup environment configuration management | |
| 71 | - | |
| 72 | -**Checkpoint**: Foundation ready - user story implementation can now begin in parallel | |
| 73 | - | |
| 74 | ---- | |
| 75 | - | |
| 76 | -## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP | |
| 77 | - | |
| 78 | -**Goal**: [Brief description of what this story delivers] | |
| 79 | - | |
| 80 | -**Independent Test**: [How to verify this story works on its own] | |
| 81 | - | |
| 82 | -### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️ | |
| 83 | - | |
| 84 | -> **NOTE: Write these tests FIRST, ensure they FAIL before implementation** | |
| 85 | - | |
| 86 | -- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py | |
| 87 | -- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py | |
| 88 | - | |
| 89 | -### Implementation for User Story 1 | |
| 90 | - | |
| 91 | -- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py | |
| 92 | -- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py | |
| 93 | -- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013) | |
| 94 | -- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py | |
| 95 | -- [ ] T016 [US1] Add validation and error handling | |
| 96 | -- [ ] T017 [US1] Add logging for user story 1 operations | |
| 97 | - | |
| 98 | -**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently | |
| 99 | - | |
| 100 | ---- | |
| 101 | - | |
| 102 | -## Phase 4: User Story 2 - [Title] (Priority: P2) | |
| 103 | - | |
| 104 | -**Goal**: [Brief description of what this story delivers] | |
| 105 | - | |
| 106 | -**Independent Test**: [How to verify this story works on its own] | |
| 107 | - | |
| 108 | -### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️ | |
| 109 | - | |
| 110 | -- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py | |
| 111 | -- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py | |
| 112 | - | |
| 113 | -### Implementation for User Story 2 | |
| 114 | - | |
| 115 | -- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py | |
| 116 | -- [ ] T021 [US2] Implement [Service] in src/services/[service].py | |
| 117 | -- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py | |
| 118 | -- [ ] T023 [US2] Integrate with User Story 1 components (if needed) | |
| 119 | - | |
| 120 | -**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently | |
| 121 | - | |
| 122 | ---- | |
| 123 | - | |
| 124 | -## Phase 5: User Story 3 - [Title] (Priority: P3) | |
| 125 | - | |
| 126 | -**Goal**: [Brief description of what this story delivers] | |
| 127 | - | |
| 128 | -**Independent Test**: [How to verify this story works on its own] | |
| 129 | - | |
| 130 | -### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️ | |
| 131 | - | |
| 132 | -- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py | |
| 133 | -- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py | |
| 134 | - | |
| 135 | -### Implementation for User Story 3 | |
| 136 | - | |
| 137 | -- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py | |
| 138 | -- [ ] T027 [US3] Implement [Service] in src/services/[service].py | |
| 139 | -- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py | |
| 140 | - | |
| 141 | -**Checkpoint**: All user stories should now be independently functional | |
| 142 | - | |
| 143 | ---- | |
| 144 | - | |
| 145 | -[Add more user story phases as needed, following the same pattern] | |
| 146 | - | |
| 147 | ---- | |
| 148 | - | |
| 149 | -## Phase N: Polish & Cross-Cutting Concerns | |
| 150 | - | |
| 151 | -**Purpose**: Improvements that affect multiple user stories | |
| 152 | - | |
| 153 | -- [ ] TXXX [P] Documentation updates in docs/ | |
| 154 | -- [ ] TXXX Code cleanup and refactoring | |
| 155 | -- [ ] TXXX Performance optimization across all stories | |
| 156 | -- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/ | |
| 157 | -- [ ] TXXX Security hardening | |
| 158 | -- [ ] TXXX Run quickstart.md validation | |
| 159 | - | |
| 160 | ---- | |
| 161 | - | |
| 162 | -## Dependencies & Execution Order | |
| 163 | - | |
| 164 | -### Phase Dependencies | |
| 165 | - | |
| 166 | -- **Setup (Phase 1)**: No dependencies - can start immediately | |
| 167 | -- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories | |
| 168 | -- **User Stories (Phase 3+)**: All depend on Foundational phase completion | |
| 169 | - - User stories can then proceed in parallel (if staffed) | |
| 170 | - - Or sequentially in priority order (P1 → P2 → P3) | |
| 171 | -- **Polish (Final Phase)**: Depends on all desired user stories being complete | |
| 172 | - | |
| 173 | -### User Story Dependencies | |
| 174 | - | |
| 175 | -- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories | |
| 176 | -- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable | |
| 177 | -- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable | |
| 178 | - | |
| 179 | -### Within Each User Story | |
| 180 | - | |
| 181 | -- Tests (if included) MUST be written and FAIL before implementation | |
| 182 | -- Models before services | |
| 183 | -- Services before endpoints | |
| 184 | -- Core implementation before integration | |
| 185 | -- Story complete before moving to next priority | |
| 186 | - | |
| 187 | -### Parallel Opportunities | |
| 188 | - | |
| 189 | -- All Setup tasks marked [P] can run in parallel | |
| 190 | -- All Foundational tasks marked [P] can run in parallel (within Phase 2) | |
| 191 | -- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows) | |
| 192 | -- All tests for a user story marked [P] can run in parallel | |
| 193 | -- Models within a story marked [P] can run in parallel | |
| 194 | -- Different user stories can be worked on in parallel by different team members | |
| 195 | - | |
| 196 | ---- | |
| 197 | - | |
| 198 | -## Parallel Example: User Story 1 | |
| 199 | - | |
| 200 | -```bash | |
| 201 | -# Launch all tests for User Story 1 together (if tests requested): | |
| 202 | -Task: "Contract test for [endpoint] in tests/contract/test_[name].py" | |
| 203 | -Task: "Integration test for [user journey] in tests/integration/test_[name].py" | |
| 204 | - | |
| 205 | -# Launch all models for User Story 1 together: | |
| 206 | -Task: "Create [Entity1] model in src/models/[entity1].py" | |
| 207 | -Task: "Create [Entity2] model in src/models/[entity2].py" | |
| 208 | -``` | |
| 209 | - | |
| 210 | ---- | |
| 211 | - | |
| 212 | -## Implementation Strategy | |
| 213 | - | |
| 214 | -### MVP First (User Story 1 Only) | |
| 215 | - | |
| 216 | -1. Complete Phase 1: Setup | |
| 217 | -2. Complete Phase 2: Foundational (CRITICAL - blocks all stories) | |
| 218 | -3. Complete Phase 3: User Story 1 | |
| 219 | -4. **STOP and VALIDATE**: Test User Story 1 independently | |
| 220 | -5. Deploy/demo if ready | |
| 221 | - | |
| 222 | -### Incremental Delivery | |
| 223 | - | |
| 224 | -1. Complete Setup + Foundational → Foundation ready | |
| 225 | -2. Add User Story 1 → Test independently → Deploy/Demo (MVP!) | |
| 226 | -3. Add User Story 2 → Test independently → Deploy/Demo | |
| 227 | -4. Add User Story 3 → Test independently → Deploy/Demo | |
| 228 | -5. Each story adds value without breaking previous stories | |
| 229 | - | |
| 230 | -### Parallel Team Strategy | |
| 231 | - | |
| 232 | -With multiple developers: | |
| 233 | - | |
| 234 | -1. Team completes Setup + Foundational together | |
| 235 | -2. Once Foundational is done: | |
| 236 | - - Developer A: User Story 1 | |
| 237 | - - Developer B: User Story 2 | |
| 238 | - - Developer C: User Story 3 | |
| 239 | -3. Stories complete and integrate independently | |
| 240 | - | |
| 241 | ---- | |
| 242 | - | |
| 243 | -## Notes | |
| 244 | - | |
| 245 | -- [P] tasks = different files, no dependencies | |
| 246 | -- [Story] label maps task to specific user story for traceability | |
| 247 | -- Each user story should be independently completable and testable | |
| 248 | -- Verify tests fail before implementing | |
| 249 | -- Commit after each task or logical group | |
| 250 | -- Stop at any checkpoint to validate story independently | |
| 251 | -- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence |
M
Cargo.lock
+3
-2
@@ -3648,9 +3648,9 @@ dependencies = [
| 3648 | 3648 | |
| 3649 | 3649 | [[package]] |
| 3650 | 3650 | name = "rustls-webpki" |
| 3651 | -version = "0.103.9" | |
| 3651 | +version = "0.103.13" | |
| 3652 | 3652 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 3653 | -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" | |
| 3653 | +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" | |
| 3654 | 3654 | dependencies = [ |
| 3655 | 3655 | "ring", |
| 3656 | 3656 | "rustls-pki-types", |
@@ -3731,6 +3731,7 @@ dependencies = [
| 3731 | 3731 | "tokio-stream", |
| 3732 | 3732 | "totp-rs", |
| 3733 | 3733 | "tower", |
| 3734 | + "tower-http", | |
| 3734 | 3735 | "tower-sessions", |
| 3735 | 3736 | "tower-sessions-redis-store", |
| 3736 | 3737 | "tracing", |
M
crates/rustwarden-core/Cargo.toml
+1
-0
@@ -17,6 +17,7 @@ serde = { version = "1.0", features = ["derive"] }
| 17 | 17 | serde_json = "1.0" |
| 18 | 18 | # Middleware |
| 19 | 19 | tower = { version = "0.5", features = ["limit", "util"] } |
| 20 | +tower-http = { version = "0.6", features = ["cors"] } | |
| 20 | 21 | # Logging |
| 21 | 22 | tracing = "0.1" |
| 22 | 23 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } |
M
crates/rustwarden-core/src/app.rs
+57
-1
@@ -20,7 +20,7 @@
| 20 | 20 | |
| 21 | 21 | use axum::{ |
| 22 | 22 | extract::Request, |
| 23 | - http::{header::HeaderName, HeaderValue}, | |
| 23 | + http::{header::HeaderName, HeaderValue, Method}, | |
| 24 | 24 | middleware::Next, |
| 25 | 25 | response::Response, |
| 26 | 26 | Router, |
@@ -29,6 +29,7 @@ use sea_orm::DatabaseConnection;
| 29 | 29 | use std::path::PathBuf; |
| 30 | 30 | use std::sync::Arc; |
| 31 | 31 | use tower::ServiceBuilder; |
| 32 | +use tower_http::cors::{AllowOrigin, Any, CorsLayer}; | |
| 32 | 33 | |
| 33 | 34 | use crate::auth::session::SessionConfig; |
| 34 | 35 | use crate::config::auth::AuthConfig; |
@@ -424,8 +425,63 @@ impl RustwardenBuilder {
| 424 | 425 | let db_for_ext = db.clone(); |
| 425 | 426 | let i18n_for_app = i18n_service.clone(); // Retained for getter access after build |
| 426 | 427 | let auth_config_for_app = auth_config.clone(); // Retained for getter access after build |
| 428 | + | |
| 429 | + let origins_str = std::env::var("CORS_ALLOWED_ORIGINS").unwrap_or_default(); | |
| 430 | + let cors_layer = if origins_str == "*" { | |
| 431 | + tracing::warn!("CORS_ALLOWED_ORIGINS=* allows any origin. Use only in development."); | |
| 432 | + CorsLayer::new() | |
| 433 | + .allow_origin(Any) | |
| 434 | + .allow_methods([ | |
| 435 | + Method::GET, | |
| 436 | + Method::POST, | |
| 437 | + Method::PUT, | |
| 438 | + Method::DELETE, | |
| 439 | + Method::PATCH, | |
| 440 | + ]) | |
| 441 | + .allow_headers([ | |
| 442 | + axum::http::header::CONTENT_TYPE, | |
| 443 | + axum::http::header::AUTHORIZATION, | |
| 444 | + HeaderName::from_static("x-csrf-token"), | |
| 445 | + ]) | |
| 446 | + .max_age(std::time::Duration::from_secs(3600)) | |
| 447 | + } else if !origins_str.is_empty() { | |
| 448 | + let origins: Vec<axum::http::HeaderValue> = origins_str | |
| 449 | + .split(',') | |
| 450 | + .filter_map(|s| s.trim().parse().ok()) | |
| 451 | + .collect(); | |
| 452 | + if origins.is_empty() { | |
| 453 | + tracing::warn!("CORS_ALLOWED_ORIGINS set but no valid origins parsed"); | |
| 454 | + CorsLayer::new().allow_origin(AllowOrigin::list( | |
| 455 | + std::iter::empty::<axum::http::HeaderValue>(), | |
| 456 | + )) | |
| 457 | + } else { | |
| 458 | + tracing::info!("CORS configured for {} origins", origins.len()); | |
| 459 | + CorsLayer::new() | |
| 460 | + .allow_origin(AllowOrigin::list(origins)) | |
| 461 | + .allow_methods([ | |
| 462 | + Method::GET, | |
| 463 | + Method::POST, | |
| 464 | + Method::PUT, | |
| 465 | + Method::DELETE, | |
| 466 | + Method::PATCH, | |
| 467 | + ]) | |
| 468 | + .allow_headers([ | |
| 469 | + axum::http::header::CONTENT_TYPE, | |
| 470 | + axum::http::header::AUTHORIZATION, | |
| 471 | + HeaderName::from_static("x-csrf-token"), | |
| 472 | + ]) | |
| 473 | + .allow_credentials(true) | |
| 474 | + .max_age(std::time::Duration::from_secs(3600)) | |
| 475 | + } | |
| 476 | + } else { | |
| 477 | + CorsLayer::new().allow_origin(AllowOrigin::list( | |
| 478 | + std::iter::empty::<axum::http::HeaderValue>(), | |
| 479 | + )) | |
| 480 | + }; | |
| 481 | + | |
| 427 | 482 | let app = router.with_state(db).layer( |
| 428 | 483 | ServiceBuilder::new() |
| 484 | + .layer(cors_layer) | |
| 429 | 485 | .layer(axum::middleware::from_fn(security_headers_middleware)) |
| 430 | 486 | .layer_fn(move |service| AdminService::new(service, db_for_admin.clone())) |
| 431 | 487 | .layer_fn(move |service| AuthService::new(service, db_for_auth.clone())) |
M
crates/rustwarden-core/src/auth/handlers.rs
+4
-21
@@ -286,6 +286,7 @@ pub async fn login(
| 286 | 286 | State(db): State<DatabaseConnection>, |
| 287 | 287 | headers: HeaderMap, |
| 288 | 288 | _user_session: Session, |
| 289 | + Extension(auth_config): Extension<Arc<AuthConfig>>, | |
| 289 | 290 | hooks: Option<Extension<Arc<dyn RustwardenHooks>>>, |
| 290 | 291 | Json(payload): Json<LoginRequest>, |
| 291 | 292 | ) -> Result<Response, AppError> { |
@@ -383,12 +384,6 @@ pub async fn login(
| 383 | 384 | permissions |
| 384 | 385 | ); |
| 385 | 386 | |
| 386 | - // Load auth configuration | |
| 387 | - let auth_config = AuthConfig::from_env().map_err(|e| { | |
| 388 | - tracing::error!("Failed to load auth config: {}", e); | |
| 389 | - AppError::Config(format!("Auth configuration error: {}", e)) | |
| 390 | - })?; | |
| 391 | - | |
| 392 | 387 | // Generate JWT token |
| 393 | 388 | let access_token = JwtService::generate_token( |
| 394 | 389 | &auth_config, |
@@ -537,6 +532,7 @@ pub struct Verify2faRequest {
| 537 | 532 | pub async fn verify_2fa( |
| 538 | 533 | State(db): State<DatabaseConnection>, |
| 539 | 534 | _user_session: Session, |
| 535 | + Extension(auth_config): Extension<Arc<AuthConfig>>, | |
| 540 | 536 | hooks: Option<Extension<Arc<dyn RustwardenHooks>>>, |
| 541 | 537 | Json(payload): Json<Verify2faRequest>, |
| 542 | 538 | ) -> Result<Response, AppError> { |
@@ -578,10 +574,6 @@ pub async fn verify_2fa(
| 578 | 574 | ) |
| 579 | 575 | .await?; |
| 580 | 576 | |
| 581 | - // Load auth config and generate tokens | |
| 582 | - let auth_config = AuthConfig::from_env() | |
| 583 | - .map_err(|e| AppError::Config(format!("Auth configuration error: {}", e)))?; | |
| 584 | - | |
| 585 | 577 | let access_token = JwtService::generate_token( |
| 586 | 578 | &auth_config, |
| 587 | 579 | user.id, |
@@ -798,6 +790,7 @@ pub async fn logout(
| 798 | 790 | State(db): State<DatabaseConnection>, |
| 799 | 791 | headers: HeaderMap, |
| 800 | 792 | _user_session: Session, |
| 793 | + Extension(auth_config): Extension<Arc<AuthConfig>>, | |
| 801 | 794 | hooks: Option<Extension<Arc<dyn RustwardenHooks>>>, |
| 802 | 795 | ) -> Result<Response, AppError> { |
| 803 | 796 | // Extract JWT from request (cookie or header) - same logic as jwt_extractor |
@@ -824,11 +817,6 @@ pub async fn logout(
| 824 | 817 | })?; |
| 825 | 818 | |
| 826 | 819 | // Validate token to get user info |
| 827 | - let auth_config = AuthConfig::from_env().map_err(|e| { | |
| 828 | - tracing::error!("Failed to load auth config: {}", e); | |
| 829 | - AppError::Config(format!("Auth configuration error: {}", e)) | |
| 830 | - })?; | |
| 831 | - | |
| 832 | 820 | let claims = JwtService::validate_token(&auth_config, &jwt_token)?; |
| 833 | 821 | let user_id = claims.sub; |
| 834 | 822 |
@@ -979,6 +967,7 @@ pub async fn logout(
| 979 | 967 | pub async fn refresh_token( |
| 980 | 968 | State(db): State<DatabaseConnection>, |
| 981 | 969 | headers: HeaderMap, |
| 970 | + Extension(auth_config): Extension<Arc<AuthConfig>>, | |
| 982 | 971 | Json(payload): Json<RefreshRequest>, |
| 983 | 972 | ) -> Result<Response, AppError> { |
| 984 | 973 | // SEC-C2: Per-IP rate limiting |
@@ -1034,12 +1023,6 @@ pub async fn refresh_token(
| 1034 | 1023 | ) |
| 1035 | 1024 | .await?; |
| 1036 | 1025 | |
| 1037 | - // Load auth configuration | |
| 1038 | - let auth_config = AuthConfig::from_env().map_err(|e| { | |
| 1039 | - tracing::error!("Failed to load auth config: {}", e); | |
| 1040 | - AppError::Config(format!("Auth configuration error: {}", e)) | |
| 1041 | - })?; | |
| 1042 | - | |
| 1043 | 1026 | // Generate new JWT access token |
| 1044 | 1027 | let access_token = JwtService::generate_token( |
| 1045 | 1028 | &auth_config, |
M
crates/rustwarden-core/src/auth/oauth_handlers.rs
+1
-3
@@ -101,6 +101,7 @@ pub async fn oauth_callback(
| 101 | 101 | headers: HeaderMap, |
| 102 | 102 | session: Session, |
| 103 | 103 | Extension(registry): Extension<Arc<ProviderRegistry>>, |
| 104 | + Extension(auth_config): Extension<Arc<AuthConfig>>, | |
| 104 | 105 | hooks: Option<Extension<Arc<dyn RustwardenHooks>>>, |
| 105 | 106 | Path(provider_name): Path<String>, |
| 106 | 107 | Query(params): Query<OAuthCallbackQuery>, |
@@ -200,9 +201,6 @@ pub async fn oauth_callback(
| 200 | 201 | ) |
| 201 | 202 | .await?; |
| 202 | 203 | |
| 203 | - let auth_config = AuthConfig::from_env() | |
| 204 | - .map_err(|e| AppError::Config(format!("Auth config error: {}", e)))?; | |
| 205 | - | |
| 206 | 204 | let access_token = JwtService::generate_token( |
| 207 | 205 | &auth_config, |
| 208 | 206 | user.id, |
M
crates/rustwarden-core/src/middleware/admin_helper.rs
+7
-6
@@ -46,13 +46,14 @@ pub async fn extract_admin_claims_from_parts(
| 46 | 46 | return Err(AppError::Unauthorized); |
| 47 | 47 | } |
| 48 | 48 | |
| 49 | - // Validate JWT token | |
| 50 | - let auth_config = AuthConfig::from_env().map_err(|e| { | |
| 51 | - tracing::error!("Failed to load auth config: {}", e); | |
| 52 | - AppError::Config(format!("Auth configuration error: {}", e)) | |
| 53 | - })?; | |
| 49 | + // Validate JWT token — use the shared AuthConfig from request extensions | |
| 50 | + let auth_config = parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() | |
| 51 | + .ok_or_else(|| { | |
| 52 | + tracing::error!("AuthConfig extension missing from request parts"); | |
| 53 | + AppError::Config("Auth configuration not available".to_string()) | |
| 54 | + })?; | |
| 54 | 55 | |
| 55 | - let claims = JwtService::validate_token(&auth_config, &jwt_token).map_err(|_| { | |
| 56 | + let claims = JwtService::validate_token(&*auth_config, &jwt_token).map_err(|_| { | |
| 56 | 57 | tracing::warn!("Invalid JWT token for admin route"); |
| 57 | 58 | AppError::Unauthorized |
| 58 | 59 | })?; |
M
crates/rustwarden-core/src/middleware/admin_required.rs
+4
-2
@@ -72,7 +72,8 @@ pub async fn admin_required(
| 72 | 72 | Some(claims) => claims, |
| 73 | 73 | None => { |
| 74 | 74 | tracing::warn!("Unauthenticated access attempt to admin route: {}", uri); |
| 75 | - let redirect_url = format!("/login?redirect={}", uri); | |
| 75 | + let redirect_url = | |
| 76 | + crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 76 | 77 | return Redirect::to(&redirect_url).into_response(); |
| 77 | 78 | } |
| 78 | 79 | }; |
@@ -111,7 +112,8 @@ pub async fn admin_required_impl(
| 111 | 112 | Some(claims) => claims, |
| 112 | 113 | None => { |
| 113 | 114 | tracing::warn!("Unauthenticated access attempt to admin route: {}", uri); |
| 114 | - let redirect_url = format!("/login?redirect={}", uri); | |
| 115 | + let redirect_url = | |
| 116 | + crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 115 | 117 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 116 | 118 | } |
| 117 | 119 | }; |
M
crates/rustwarden-core/src/middleware/admin_service.rs
+5
-5
@@ -89,10 +89,10 @@ where
| 89 | 89 | return Ok(axum::response::Redirect::to(&redirect_url).into_response()); |
| 90 | 90 | } |
| 91 | 91 | |
| 92 | - // Load auth config and validate token | |
| 93 | - let auth_config = match AuthConfig::from_env() { | |
| 94 | - Ok(config) => config, | |
| 95 | - Err(_) => { | |
| 92 | + // Load auth config from request extensions (injected at startup) | |
| 93 | + let auth_config = match parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() { | |
| 94 | + Some(config) => config, | |
| 95 | + None => { | |
| 96 | 96 | return Ok(( |
| 97 | 97 | axum::http::StatusCode::INTERNAL_SERVER_ERROR, |
| 98 | 98 | "Internal server error", |
@@ -101,7 +101,7 @@ where
| 101 | 101 | } |
| 102 | 102 | }; |
| 103 | 103 | |
| 104 | - let claims = match JwtService::validate_token(&auth_config, &jwt_token) { | |
| 104 | + let claims = match JwtService::validate_token(&*auth_config, &jwt_token) { | |
| 105 | 105 | Ok(claims) => claims, |
| 106 | 106 | Err(_) => { |
| 107 | 107 | let redirect_url = auth_service::sanitize_redirect(uri.path()); |
M
crates/rustwarden-core/src/middleware/auth_required.rs
+29
-18
@@ -11,6 +11,8 @@ use axum::{
| 11 | 11 | }; |
| 12 | 12 | use sea_orm::DatabaseConnection; |
| 13 | 13 | |
| 14 | +use std::sync::Arc; | |
| 15 | + | |
| 14 | 16 | use crate::auth::jwt::{Claims, JwtService}; |
| 15 | 17 | use crate::auth::jwt_extractor; |
| 16 | 18 | use crate::config::auth::AuthConfig; |
@@ -37,12 +39,18 @@ pub async fn extract_user_from_jwt(db: &DatabaseConnection, request: &Request) -
| 37 | 39 | |
| 38 | 40 | /// Internal implementation of extract_user_from_jwt |
| 39 | 41 | async fn extract_user_from_jwt_impl(db: &DatabaseConnection, request: &Request) -> Option<Claims> { |
| 40 | - // Load auth configuration | |
| 41 | - let auth_config = match AuthConfig::from_env() { | |
| 42 | - Ok(config) => config, | |
| 43 | - Err(e) => { | |
| 44 | - tracing::warn!("Failed to load auth config: {}", e); | |
| 45 | - return None; | |
| 42 | + // Load auth configuration from request extensions (injected at startup) | |
| 43 | + let auth_config = match request.extensions().get::<Arc<AuthConfig>>() { | |
| 44 | + Some(config) => config.clone(), | |
| 45 | + None => { | |
| 46 | + tracing::warn!("AuthConfig extension missing, falling back to from_env"); | |
| 47 | + match AuthConfig::from_env() { | |
| 48 | + Ok(config) => Arc::new(config), | |
| 49 | + Err(e) => { | |
| 50 | + tracing::warn!("Failed to load auth config: {}", e); | |
| 51 | + return None; | |
| 52 | + } | |
| 53 | + } | |
| 46 | 54 | } |
| 47 | 55 | }; |
| 48 | 56 |
@@ -62,7 +70,7 @@ async fn extract_user_from_jwt_impl(db: &DatabaseConnection, request: &Request)
| 62 | 70 | } |
| 63 | 71 | |
| 64 | 72 | // Validate JWT token |
| 65 | - match JwtService::validate_token(&auth_config, &jwt_token) { | |
| 73 | + match JwtService::validate_token(&*auth_config, &jwt_token) { | |
| 66 | 74 | Ok(claims) => { |
| 67 | 75 | tracing::debug!("User extracted from JWT: {}", claims.sub); |
| 68 | 76 | Some(claims) |
@@ -116,18 +124,20 @@ pub async fn auth_required_impl(
| 116 | 124 | uri |
| 117 | 125 | ); |
| 118 | 126 | |
| 119 | - // Load auth configuration | |
| 120 | - let auth_config = AuthConfig::from_env().map_err(|e| { | |
| 121 | - tracing::error!("Failed to load auth config: {}", e); | |
| 122 | - AppError::Config(format!("Auth configuration error: {}", e)) | |
| 123 | - })?; | |
| 127 | + // Load auth configuration from request extensions (injected at startup) | |
| 128 | + let auth_config = request.extensions().get::<Arc<AuthConfig>>().cloned() | |
| 129 | + .ok_or_else(|| { | |
| 130 | + tracing::error!("AuthConfig extension missing from request"); | |
| 131 | + AppError::Config("Auth configuration not available".to_string()) | |
| 132 | + })?; | |
| 124 | 133 | |
| 125 | 134 | // Extract JWT from request (cookie or header) |
| 126 | 135 | let jwt_token = match jwt_extractor::extract_jwt_from_request(&request) { |
| 127 | 136 | Ok(token) => token, |
| 128 | 137 | Err(_) => { |
| 129 | 138 | tracing::warn!("Unauthenticated access attempt to: {} (no token)", uri); |
| 130 | - let redirect_url = format!("/login?redirect={}", uri); | |
| 139 | + let redirect_url = | |
| 140 | + crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 131 | 141 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 132 | 142 | } |
| 133 | 143 | }; |
@@ -135,16 +145,17 @@ pub async fn auth_required_impl(
| 135 | 145 | // Check if token is blacklisted |
| 136 | 146 | if JwtBlacklistService::is_blacklisted(db, &jwt_token).await? { |
| 137 | 147 | tracing::warn!("Blacklisted JWT token used for: {}", uri); |
| 138 | - let redirect_url = format!("/login?redirect={}", uri); | |
| 148 | + let redirect_url = crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 139 | 149 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 140 | 150 | } |
| 141 | 151 | |
| 142 | 152 | // Validate JWT token |
| 143 | - let claims = match JwtService::validate_token(&auth_config, &jwt_token) { | |
| 153 | + let claims = match JwtService::validate_token(&*auth_config, &jwt_token) { | |
| 144 | 154 | Ok(claims) => claims, |
| 145 | 155 | Err(e) => { |
| 146 | 156 | tracing::warn!("Invalid JWT token for {}: {}", uri, e); |
| 147 | - let redirect_url = format!("/login?redirect={}", uri); | |
| 157 | + let redirect_url = | |
| 158 | + crate::middleware::auth_service::sanitize_redirect(&uri.to_string()); | |
| 148 | 159 | return Ok(Redirect::to(&redirect_url).into_response()); |
| 149 | 160 | } |
| 150 | 161 | }; |
@@ -208,12 +219,12 @@ pub async fn optional_auth(
| 208 | 219 | |
| 209 | 220 | // Try to extract and validate JWT (but don't fail if missing) |
| 210 | 221 | if let Ok(jwt_token) = jwt_extractor::extract_jwt_from_request(&request) { |
| 211 | - if let Ok(auth_config) = AuthConfig::from_env() { | |
| 222 | + if let Some(auth_config) = request.extensions().get::<Arc<AuthConfig>>().cloned() { | |
| 212 | 223 | if JwtBlacklistService::is_blacklisted(&db, &jwt_token) |
| 213 | 224 | .await |
| 214 | 225 | .is_ok_and(|b| !b) |
| 215 | 226 | { |
| 216 | - if let Ok(claims) = JwtService::validate_token(&auth_config, &jwt_token) { | |
| 227 | + if let Ok(claims) = JwtService::validate_token(&*auth_config, &jwt_token) { | |
| 217 | 228 | tracing::debug!("User authenticated: {} (from JWT)", claims.sub); |
| 218 | 229 | } |
| 219 | 230 | } |
M
crates/rustwarden-core/src/middleware/auth_service.rs
+6
-6
@@ -96,9 +96,9 @@ where
| 96 | 96 | return Ok(axum::response::Redirect::to(&redirect_url).into_response()); |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | - let auth_config = match AuthConfig::from_env() { | |
| 100 | - Ok(config) => config, | |
| 101 | - Err(_) => { | |
| 99 | + let auth_config = match parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() { | |
| 100 | + Some(config) => config, | |
| 101 | + None => { | |
| 102 | 102 | return Ok(( |
| 103 | 103 | axum::http::StatusCode::INTERNAL_SERVER_ERROR, |
| 104 | 104 | "Internal server error", |
@@ -107,7 +107,7 @@ where
| 107 | 107 | } |
| 108 | 108 | }; |
| 109 | 109 | |
| 110 | - let claims = match JwtService::validate_token(&auth_config, &jwt_token) { | |
| 110 | + let claims = match JwtService::validate_token(&*auth_config, &jwt_token) { | |
| 111 | 111 | Ok(claims) => claims, |
| 112 | 112 | Err(_) => { |
| 113 | 113 | tracing::warn!("AuthService: Invalid JWT token, redirecting to login"); |
@@ -231,8 +231,8 @@ where
| 231 | 231 | .unwrap_or(true); |
| 232 | 232 | |
| 233 | 233 | if !is_blacklisted { |
| 234 | - if let Ok(auth_config) = AuthConfig::from_env() { | |
| 235 | - if let Ok(claims) = JwtService::validate_token(&auth_config, &jwt_token) | |
| 234 | + if let Some(auth_config) = parts.extensions.get::<std::sync::Arc<AuthConfig>>().cloned() { | |
| 235 | + if let Ok(claims) = JwtService::validate_token(&*auth_config, &jwt_token) | |
| 236 | 236 | { |
| 237 | 237 | parts.extensions.insert(claims.clone()); |
| 238 | 238 | if let Ok(json) = serde_json::to_string(&claims) { |
A
docs/SECURITY_AUDIT_2026_05.md
+192
-0
@@ -0,0 +1,192 @@
| 1 | +# Audit de sécurité Rustwarden — Mai 2026 | |
| 2 | + | |
| 3 | +**Date :** 4 mai 2026 | |
| 4 | +**Périmètre :** Ensemble du code source (`rustwarden-core`, `rustwarden-ui`, `app`, configuration) | |
| 5 | +**Contexte :** Rustwarden est un framework backend Rust de gestion des utilisateurs et d'authentification (JWT, RBAC, OAuth2/OIDC, 2FA/TOTP, équipes, sessions, emails). Ce n'est pas un gestionnaire de mots de passe. | |
| 6 | + | |
| 7 | +**Niveau de risque global : HAUT** | |
| 8 | + | |
| 9 | +--- | |
| 10 | + | |
| 11 | +## Synthèse | |
| 12 | + | |
| 13 | +| Sévérité | Nombre | Corrigés | Risque accepté | À planifier | | |
| 14 | +|----------|--------|----------|----------------|-------------| | |
| 15 | +| CRITIQUE | 2 | 1 | 1 | 0 | | |
| 16 | +| HAUT | 5 | 4 | 1 | 0 | | |
| 17 | +| MOYEN | 7 | 0 | 0 | 1 (CSP) | | |
| 18 | +| BAS / INFO | 8 | 0 | 0 | 0 | | |
| 19 | + | |
| 20 | +--- | |
| 21 | + | |
| 22 | +## Findings détaillés | |
| 23 | + | |
| 24 | +### CRITIQUE | |
| 25 | + | |
| 26 | +#### C1. Blocklist JWT_SECRET incomplète | |
| 27 | +- **Fichier :** `crates/rustwarden-core/src/config/auth.rs:73-78` | |
| 28 | +- **OWASP :** A02 — Cryptographic Failures | |
| 29 | +- **Description :** Les valeurs placeholder de `.env` et `.env.sample` ne figurent pas dans `KNOWN_EXAMPLES`. Un déploiement copiant ces fichiers sans modifier le secret ne serait pas bloqué au démarrage. | |
| 30 | +- **Décision : RISQUE ACCEPTÉ** — Le `.env` est dans `.gitignore`, seul `.env.sample` est commité. Environnement de développement uniquement. | |
| 31 | + | |
| 32 | +#### C2. Open redirect dans `auth_required` et `admin_required` | |
| 33 | +- **Fichiers :** | |
| 34 | + - `crates/rustwarden-core/src/middleware/auth_required.rs:130,138,147` | |
| 35 | + - `crates/rustwarden-core/src/middleware/admin_required.rs:75,114` | |
| 36 | +- **OWASP :** A01 — Broken Access Control | |
| 37 | +- **Description :** Le paramètre `redirect` utilise le raw URI sans sanitisation (`format!("/login?redirect={}", uri)`), contrairement à `AuthService` qui dispose de `sanitize_redirect()`. Un URI type `//evil.com` est interprété comme protocol-relative par le navigateur. | |
| 38 | +- **Décision : CORRIGÉ** — Réutilisation de `sanitize_redirect()` sur tous les points de redirection. | |
| 39 | + | |
| 40 | +--- | |
| 41 | + | |
| 42 | +### HAUT | |
| 43 | + | |
| 44 | +#### H1. Aucune configuration CORS appliquée | |
| 45 | +- **Fichier :** `crates/rustwarden-core/src/app.rs` (stack middleware) | |
| 46 | +- **OWASP :** A05 — Security Misconfiguration | |
| 47 | +- **Description :** `tower-http` est déclaré avec la feature `cors` dans `rustwarden-ui` mais aucun `CorsLayer` n'est instancié. Pour un framework destiné à des SPA sur des origines différentes, c'est un trou fonctionnel et sécuritaire. | |
| 48 | +- **Décision : CORRIGÉ** — `CorsLayer` configurable ajouté dans `app.rs` via variables d'environnement. | |
| 49 | + | |
| 50 | +#### H2. CSP neutralisée par `unsafe-inline` + `unsafe-eval` | |
| 51 | +- **Fichier :** `crates/rustwarden-core/src/app.rs:589-591` | |
| 52 | +- **OWASP :** A03 — Injection (XSS) | |
| 53 | +- **Description :** `script-src 'self' 'unsafe-inline' 'unsafe-eval'` permet l'exécution de scripts injectés. Alpine.js requiert `eval()` sauf avec le build CSP-compatible `@alpinejs/csp`. HTMX n'utilise pas `eval()` mais peut injecter du HTML via `innerHTML`. | |
| 54 | +- **Décision : PLAN SPÉCIFIQUE** — Voir `docs/plans/PLAN_CSP_HTMX.md`. | |
| 55 | + | |
| 56 | +#### H3. Dépendances vulnérables | |
| 57 | +- **Fichier :** `Cargo.lock` | |
| 58 | +- **OWASP :** A06 — Vulnerable and Outdated Components | |
| 59 | +- **Advisories :** | |
| 60 | + | |
| 61 | +| Crate | Advisory | Impact | | |
| 62 | +|-------|----------|--------| | |
| 63 | +| `rsa 0.9.10` | RUSTSEC-2023-0071 | Marvin Attack (timing). Pas de fix upstream. Risque limité : JWT utilise HS256. | | |
| 64 | +| `rustls-webpki 0.103.9` | RUSTSEC-2026-0104 | Panic DoS dans CRL parsing | | |
| 65 | +| `rustls-webpki 0.103.9` | RUSTSEC-2026-0099 | Bypass wildcard name constraint | | |
| 66 | +| `rand 0.9.2` | RUSTSEC-2026-0097 | Unsoundness avec custom loggers | | |
| 67 | + | |
| 68 | +- **Décision : CORRIGÉ** — `cargo update -p rustls-webpki` appliqué. | |
| 69 | + | |
| 70 | +#### H4. `AuthConfig::from_env()` appelé à chaque requête | |
| 71 | +- **Fichiers :** | |
| 72 | + - `crates/rustwarden-core/src/middleware/auth_required.rs:40-41,119-123` | |
| 73 | + - `crates/rustwarden-core/src/auth/extractors.rs:200` | |
| 74 | + - `crates/rustwarden-core/src/auth/handlers.rs:386,581,826,1037` | |
| 75 | +- **OWASP :** A04 — Insecure Design | |
| 76 | +- **Description :** Au lieu de lire le `Extension<Arc<AuthConfig>>` partagé (injecté en `app.rs:440`), les handlers et middlewares rechargent la config depuis les variables d'env à chaque requête, créant une fenêtre TOCTOU. | |
| 77 | +- **Décision : CORRIGÉ** — Tous les appels remplacés par lecture de l'extension Axum. | |
| 78 | + | |
| 79 | +#### H5. `accept_invalid_certs` sans garde production | |
| 80 | +- **Fichiers :** `config/email.rs:48,76`, `services/email_service.rs:97-98` | |
| 81 | +- **OWASP :** A02 — Cryptographic Failures | |
| 82 | +- **Description :** `SMTP_ACCEPT_INVALID_CERTS=true` n'est pas bloqué en production contrairement à `COOKIE_SECURE`. | |
| 83 | +- **Décision : RISQUE ACCEPTÉ** — Cas d'usage légitime avec certificats TLS auto-signés. Le warning `tracing::warn!` est conservé. | |
| 84 | + | |
| 85 | +--- | |
| 86 | + | |
| 87 | +### MOYEN | |
| 88 | + | |
| 89 | +#### M1. HS256 JWT avec secret symétrique partagé | |
| 90 | +- **Fichier :** `auth/jwt.rs:163-167` | |
| 91 | +- **OWASP :** A02 | |
| 92 | +- **Description :** Tout service validant les JWT peut aussi en forger. Acceptable pour monolithe, problématique pour multi-services. | |
| 93 | +- **Recommandation :** Documenter comme contrainte architecturale. Considérer RS256/ES256 pour déploiements multi-services. | |
| 94 | + | |
| 95 | +#### M2. Rate limiter in-memory non partagé entre instances | |
| 96 | +- **Fichier :** `services/rate_limit_service.rs` | |
| 97 | +- **OWASP :** A04 | |
| 98 | +- **Description :** Rate limiting contournable derrière un load balancer (N × limite par instance). | |
| 99 | +- **Recommandation :** Documenter. Intégrer Redis quand `SESSION_BACKEND=redis`. | |
| 100 | + | |
| 101 | +#### M3. `innerHTML` dans les templates admin | |
| 102 | +- **Fichiers :** `templates/admin/users_list.html:434,459`, `templates/settings/password.html:321,333,349` | |
| 103 | +- **OWASP :** A03 | |
| 104 | +- **Description :** Malgré un commentaire `// SEC-FIX: Use textContent`, le code utilise toujours `innerHTML`. | |
| 105 | +- **Recommandation :** Inclus dans le plan CSP/HTMX (H2). | |
| 106 | + | |
| 107 | +#### M4. TOTP SHA-1 | |
| 108 | +- **Fichier :** `services/totp_service.rs:62` | |
| 109 | +- **OWASP :** A02 | |
| 110 | +- **Description :** Standard RFC 6238 pour compatibilité authenticator apps. Acceptable. | |
| 111 | +- **Recommandation :** Documenter le choix. SHA-256 en option future. | |
| 112 | + | |
| 113 | +#### M5. `.env.sample` JWT_SECRET pas dans la blocklist | |
| 114 | +- **Fichier :** `.env.sample:15` | |
| 115 | +- **OWASP :** A05 | |
| 116 | +- **Description :** Copie directe sans modification → déploiement vulnérable. | |
| 117 | +- **Note :** Couvert par la décision C1 (risque accepté en dev). | |
| 118 | + | |
| 119 | +#### M6. Session backend SeaORM fallback silencieux vers MemoryStore | |
| 120 | +- **Fichier :** `auth/session.rs:95` | |
| 121 | +- **OWASP :** A07 | |
| 122 | +- **Description :** Devrait être une erreur, pas un simple warning. | |
| 123 | +- **Recommandation :** Logger en `error` ou refuser le démarrage. | |
| 124 | + | |
| 125 | +#### M7. Pas de rate limit IP sur le endpoint 2FA | |
| 126 | +- **Fichier :** `auth/handlers.rs:536-541` | |
| 127 | +- **OWASP :** A07 | |
| 128 | +- **Description :** 5 tentatives/challenge mais challenges recréables sans limite IP. | |
| 129 | +- **Recommandation :** Ajouter `check_auth_rate_limit` sur `verify_2fa`. | |
| 130 | + | |
| 131 | +--- | |
| 132 | + | |
| 133 | +### BAS / INFO | |
| 134 | + | |
| 135 | +| # | Description | Fichier | | |
| 136 | +|---|-------------|---------| | |
| 137 | +| L1 | `certificat_imap.pem` en racine — vérifier qu'il s'agit d'un cert public | `./certificat_imap.pem` | | |
| 138 | +| L2 | `AppError::Validation` passe les détails au client | `error.rs:135-140` | | |
| 139 | +| L3 | Mot de passe admin par défaut loggé — limiter au niveau DEBUG | `services/default_admin_service.rs` | | |
| 140 | +| L4 | Chemin relatif `certificat_imap.pem` dans `.env` — fragile | `.env:41,56` | | |
| 141 | +| L5 | HSTS sans `preload` — acceptable, rendre configurable | `app.rs:599-601` | | |
| 142 | +| I1 | `\|safe` filter sur variables `_json` dans Askama — vérifier échappement `</script>` | `templates/settings/preferences.html:312-316` | | |
| 143 | +| I2 | `X-XSS-Protection: 0` correctement implémenté (recommandation moderne) | `app.rs:575-577` | | |
| 144 | +| I3 | SeaORM paramétré partout, zéro raw SQL — conforme à la constitution | Ensemble du code | | |
| 145 | + | |
| 146 | +--- | |
| 147 | + | |
| 148 | +## Points positifs | |
| 149 | + | |
| 150 | +Le framework démontre une maturité sécurité significative : | |
| 151 | + | |
| 152 | +- **Anti-timing attack** : dummy bcrypt hash avec coût identique pour utilisateurs inexistants | |
| 153 | +- **Refresh token rotation** avec détection de rejeu et révocation totale | |
| 154 | +- **Tokens stockés en SHA-256**, jamais en clair (refresh tokens, personal access tokens) | |
| 155 | +- **Session ID recyclé** au login (anti-fixation) | |
| 156 | +- **CSRF constant-time** via `subtle::ConstantTimeEq`, exemption Bearer | |
| 157 | +- **OAuth** : state CSRF constant-time + PKCE + nonce | |
| 158 | +- **AES-256-GCM** pour secrets at-rest (TOTP, OAuth) | |
| 159 | +- **Cookie Secure forcé** en production (`APP_DEBUG=false`) | |
| 160 | +- **TOTP brute-force** : max 5 tentatives/challenge, lockout 15 min | |
| 161 | +- **Validation systématique** via `validator` sur tous les DTOs | |
| 162 | +- **Zéro raw SQL** — règle constitution respectée | |
| 163 | +- **Password complexity** configurable via middleware dédié | |
| 164 | +- **Audit logging** via hooks extensibles (`on_audit_event`) | |
| 165 | + | |
| 166 | +--- | |
| 167 | + | |
| 168 | +## Conformité OWASP Top 10 | |
| 169 | + | |
| 170 | +| Catégorie | Statut | Notes | | |
| 171 | +|-----------|--------|-------| | |
| 172 | +| A01: Broken Access Control | **CORRIGÉ** | Open redirect corrigé, CORS ajouté. RBAC solide. | | |
| 173 | +| A02: Cryptographic Failures | **PARTIEL** | HS256 documenté. AES-256-GCM, bcrypt bien implémentés. accept_invalid_certs accepté (auto-signé). | | |
| 174 | +| A03: Injection | **PLAN** | CSP à renforcer (plan HTMX). Askama auto-escape. Zéro SQL injection. | | |
| 175 | +| A04: Insecure Design | **CORRIGÉ** | AuthConfig centralisé. Rate limiter in-memory documenté. | | |
| 176 | +| A05: Security Misconfiguration | **CORRIGÉ** | CORS configuré. Secrets en env vars. | | |
| 177 | +| A06: Vulnerable Components | **CORRIGÉ** | rustls-webpki mis à jour. rsa en surveillance. | | |
| 178 | +| A07: Auth Failures | **PARTIEL** | 2FA rate limit IP à ajouter. Session backend à clarifier. | | |
| 179 | +| A08: Integrity Failures | PASS | Pas de mécanisme de mise à jour non signé. | | |
| 180 | +| A09: Logging Failures | MINEUR | Détails de validation exposés. Auth events bien loggés. | | |
| 181 | +| A10: SSRF | PASS | URLs OAuth construites depuis config, pas input utilisateur. | | |
| 182 | + | |
| 183 | +--- | |
| 184 | + | |
| 185 | +## Historique des corrections | |
| 186 | + | |
| 187 | +| Date | Finding | Action | | |
| 188 | +|------|---------|--------| | |
| 189 | +| 2026-05-04 | C2 | Open redirect corrigé via `sanitize_redirect()` | | |
| 190 | +| 2026-05-04 | H1 | CorsLayer configurable ajouté | | |
| 191 | +| 2026-05-04 | H3 | `cargo update -p rustls-webpki` | | |
| 192 | +| 2026-05-04 | H4 | AuthConfig centralisé via Extension | |
D
docs/admin_oauth_settings_ui_7ca3f1fd.plan.html
+0
-40
@@ -1,40 +0,0 @@
| 1 | -<!DOCTYPE html><html><head> | |
| 2 | - <title>admin_oauth_settings_ui_7ca3f1fd.plan</title> | |
| 3 | - <meta charset="utf-8"> | |
| 4 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 5 | - | |
| 6 | - <link rel="stylesheet" href="file:////home/monsieurb/.cursor/extensions/shd101wyy.markdown-preview-enhanced-0.8.20-universal/crossnote/dependencies/katex/katex.min.css"> | |
| 7 | - | |
| 8 | - | |
| 9 | - | |
| 10 | - | |
| 11 | - | |
| 12 | - <style> | |
| 13 | - code[class*=language-],pre[class*=language-]{color:#333;background:0 0;font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.4;-moz-tab-size:8;-o-tab-size:8;tab-size:8;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:.8em;overflow:auto;border-radius:3px;background:#f5f5f5}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal;background:#f5f5f5}.token.blockquote,.token.comment{color:#969896}.token.cdata{color:#183691}.token.doctype,.token.macro.property,.token.punctuation,.token.variable{color:#333}.token.builtin,.token.important,.token.keyword,.token.operator,.token.rule{color:#a71d5d}.token.attr-value,.token.regex,.token.string,.token.url{color:#183691}.token.atrule,.token.boolean,.token.code,.token.command,.token.constant,.token.entity,.token.number,.token.property,.token.symbol{color:#0086b3}.token.prolog,.token.selector,.token.tag{color:#63a35c}.token.attr-name,.token.class,.token.class-name,.token.function,.token.id,.token.namespace,.token.pseudo-class,.token.pseudo-element,.token.url-reference .token.variable{color:#795da3}.token.entity{cursor:help}.token.title,.token.title .token.punctuation{font-weight:700;color:#1d3e81}.token.list{color:#ed6a43}.token.inserted{background-color:#eaffea;color:#55a532}.token.deleted{background-color:#ffecec;color:#bd2c00}.token.bold{font-weight:700}.token.italic{font-style:italic}.language-json .token.property{color:#183691}.language-markup .token.tag .token.punctuation{color:#333}.language-css .token.function,code.language-css{color:#0086b3}.language-yaml .token.atrule{color:#63a35c}code.language-yaml{color:#183691}.language-ruby .token.function{color:#333}.language-markdown .token.url{color:#795da3}.language-makefile .token.symbol{color:#795da3}.language-makefile .token.variable{color:#183691}.language-makefile .token.builtin{color:#0086b3}.language-bash .token.keyword{color:#0086b3}pre[data-line]{position:relative;padding:1em 0 1em 3em}pre[data-line] .line-highlight-wrapper{position:absolute;top:0;left:0;background-color:transparent;display:block;width:100%}pre[data-line] .line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}pre[data-line] .line-highlight:before,pre[data-line] .line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}pre[data-line] .line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}html body{font-family:'Helvetica Neue',Helvetica,'Segoe UI',Arial,freesans,sans-serif;font-size:16px;line-height:1.6;color:#333;background-color:#fff;overflow:initial;box-sizing:border-box;word-wrap:break-word}html body>:first-child{margin-top:0}html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{line-height:1.2;margin-top:1em;margin-bottom:16px;color:#000}html body h1{font-size:2.25em;font-weight:300;padding-bottom:.3em}html body h2{font-size:1.75em;font-weight:400;padding-bottom:.3em}html body h3{font-size:1.5em;font-weight:500}html body h4{font-size:1.25em;font-weight:600}html body h5{font-size:1.1em;font-weight:600}html body h6{font-size:1em;font-weight:600}html body h1,html body h2,html body h3,html body h4,html body h5{font-weight:600}html body h5{font-size:1em}html body h6{color:#5c5c5c}html body strong{color:#000}html body del{color:#5c5c5c}html body a:not([href]){color:inherit;text-decoration:none}html body a{color:#08c;text-decoration:none}html body a:hover{color:#00a3f5;text-decoration:none}html body img{max-width:100%}html body>p{margin-top:0;margin-bottom:16px;word-wrap:break-word}html body>ol,html body>ul{margin-bottom:16px}html body ol,html body ul{padding-left:2em}html body ol.no-list,html body ul.no-list{padding:0;list-style-type:none}html body ol ol,html body ol ul,html body ul ol,html body ul ul{margin-top:0;margin-bottom:0}html body li{margin-bottom:0}html body li.task-list-item{list-style:none}html body li>p{margin-top:0;margin-bottom:0}html body .task-list-item-checkbox{margin:0 .2em .25em -1.8em;vertical-align:middle}html body .task-list-item-checkbox:hover{cursor:pointer}html body blockquote{margin:16px 0;font-size:inherit;padding:0 15px;color:#5c5c5c;background-color:#f0f0f0;border-left:4px solid #d6d6d6}html body blockquote>:first-child{margin-top:0}html body blockquote>:last-child{margin-bottom:0}html body hr{height:4px;margin:32px 0;background-color:#d6d6d6;border:0 none}html body table{margin:10px 0 15px 0;border-collapse:collapse;border-spacing:0;display:block;width:100%;overflow:auto;word-break:normal;word-break:keep-all}html body table th{font-weight:700;color:#000}html body table td,html body table th{border:1px solid #d6d6d6;padding:6px 13px}html body dl{padding:0}html body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}html body dl dd{padding:0 16px;margin-bottom:16px}html body code{font-family:Menlo,Monaco,Consolas,'Courier New',monospace;font-size:.85em;color:#000;background-color:#f0f0f0;border-radius:3px;padding:.2em 0}html body code::after,html body code::before{letter-spacing:-.2em;content:'\00a0'}html body pre>code{padding:0;margin:0;word-break:normal;white-space:pre;background:0 0;border:0}html body .highlight{margin-bottom:16px}html body .highlight pre,html body pre{padding:1em;overflow:auto;line-height:1.45;border:#d6d6d6;border-radius:3px}html body .highlight pre{margin-bottom:0;word-break:normal}html body pre code,html body pre tt{display:inline;max-width:initial;padding:0;margin:0;overflow:initial;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}html body pre code:after,html body pre code:before,html body pre tt:after,html body pre tt:before{content:normal}html body blockquote,html body dl,html body ol,html body p,html body pre,html body ul{margin-top:0;margin-bottom:16px}html body kbd{color:#000;border:1px solid #d6d6d6;border-bottom:2px solid #c7c7c7;padding:2px 4px;background-color:#f0f0f0;border-radius:3px}@media print{html body{background-color:#fff}html body h1,html body h2,html body h3,html body h4,html body h5,html body h6{color:#000;page-break-after:avoid}html body blockquote{color:#5c5c5c}html body pre{page-break-inside:avoid}html body table{display:table}html body img{display:block;max-width:100%;max-height:100%}html body code,html body pre{word-wrap:break-word;white-space:pre}}.markdown-preview{width:100%;height:100%;box-sizing:border-box}.markdown-preview ul{list-style:disc}.markdown-preview ul ul{list-style:circle}.markdown-preview ul ul ul{list-style:square}.markdown-preview ol{list-style:decimal}.markdown-preview ol ol,.markdown-preview ul ol{list-style-type:lower-roman}.markdown-preview ol ol ol,.markdown-preview ol ul ol,.markdown-preview ul ol ol,.markdown-preview ul ul ol{list-style-type:lower-alpha}.markdown-preview .newpage,.markdown-preview .pagebreak{page-break-before:always}.markdown-preview pre.line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}.markdown-preview pre.line-numbers>code{position:relative}.markdown-preview pre.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:1em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.markdown-preview pre.line-numbers .line-numbers-rows>span{pointer-events:none;display:block;counter-increment:linenumber}.markdown-preview pre.line-numbers .line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}.markdown-preview .mathjax-exps .MathJax_Display{text-align:center!important}.markdown-preview:not([data-for=preview]) .code-chunk .code-chunk-btn-group{display:none}.markdown-preview:not([data-for=preview]) .code-chunk .status{display:none}.markdown-preview:not([data-for=preview]) .code-chunk .output-div{margin-bottom:16px}.markdown-preview .md-toc{padding:0}.markdown-preview .md-toc .md-toc-link-wrapper .md-toc-link{display:inline;padding:.25rem 0}.markdown-preview .md-toc .md-toc-link-wrapper .md-toc-link div,.markdown-preview .md-toc .md-toc-link-wrapper .md-toc-link p{display:inline}.markdown-preview .md-toc .md-toc-link-wrapper.highlighted .md-toc-link{font-weight:800}.scrollbar-style::-webkit-scrollbar{width:8px}.scrollbar-style::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}.scrollbar-style::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,.66);border:4px solid rgba(150,150,150,.66);background-clip:content-box}html body[for=html-export]:not([data-presentation-mode]){position:relative;width:100%;height:100%;top:0;left:0;margin:0;padding:0;overflow:auto}html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{position:relative;top:0;min-height:100vh}@media screen and (min-width:914px){html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{padding:2em calc(50% - 457px + 2em)}}@media screen and (max-width:914px){html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{padding:2em}}@media screen and (max-width:450px){html body[for=html-export]:not([data-presentation-mode]) .markdown-preview{font-size:14px!important;padding:1em}}@media print{html body[for=html-export]:not([data-presentation-mode]) #sidebar-toc-btn{display:none}}html body[for=html-export]:not([data-presentation-mode]) #sidebar-toc-btn{position:fixed;bottom:8px;left:8px;font-size:28px;cursor:pointer;color:inherit;z-index:99;width:32px;text-align:center;opacity:.4}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] #sidebar-toc-btn{opacity:1}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc{position:fixed;top:0;left:0;width:300px;height:100%;padding:32px 0 48px 0;font-size:14px;box-shadow:0 0 4px rgba(150,150,150,.33);box-sizing:border-box;overflow:auto;background-color:inherit}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar{width:8px}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar-track{border-radius:10px;background-color:transparent}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc::-webkit-scrollbar-thumb{border-radius:5px;background-color:rgba(150,150,150,.66);border:4px solid rgba(150,150,150,.66);background-clip:content-box}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc a{text-decoration:none}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc{padding:0 16px}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper .md-toc-link{display:inline;padding:.25rem 0}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper .md-toc-link div,html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper .md-toc-link p{display:inline}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .md-sidebar-toc .md-toc .md-toc-link-wrapper.highlighted .md-toc-link{font-weight:800}html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{left:300px;width:calc(100% - 300px);padding:2em calc(50% - 457px - 300px / 2);margin:0;box-sizing:border-box}@media screen and (max-width:1274px){html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{padding:2em}}@media screen and (max-width:450px){html body[for=html-export]:not([data-presentation-mode])[html-show-sidebar-toc] .markdown-preview{width:100%}}html body[for=html-export]:not([data-presentation-mode]):not([html-show-sidebar-toc]) .markdown-preview{left:50%;transform:translateX(-50%)}html body[for=html-export]:not([data-presentation-mode]):not([html-show-sidebar-toc]) .md-sidebar-toc{display:none} | |
| 14 | -/* Please visit the URL below for more information: */ | |
| 15 | -/* https://shd101wyy.github.io/markdown-preview-enhanced/#/customize-css */ | |
| 16 | - | |
| 17 | - </style> | |
| 18 | - <!-- The content below will be included at the end of the <head> element. --><script type="text/javascript"> | |
| 19 | - document.addEventListener("DOMContentLoaded", function () { | |
| 20 | - // your code here | |
| 21 | - }); | |
| 22 | -</script></head><body for="html-export"> | |
| 23 | - | |
| 24 | - | |
| 25 | - <div class="crossnote markdown-preview "> | |
| 26 | - | |
| 27 | -<h1 id="refonte-admin-oauth-settings---configuration-complete-via-ui">Refonte Admin OAuth Settings -- Configuration complete via UI </h1> | |
| 28 | -<h2></h2> | |
| 29 | - | |
| 30 | - </div> | |
| 31 | - | |
| 32 | - | |
| 33 | - | |
| 34 | - | |
| 35 | - | |
| 36 | - | |
| 37 | - | |
| 38 | - | |
| 39 | - | |
| 40 | - </body></html> | |
| 40 | > \ No newline at end of file |
A
docs/constitution.md
+150
-0
@@ -0,0 +1,150 @@
| 1 | +<!-- | |
| 2 | +Sync Impact Report: | |
| 3 | +- Version change: (none) → 0.5.2 (initial placement) | |
| 4 | +- Source: Rustwarden/constitution.md — constitution complète importée pour le nouveau projet | |
| 5 | +- Modified principles: N/A (placement initial) | |
| 6 | +- Added sections: Constitution complète (Core Principles I–V, Technical Constraints, Development Workflow, Governance) | |
| 7 | +- Removed sections: None | |
| 8 | +- Templates requiring updates: | |
| 9 | + ✅ plan-template.md (Constitution Check / gates — aligné, pas de modification) | |
| 10 | + ✅ spec-template.md (scope/requirements — aligné, pas de modification) | |
| 11 | + ✅ tasks-template.md (catégorisation tâches — aligné, pas de modification) | |
| 12 | + ✅ .cursor/commands/*.md (référence .specify/memory/constitution.md — valide) | |
| 13 | +- Follow-up TODOs: None | |
| 14 | +--> | |
| 15 | + | |
| 16 | +# Rustwarden Constitution | |
| 17 | + | |
| 18 | +## Core Principles | |
| 19 | + | |
| 20 | +### I. Qualité de Code | |
| 21 | + | |
| 22 | +Toutes les fonctions publiques DOIVENT être documentées avec `///` (doc comments Rust). Le code DOIT respecter les bonnes pratiques Rust (clippy warnings = 0). Les structures de données DOIVENT utiliser `#[derive(Debug, Clone)]` où approprié. La gestion d'erreurs DOIT utiliser `Result<T, E>` avec des types d'erreur explicites. | |
| 23 | + | |
| 24 | +**Rationale**: La documentation et la qualité du code facilitent la maintenance, la collaboration et réduisent les bugs. Rust offre des outils puissants (clippy, rustfmt) qui doivent être utilisés. | |
| 25 | + | |
| 26 | +### II. Architecture Axum et SeaORM | |
| 27 | + | |
| 28 | +Toutes les routes HTTP DOIVENT être définies avec Axum uniquement (`axum::Router`). Toutes les interactions avec la base de données DOIVENT passer par SeaORM (aucun raw SQL autorisé). Les migrations DOIVENT être gérées via le système de migrations SeaORM (`migrations/src/`). Le serveur DOIT utiliser Tokio comme runtime async (requis par Axum). | |
| 29 | + | |
| 30 | +**Documentation SeaORM** (amendement v0.4.1): Toutes les interactions avec la base de données DOIVENT être implémentées en référence à la [documentation officielle SeaORM](https://www.sea-ql.org/SeaORM/docs/index/). Avant d'implémenter une nouvelle fonctionnalité de base de données (requêtes, relations, transactions, etc.), la documentation SeaORM DOIT être consultée pour garantir l'utilisation des bonnes pratiques et des patterns recommandés. Les exemples de code et les patterns documentés dans la documentation SeaORM DOIVENT être privilégiés par rapport aux approches personnalisées. | |
| 31 | + | |
| 32 | +**Implémentation obligatoire des handlers serveur** (amendement v0.5.0): TOUJOURS implémenter les handlers serveur pour que l'API soit fonctionnelle. Les handlers stub (retournant `501 Not Implemented`) sont INTERDITS dans le code de production. Chaque route définie DOIT avoir un handler complet qui : | |
| 33 | +- Injecte l'état nécessaire (`State<DatabaseConnection>`, `Extension<Claims>`, etc.) | |
| 34 | +- Utilise les services métier appropriés | |
| 35 | +- Retourne des réponses fonctionnelles avec données réelles | |
| 36 | +- Gère les erreurs de manière appropriée | |
| 37 | + | |
| 38 | +Les handlers stub sont uniquement autorisés temporairement pendant le développement incrémental, mais DOIVENT être remplacés par des implémentations complètes avant que la feature soit considérée comme terminée. Toute documentation d'API (OpenAPI, guides, exemples) qui référence un endpoint DOIT correspondre à un handler serveur fonctionnel. | |
| 39 | + | |
| 40 | +**Rationale**: Axum et SeaORM sont les frameworks standard pour Rust, offrant sécurité, performance et maintenabilité. L'utilisation exclusive de ces outils garantit la cohérence de l'architecture. La documentation officielle SeaORM contient les meilleures pratiques, les patterns recommandés et les solutions aux problèmes courants. En s'y référant systématiquement, on évite les erreurs d'implémentation, on améliore la maintenabilité et on garantit la compatibilité avec les futures versions de SeaORM. L'implémentation complète des handlers serveur est essentielle pour que l'API soit réellement utilisable par les clients. Des handlers stub créent une fausse impression de fonctionnalité et empêchent les tests d'intégration réels. Cette règle garantit que toute documentation d'API correspond à une implémentation fonctionnelle, évitant les frustrations des développeurs clients. | |
| 41 | + | |
| 42 | +### III. Développement Incrémental | |
| 43 | + | |
| 44 | +Chaque fonctionnalité DOIT être complète et testée avant de passer à la suivante. L'architecture DOIT être modulaire (structure de dossiers claire, séparation des responsabilités). Les user stories DOIVENT être indépendantes et testables individuellement. | |
| 45 | + | |
| 46 | +**Rationale**: Le développement incrémental permet de livrer de la valeur rapidement, facilite les tests et réduit la complexité. Chaque étape doit être validée avant de continuer. | |
| 47 | + | |
| 48 | +### IV. Sécurité par Défaut | |
| 49 | + | |
| 50 | +Tous les secrets DOIVENT être stockés dans des variables d'environnement, JAMAIS dans le code. La validation et la sanitization des entrées utilisateur DOIVENT être effectuées systématiquement. L'authentification et l'autorisation RBAC DOIVENT être implémentées pour toutes les routes protégées. | |
| 51 | + | |
| 52 | +**JWT Authentication Standard** (amendement v0.3.1): Le système DOIT extraire et vérifier les JWT tokens depuis le header `Authorization` au format `Bearer <token>`. Cette extraction DOIT être la méthode principale pour l'authentification API. Les cookies HttpOnly peuvent être utilisés comme méthode alternative pour les clients web, mais le header Authorization a priorité pour permettre aux clients API de surcharger l'authentification par cookie. | |
| 53 | + | |
| 54 | +**Conformité ANSSI Rust** (amendement v0.4.0): Le code DOIT respecter les recommandations du [Guide ANSSI pour le développement sécurisé en Rust](https://anssi-fr.github.io/rust-guide/). Spécifiquement : | |
| 55 | +- Le code DOIT compiler avec `#![deny(warnings)]` activé (ou au minimum `#![warn(warnings)]`) | |
| 56 | +- Toute utilisation de `unsafe` DOIT être justifiée et documentée avec un commentaire `// SAFETY:` expliquant pourquoi `unsafe` est nécessaire et quels invariants doivent être respectés | |
| 57 | +- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (sauf dans les tests unitaires). Toutes les erreurs DOIVENT être gérées explicitement avec `Result<T, E>` et l'opérateur `?` | |
| 58 | +- L'utilisation de `std::mem::uninitialized` est INTERDITE (dépréciée). L'utilisation de `std::mem::MaybeUninit` DOIT être justifiée pour chaque cas d'usage | |
| 59 | +- Les outils `cargo-audit` et `cargo-outdated` DOIVENT être exécutés régulièrement. Les vulnérabilités détectées DOIVENT être corrigées avant chaque release | |
| 60 | +- Toutes les considérations de sécurité, invariants et garanties DOIVENT être documentées | |
| 61 | + | |
| 62 | +Voir `docs/ANSSI_COMPLIANCE_REPORT.md` pour le rapport de conformité détaillé et les actions prioritaires. | |
| 63 | + | |
| 64 | +**Rationale**: La sécurité est critique pour un système cloud. Les secrets dans le code sont une vulnérabilité majeure. La validation des entrées prévient les attaques par injection. L'authentification JWT via header Authorization est le standard de l'industrie pour les APIs REST et permet une authentification stateless sécurisée. Les guidelines ANSSI représentent les meilleures pratiques pour le développement sécurisé en Rust. Leur respect garantit que les garanties de sécurité offertes par Rust ne sont pas invalidées par une utilisation incorrecte du langage. | |
| 65 | + | |
| 66 | +### V. Tests et Validation Continue | |
| 67 | + | |
| 68 | +Tous les composants DOIVENT avoir des tests unitaires et d'intégration. La couverture de tests DOIT être > 80% (objectif > 90% pour les composants critiques). Les tests DOIVENT être exécutés avant chaque commit (`cargo test`). Les tests DOIVENT suivre les bonnes pratiques Rust (structure `#[cfg(test)]`, tests d'intégration dans `tests/integration/`). | |
| 69 | + | |
| 70 | +**Rationale**: Les tests garantissent la qualité et la fiabilité du code. Une couverture élevée réduit les régressions et facilite les refactorings. L'exécution automatique des tests avant commit maintient la qualité du codebase. | |
| 71 | + | |
| 72 | +## Technical Constraints | |
| 73 | + | |
| 74 | +### Framework Requirements | |
| 75 | + | |
| 76 | +- **Rust Version**: Rust 1.91.1+ (Edition 2024) - **IMPORTANT**: Version minimale requise pour Axum/SeaORM | |
| 77 | +- **Web Framework**: Axum 0.8.7+ (dernière version stable) | |
| 78 | +- **ORM**: SeaORM 2.0.x+ (requis pour toutes les interactions DB) | |
| 79 | +- **Runtime**: Tokio 1.x (requis par Axum) | |
| 80 | +- **Database**: PostgreSQL 18+ (recommandé) | |
| 81 | + | |
| 82 | +### Development Environment | |
| 83 | + | |
| 84 | +**Docker pour Base de Données** (amendement v0.5.2): En développement, la base de données PostgreSQL DOIT être lancée via Docker depuis le dossier `database/` à la racine du projet. Le développeur DOIT utiliser le fichier `database/docker-compose.yml` pour démarrer la base de données. La base de données est lancée et gérée par le développeur (pas automatiquement par le système). Cette configuration garantit un environnement de développement cohérent et isolé, facilite le partage de configuration entre développeurs, et permet un reset rapide de la base de données pour les tests. | |
| 85 | + | |
| 86 | +**Rationale**: L'utilisation de Docker pour PostgreSQL en développement standardise l'environnement de développement, évite les conflits de versions entre développeurs, et simplifie la configuration initiale. Le développeur garde le contrôle sur le démarrage/arrêt de la base de données, permettant une meilleure gestion des ressources et une flexibilité dans le workflow de développement. | |
| 87 | + | |
| 88 | +### Code Quality Requirements | |
| 89 | + | |
| 90 | +- Toutes les fonctions publiques DOIVENT avoir une documentation `///` | |
| 91 | +- `cargo clippy -- -D warnings` DOIT passer sans erreur | |
| 92 | +- `cargo fmt` DOIT être appliqué avant chaque commit | |
| 93 | +- Les erreurs DOIVENT utiliser des types explicites (`Result<T, AppError>`) | |
| 94 | +- `#![deny(warnings)]` DOIT être activé dans `src/lib.rs` ou `src/main.rs` (conformité ANSSI) | |
| 95 | +- Les `unwrap()` et `expect()` sont INTERDITS dans le code de production (conformité ANSSI) | |
| 96 | +- Toute utilisation de `unsafe` DOIT être documentée avec `// SAFETY:` (conformité ANSSI) | |
| 97 | + | |
| 98 | +### Testing Requirements | |
| 99 | + | |
| 100 | +- Tests unitaires dans le même fichier avec `#[cfg(test)]` ou dans `tests/unit/` | |
| 101 | +- Tests d'intégration dans `tests/integration/` | |
| 102 | +- Tests de contrat dans `tests/contract/` | |
| 103 | +- Couverture > 80% (objectif > 90% pour auth, security, core services) | |
| 104 | + | |
| 105 | +### Security Requirements | |
| 106 | + | |
| 107 | +- Pas de secrets hardcodés (variables d'environnement uniquement) | |
| 108 | +- Validation de toutes les entrées utilisateur | |
| 109 | +- Hashing des mots de passe (bcrypt, jamais en clair) | |
| 110 | +- JWT tokens: extraction depuis header `Authorization: Bearer <token>` (priorité 1) | |
| 111 | +- Cookies HttpOnly pour les clients web (fallback, priorité 2) | |
| 112 | +- Rate limiting sur les endpoints d'authentification | |
| 113 | +- Logging de tous les événements d'authentification (succès et échecs) | |
| 114 | +- Conformité aux guidelines ANSSI Rust (voir `docs/ANSSI_COMPLIANCE_REPORT.md`) | |
| 115 | +- Code `unsafe` minimisé et documenté avec `// SAFETY:` | |
| 116 | +- Gestion d'erreurs explicite (pas de panics en production) | |
| 117 | + | |
| 118 | +## Development Workflow | |
| 119 | + | |
| 120 | +### Code Review Process | |
| 121 | + | |
| 122 | +Toutes les pull requests DOIVENT être revues avant merge. Les reviewers DOIVENT vérifier la conformité avec cette constitution. Les violations constitutionnelles DOIVENT être corrigées avant merge. | |
| 123 | + | |
| 124 | +### Quality Gates | |
| 125 | + | |
| 126 | +- ✅ `cargo build` passe sans erreur | |
| 127 | +- ✅ `cargo test` passe (tous les tests) | |
| 128 | +- ✅ `cargo clippy -- -D warnings` passe | |
| 129 | +- ✅ `cargo fmt --check` passe | |
| 130 | +- ✅ Couverture de tests > 80% | |
| 131 | +- ✅ Documentation complète (`cargo doc` génère sans warnings) | |
| 132 | +- ✅ `cargo audit` ne signale pas de vulnérabilités (avant release, conformité ANSSI) | |
| 133 | +- ✅ `cargo outdated` a été vérifié (avant release, conformité ANSSI) | |
| 134 | +- ✅ Aucun `unwrap()`/`expect()` dans le code de production (sauf tests, conformité ANSSI) | |
| 135 | +- ✅ Tous les blocs `unsafe` sont documentés avec `// SAFETY:` (conformité ANSSI) | |
| 136 | + | |
| 137 | +### Testing Strategy | |
| 138 | + | |
| 139 | +1. **TDD recommandé**: Écrire les tests avant l'implémentation pour les nouvelles fonctionnalités | |
| 140 | +2. **Tests unitaires**: Pour chaque fonction publique, tester les cas normaux et d'erreur | |
| 141 | +3. **Tests d'intégration**: Pour chaque endpoint API, tester les scénarios complets | |
| 142 | +4. **Tests de contrat**: Pour chaque endpoint public, valider le format de réponse | |
| 143 | + | |
| 144 | +## Governance | |
| 145 | + | |
| 146 | +Cette constitution prévaut sur toutes les autres pratiques du projet. Les amendements nécessitent une documentation, une approbation et un plan de migration si nécessaire. | |
| 147 | + | |
| 148 | +**Version**: 0.5.2 | **Ratified**: 2025-12-09 | **Last Amended**: 2025-03-05 | |
| 149 | + | |
| 150 | +**Compliance**: Toutes les PRs et reviews DOIVENT vérifier la conformité avec cette constitution. La complexité DOIT être justifiée. Utiliser les templates dans `.specify/templates/` pour la documentation des features. |
A
docs/plans/PLAN_CSP_HTMX.md
+246
-0
@@ -0,0 +1,246 @@
| 1 | +# Plan : Renforcement CSP pour la stack HTMX + Alpine.js + DaisyUI | |
| 2 | + | |
| 3 | +**Date :** 4 mai 2026 | |
| 4 | +**Finding :** H2 — CSP neutralisée par `unsafe-inline` et `unsafe-eval` | |
| 5 | +**Fichier principal :** `crates/rustwarden-core/src/app.rs:588-592` | |
| 6 | +**Objectif :** Supprimer `'unsafe-inline'` et `'unsafe-eval'` de la CSP sans casser le fonctionnement de l'UI. | |
| 7 | + | |
| 8 | +--- | |
| 9 | + | |
| 10 | +## État actuel | |
| 11 | + | |
| 12 | +``` | |
| 13 | +script-src 'self' 'unsafe-inline' 'unsafe-eval'; | |
| 14 | +style-src 'self' 'unsafe-inline'; | |
| 15 | +``` | |
| 16 | + | |
| 17 | +### Pourquoi c'est nécessaire aujourd'hui | |
| 18 | + | |
| 19 | +| Directive | Raison | | |
| 20 | +|-----------|--------| | |
| 21 | +| `script-src 'unsafe-eval'` | Alpine.js standard utilise `new Function()` pour évaluer les expressions `x-data`, `x-bind`, etc. | | |
| 22 | +| `script-src 'unsafe-inline'` | Scripts `<script>` inline dans les templates Askama (init Alpine, handlers HTMX). | | |
| 23 | +| `style-src 'unsafe-inline'` | Attributs `style=""` sur les éléments, styles dynamiques Alpine (`x-bind:style`), classes Tailwind générées à la volée par DaisyUI. | | |
| 24 | + | |
| 25 | +--- | |
| 26 | + | |
| 27 | +## Plan en 4 phases | |
| 28 | + | |
| 29 | +### Phase 1 : Migrer vers Alpine.js CSP build (supprime `unsafe-eval`) | |
| 30 | + | |
| 31 | +**Effort :** Faible | |
| 32 | +**Impact :** Supprime `'unsafe-eval'` de `script-src` | |
| 33 | + | |
| 34 | +Alpine.js fournit un build CSP-compatible (`@alpinejs/csp`) qui pré-compile les expressions au lieu d'utiliser `eval()`. | |
| 35 | + | |
| 36 | +#### Actions | |
| 37 | + | |
| 38 | +1. **Remplacer le CDN/import Alpine.js** par `@alpinejs/csp` dans les assets statiques : | |
| 39 | + ```html | |
| 40 | + <!-- AVANT --> | |
| 41 | + <script src="/static/js/alpine.min.js" defer></script> | |
| 42 | + <!-- APRÈS --> | |
| 43 | + <script src="/static/js/alpine-csp.min.js" defer></script> | |
| 44 | + ``` | |
| 45 | + | |
| 46 | +2. **Adapter les expressions Alpine** si nécessaire. Le build CSP ne supporte pas : | |
| 47 | + - `x-data="{ open: false }"` avec des expressions complexes inline | |
| 48 | + - `x-on:click="count++"` avec des expressions JavaScript arbitraires | |
| 49 | + | |
| 50 | + **Alternative :** Déplacer la logique dans des composants Alpine enregistrés : | |
| 51 | + ```javascript | |
| 52 | + // static/js/components.js | |
| 53 | + Alpine.data('dropdown', () => ({ | |
| 54 | + open: false, | |
| 55 | + toggle() { this.open = !this.open } | |
| 56 | + })); | |
| 57 | + ``` | |
| 58 | + ```html | |
| 59 | + <div x-data="dropdown"> | |
| 60 | + <button x-on:click="toggle">Menu</button> | |
| 61 | + </div> | |
| 62 | + ``` | |
| 63 | + | |
| 64 | +3. **Inventaire des templates** utilisant Alpine inline : | |
| 65 | + - `templates/admin/*.html` — modals, dropdowns, toggles | |
| 66 | + - `templates/settings/*.html` — préférences, sécurité TOTP | |
| 67 | + - `templates/login.html` — password toggle, 2FA form | |
| 68 | + | |
| 69 | + Chaque expression `x-data="{ ... }"` et `x-on:click="..."` doit être auditée. | |
| 70 | + | |
| 71 | +4. **Mettre à jour la CSP** : | |
| 72 | + ``` | |
| 73 | + script-src 'self' 'unsafe-inline'; // unsafe-eval supprimé | |
| 74 | + ``` | |
| 75 | + | |
| 76 | +#### Vérification | |
| 77 | +- Toutes les pages avec Alpine fonctionnent (login, admin, settings, profil) | |
| 78 | +- Console navigateur : aucune erreur CSP | |
| 79 | + | |
| 80 | +--- | |
| 81 | + | |
| 82 | +### Phase 2 : Externaliser les scripts inline (supprime `unsafe-inline` pour scripts) | |
| 83 | + | |
| 84 | +**Effort :** Moyen | |
| 85 | +**Impact :** Supprime `'unsafe-inline'` de `script-src` | |
| 86 | + | |
| 87 | +#### Actions | |
| 88 | + | |
| 89 | +1. **Identifier tous les `<script>` inline** dans les templates Askama : | |
| 90 | + ```bash | |
| 91 | + grep -rn '<script>' crates/rustwarden-ui/templates/ | |
| 92 | + ``` | |
| 93 | + | |
| 94 | +2. **Extraire chaque bloc `<script>` dans un fichier `.js` externe** : | |
| 95 | + - `static/js/login.js` — logique login, 2FA, redirect | |
| 96 | + - `static/js/admin.js` — modals, CRUD utilisateurs, gestion rôles | |
| 97 | + - `static/js/settings.js` — password change, TOTP setup, preferences | |
| 98 | + - `static/js/common.js` — CSRF token injection, notifications | |
| 99 | + | |
| 100 | +3. **Gérer les variables serveur** injectées dans les scripts inline. | |
| 101 | + | |
| 102 | + Pattern actuel (Askama → script inline) : | |
| 103 | + ```html | |
| 104 | + <script> | |
| 105 | + const csrfToken = "{{ csrf_token }}"; | |
| 106 | + const userId = {{ user_id }}; | |
| 107 | + </script> | |
| 108 | + ``` | |
| 109 | + | |
| 110 | + Pattern cible (data attributes → script externe) : | |
| 111 | + ```html | |
| 112 | + <div id="app-data" | |
| 113 | + data-csrf-token="{{ csrf_token }}" | |
| 114 | + data-user-id="{{ user_id }}"> | |
| 115 | + </div> | |
| 116 | + <script src="/static/js/settings.js"></script> | |
| 117 | + ``` | |
| 118 | + ```javascript | |
| 119 | + // static/js/settings.js | |
| 120 | + const appData = document.getElementById('app-data').dataset; | |
| 121 | + const csrfToken = appData.csrfToken; | |
| 122 | + ``` | |
| 123 | + | |
| 124 | +4. **Gérer les `|safe` filters Askama** qui injectent du JSON dans `<script>` : | |
| 125 | + | |
| 126 | + Pattern actuel (`preferences.html:312-316`) : | |
| 127 | + ```html | |
| 128 | + <script> | |
| 129 | + const themes = {{ themes_json|safe }}; | |
| 130 | + </script> | |
| 131 | + ``` | |
| 132 | + | |
| 133 | + Pattern cible : | |
| 134 | + ```html | |
| 135 | + <script id="themes-data" type="application/json">{{ themes_json|safe }}</script> | |
| 136 | + <script src="/static/js/preferences.js"></script> | |
| 137 | + ``` | |
| 138 | + ```javascript | |
| 139 | + const themes = JSON.parse(document.getElementById('themes-data').textContent); | |
| 140 | + ``` | |
| 141 | + | |
| 142 | +5. **Configurer HTMX pour fonctionner sans inline scripts** : | |
| 143 | + | |
| 144 | + HTMX n'utilise pas `eval()` par défaut. Cependant, certains attributs HTMX peuvent exécuter du JS : | |
| 145 | + - `hx-on:*` (exécute du JS inline) → Migrer vers des event listeners externes | |
| 146 | + - `hx-vals='js:...'` → Migrer vers `hx-vals` avec JSON statique ou `hx-include` | |
| 147 | + | |
| 148 | + Inventorier les usages : | |
| 149 | + ```bash | |
| 150 | + grep -rn 'hx-on:' crates/rustwarden-ui/templates/ | |
| 151 | + grep -rn "hx-vals.*js:" crates/rustwarden-ui/templates/ | |
| 152 | + ``` | |
| 153 | + | |
| 154 | +6. **Mettre à jour la CSP** : | |
| 155 | + ``` | |
| 156 | + script-src 'self'; // Plus de unsafe-inline ni unsafe-eval | |
| 157 | + ``` | |
| 158 | + | |
| 159 | +#### Vérification | |
| 160 | +- Toutes les fonctionnalités JS opérationnelles | |
| 161 | +- Console navigateur : aucune violation CSP pour scripts | |
| 162 | +- Tests manuels : login, 2FA, admin CRUD, settings | |
| 163 | + | |
| 164 | +--- | |
| 165 | + | |
| 166 | +### Phase 3 : Traiter `innerHTML` (corrige finding M3) | |
| 167 | + | |
| 168 | +**Effort :** Faible | |
| 169 | +**Impact :** Élimine les vecteurs XSS dans le DOM | |
| 170 | + | |
| 171 | +#### Actions | |
| 172 | + | |
| 173 | +1. **Remplacer `innerHTML` par `textContent`** partout où du texte brut est injecté : | |
| 174 | + - `templates/admin/users_list.html:434,459` | |
| 175 | + - `templates/settings/password.html:321,333,349` | |
| 176 | + | |
| 177 | +2. **Pour les cas nécessitant du HTML** (modals avec formatage), utiliser la construction DOM : | |
| 178 | + ```javascript | |
| 179 | + // AVANT | |
| 180 | + element.innerHTML = `<strong>${username}</strong>`; | |
| 181 | + | |
| 182 | + // APRÈS | |
| 183 | + const strong = document.createElement('strong'); | |
| 184 | + strong.textContent = username; | |
| 185 | + element.replaceChildren(strong); | |
| 186 | + ``` | |
| 187 | + | |
| 188 | +3. **Auditer les réponses HTMX** : HTMX injecte les réponses serveur via `innerHTML` par design. C'est sûr tant que : | |
| 189 | + - Les réponses HTML sont générées par Askama (auto-escaped) | |
| 190 | + - Aucune donnée utilisateur non-échappée n'est dans les fragments HTML | |
| 191 | + | |
| 192 | + Vérifier qu'aucun handler ne renvoie du HTML construit par string concatenation. | |
| 193 | + | |
| 194 | +#### Vérification | |
| 195 | +- `grep -rn 'innerHTML' templates/` → zéro résultat (hors HTMX internals) | |
| 196 | +- Tests fonctionnels des modals et formulaires | |
| 197 | + | |
| 198 | +--- | |
| 199 | + | |
| 200 | +### Phase 4 : Réduire `unsafe-inline` pour les styles (optionnel) | |
| 201 | + | |
| 202 | +**Effort :** Élevé | |
| 203 | +**Impact :** Sécurité marginale (XSS via CSS est rare et peu exploitable) | |
| 204 | + | |
| 205 | +`'unsafe-inline'` pour les styles est beaucoup moins dangereux que pour les scripts. Les attaques CSS-only sont limitées (exfiltration de données via `background-image: url(...)`, pas d'exécution de code). | |
| 206 | + | |
| 207 | +#### Contraintes | |
| 208 | + | |
| 209 | +- **Tailwind/DaisyUI** : Les classes utilitaires sont dans le CSS compilé (pas inline). OK. | |
| 210 | +- **Alpine.js `x-bind:style`** : Génère des attributs `style=""` dynamiquement. Nécessiterait des nonces. | |
| 211 | +- **HTMX `htmx-settling`, `htmx-swapping`** : Classes CSS ajoutées dynamiquement. OK (pas de styles inline). | |
| 212 | + | |
| 213 | +#### Si souhaité | |
| 214 | + | |
| 215 | +1. Implémenter un middleware de nonce CSP : | |
| 216 | + - Générer un nonce par requête (`rand::random::<[u8; 16]>()`) | |
| 217 | + - L'injecter dans le header CSP : `style-src 'self' 'nonce-{nonce}'` | |
| 218 | + - Le passer au contexte Askama pour les templates | |
| 219 | + - Ajouter `nonce="{nonce}"` sur chaque `<style>` tag | |
| 220 | + | |
| 221 | +2. Alternative : utiliser des hashes CSP pour les blocs `<style>` statiques. | |
| 222 | + | |
| 223 | +#### Recommandation | |
| 224 | + | |
| 225 | +**Reporter cette phase.** Le gain sécurité est marginal et le coût d'implémentation est élevé avec Alpine.js. Prioriser les phases 1-3 qui éliminent les vrais vecteurs XSS. | |
| 226 | + | |
| 227 | +--- | |
| 228 | + | |
| 229 | +## Résumé des changements CSP par phase | |
| 230 | + | |
| 231 | +| Phase | CSP résultante | `unsafe-eval` | `unsafe-inline` (scripts) | `unsafe-inline` (styles) | | |
| 232 | +|-------|---------------|---------------|--------------------------|--------------------------| | |
| 233 | +| Actuel | `script-src 'self' 'unsafe-inline' 'unsafe-eval'` | Oui | Oui | Oui | | |
| 234 | +| Phase 1 | `script-src 'self' 'unsafe-inline'` | **Non** | Oui | Oui | | |
| 235 | +| Phase 2 | `script-src 'self'` | **Non** | **Non** | Oui | | |
| 236 | +| Phase 3 | (pas de changement CSP, correction DOM) | — | — | — | | |
| 237 | +| Phase 4 | `style-src 'self' 'nonce-...'` | **Non** | **Non** | **Non** | | |
| 238 | + | |
| 239 | +--- | |
| 240 | + | |
| 241 | +## Priorité recommandée | |
| 242 | + | |
| 243 | +1. **Phase 1** (immédiat) — Quick win, faible effort | |
| 244 | +2. **Phase 3** (immédiat) — Quick win, corrections `innerHTML` | |
| 245 | +3. **Phase 2** (1-2 semaines) — Chantier principal | |
| 246 | +4. **Phase 4** (backlog) — Optionnel, gain marginal |
D
docs/spec-remember-me.md
+0
-282
@@ -1,282 +0,0 @@
| 1 | -# Spécification : "Se souvenir de moi" (Remember Me) | |
| 2 | - | |
| 3 | -**Version** : 1.0 | |
| 4 | -**Date** : 2026-03-23 | |
| 5 | -**Composant** : `rustwarden-core` (crate framework) | |
| 6 | -**Statut** : Proposition | |
| 7 | - | |
| 8 | ---- | |
| 9 | - | |
| 10 | -## 1. Contexte et problème | |
| 11 | - | |
| 12 | -Les utilisateurs perdent leur session fréquemment à cause de la combinaison de trois timers : | |
| 13 | - | |
| 14 | -| Timer | Valeur par défaut | Effet | | |
| 15 | -|-------|-------------------|-------| | |
| 16 | -| `JWT_EXPIRATION_MINUTES` | 15 min | Cookie `jwt_token` expiré, requêtes rejetées | | |
| 17 | -| `SESSION_TIMEOUT_MINUTES` | 30 min | Session serveur invalidée après inactivité | | |
| 18 | -| `REFRESH_TOKEN_EXPIRATION_DAYS` | 7 jours | Impossibilité de renouveler le JWT | | |
| 19 | - | |
| 20 | -Sans mécanisme de renouvellement automatique côté client, dès que le JWT expire (15 min), l'utilisateur est déconnecté même si son refresh token est encore valide. | |
| 21 | - | |
| 22 | -## 2. Objectif | |
| 23 | - | |
| 24 | -Ajouter un champ optionnel `remember_me` au flux d'authentification pour permettre aux utilisateurs de maintenir une session longue durée tout en préservant la sécurité du JWT court. | |
| 25 | - | |
| 26 | -**Principe fondamental** : le JWT (access token) reste toujours court (5-15 min). Seule la durée du refresh token et du cookie de refresh changent selon le choix de l'utilisateur. | |
| 27 | - | |
| 28 | -## 3. Changements requis | |
| 29 | - | |
| 30 | -### 3.1 `LoginRequest` — Nouveau champ | |
| 31 | - | |
| 32 | -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs` (ligne 43) | |
| 33 | - | |
| 34 | -```rust | |
| 35 | -#[derive(Debug, Deserialize, Validate)] | |
| 36 | -pub struct LoginRequest { | |
| 37 | - #[validate(length(min = 1))] | |
| 38 | - pub username: String, | |
| 39 | - #[validate(length(min = 1))] | |
| 40 | - pub password: String, | |
| 41 | - /// Si true, le refresh token a une durée de vie étendue. | |
| 42 | - /// Défaut : false (rétrocompatibilité). | |
| 43 | - #[serde(default)] | |
| 44 | - pub remember_me: bool, | |
| 45 | -} | |
| 46 | -``` | |
| 47 | - | |
| 48 | -`#[serde(default)]` garantit la **rétrocompatibilité** : les clients existants qui n'envoient pas ce champ obtiennent `false`. | |
| 49 | - | |
| 50 | -### 3.2 `AuthConfig` — Nouvelle variable d'environnement | |
| 51 | - | |
| 52 | -**Fichier** : `crates/rustwarden-core/src/config/auth.rs` | |
| 53 | - | |
| 54 | -```rust | |
| 55 | -pub struct AuthConfig { | |
| 56 | - // ... champs existants ... | |
| 57 | - | |
| 58 | - /// Durée du refresh token quand "remember me" est activé. | |
| 59 | - /// Variable : REMEMBER_ME_EXPIRATION_DAYS (défaut : 30) | |
| 60 | - pub remember_me_expiration_days: i64, | |
| 61 | -} | |
| 62 | -``` | |
| 63 | - | |
| 64 | -Chargement : | |
| 65 | - | |
| 66 | -```rust | |
| 67 | -let remember_me_expiration_days = env::var("REMEMBER_ME_EXPIRATION_DAYS") | |
| 68 | - .ok() | |
| 69 | - .and_then(|v| v.parse::<i64>().ok()) | |
| 70 | - .unwrap_or(30); | |
| 71 | -``` | |
| 72 | - | |
| 73 | -**Contrainte de validation** : `remember_me_expiration_days` doit être >= `refresh_token_expiration_days` et <= 90 jours. Log un warning si > 60 jours. | |
| 74 | - | |
| 75 | -### 3.3 Handler `login()` — Logique conditionnelle | |
| 76 | - | |
| 77 | -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs`, fonction `login()` | |
| 78 | - | |
| 79 | -Modifier la création du refresh token (actuellement ligne 300-305) : | |
| 80 | - | |
| 81 | -```rust | |
| 82 | -// Avant (actuel) : | |
| 83 | -let created_rt = RefreshTokenService::create_refresh_token( | |
| 84 | - &db, | |
| 85 | - user.id, | |
| 86 | - auth_config.refresh_token_expiration_days, | |
| 87 | -).await?; | |
| 88 | - | |
| 89 | -// Après : | |
| 90 | -let rt_expiration_days = if payload.remember_me { | |
| 91 | - auth_config.remember_me_expiration_days | |
| 92 | -} else { | |
| 93 | - auth_config.refresh_token_expiration_days | |
| 94 | -}; | |
| 95 | - | |
| 96 | -let created_rt = RefreshTokenService::create_refresh_token( | |
| 97 | - &db, | |
| 98 | - user.id, | |
| 99 | - rt_expiration_days, | |
| 100 | -).await?; | |
| 101 | -``` | |
| 102 | - | |
| 103 | -### 3.4 `LoginResponse` — Information de durée | |
| 104 | - | |
| 105 | -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs` | |
| 106 | - | |
| 107 | -Ajouter un champ pour que le client sache quand renouveler : | |
| 108 | - | |
| 109 | -```rust | |
| 110 | -#[derive(Debug, Serialize)] | |
| 111 | -pub struct LoginResponse { | |
| 112 | - pub access_token: String, | |
| 113 | - pub refresh_token: Option<String>, | |
| 114 | - pub token_type: String, | |
| 115 | - pub expires_in: i64, | |
| 116 | - /// Durée de vie du refresh token en secondes. | |
| 117 | - /// Permet au client de planifier le renouvellement ou d'afficher | |
| 118 | - /// la durée restante de la session. | |
| 119 | - pub refresh_expires_in: i64, | |
| 120 | - pub user: UserInfo, | |
| 121 | -} | |
| 122 | -``` | |
| 123 | - | |
| 124 | -Valeur : `rt_expiration_days * 86400`. | |
| 125 | - | |
| 126 | -### 3.5 Endpoint `refresh_token()` — Propagation de la durée | |
| 127 | - | |
| 128 | -**Fichier** : `crates/rustwarden-core/src/auth/handlers.rs`, fonction `refresh_token()` | |
| 129 | - | |
| 130 | -Problème : lors du renouvellement, la durée du nouveau refresh token utilise `auth_config.refresh_token_expiration_days` (7 jours), ce qui **raccourcit** un token "remember me" de 30 jours à chaque rotation. | |
| 131 | - | |
| 132 | -**Solution** : conserver la durée restante originale ou la durée "remember me" lors de la rotation. | |
| 133 | - | |
| 134 | -Option recommandée — **conserver la durée configurée du token d'origine** : | |
| 135 | - | |
| 136 | -```rust | |
| 137 | -// Dans refresh_token(), après validation du old_token : | |
| 138 | -let original_duration_days = (old_token.expires_at - old_token.created_at).num_days(); | |
| 139 | - | |
| 140 | -// Utiliser cette durée pour le nouveau token (plafonné à remember_me_expiration_days) | |
| 141 | -let rt_expiration_days = original_duration_days | |
| 142 | - .min(auth_config.remember_me_expiration_days) | |
| 143 | - .max(auth_config.refresh_token_expiration_days); | |
| 144 | - | |
| 145 | -let new_created_rt = RefreshTokenService::create_refresh_token( | |
| 146 | - &db, | |
| 147 | - user.id, | |
| 148 | - rt_expiration_days, | |
| 149 | -).await?; | |
| 150 | -``` | |
| 151 | - | |
| 152 | -Ainsi, un token "remember me" de 30 jours génère un nouveau token de 30 jours à chaque rotation, sans jamais dépasser la limite configurée. | |
| 153 | - | |
| 154 | -### 3.6 `RefreshResponse` — Même ajout | |
| 155 | - | |
| 156 | -```rust | |
| 157 | -#[derive(Debug, Serialize)] | |
| 158 | -pub struct RefreshResponse { | |
| 159 | - pub access_token: String, | |
| 160 | - pub refresh_token: Option<String>, | |
| 161 | - pub token_type: String, | |
| 162 | - pub expires_in: i64, | |
| 163 | - pub refresh_expires_in: i64, // Nouveau | |
| 164 | -} | |
| 165 | -``` | |
| 166 | - | |
| 167 | -## 4. Variables d'environnement | |
| 168 | - | |
| 169 | -| Variable | Défaut | Description | | |
| 170 | -|----------|--------|-------------| | |
| 171 | -| `REMEMBER_ME_EXPIRATION_DAYS` | 30 | Durée du refresh token avec "remember me" | | |
| 172 | -| `REFRESH_TOKEN_EXPIRATION_DAYS` | 7 | Durée du refresh token sans "remember me" (inchangé) | | |
| 173 | -| `JWT_EXPIRATION_MINUTES` | 15 | Durée du JWT access token (inchangé) | | |
| 174 | - | |
| 175 | -## 5. Matrice de sécurité | |
| 176 | - | |
| 177 | -| Vecteur d'attaque | Mitigation | Statut | | |
| 178 | -|-------------------|------------|--------| | |
| 179 | -| Vol du JWT | Durée courte (15 min), inchangée | Existant | | |
| 180 | -| Vol du refresh token (localStorage) | Rotation + détection de réutilisation + hash SHA-256 en base | Existant | | |
| 181 | -| Brute-force refresh | Rate limiting (5/min/IP) | Existant | | |
| 182 | -| Accumulation de tokens | Limite 10 tokens actifs/utilisateur | Existant | | |
| 183 | -| Session longue après vol de credentials | Le refresh token étendu ne bypass pas la révocation au logout ni au changement de mot de passe | Existant | | |
| 184 | -| Refresh token "remember me" volé | Fenêtre d'exploitation plus longue (30j vs 7j) — **risque accepté** car mitigé par rotation et détection de réutilisation | Nouveau — acceptable | | |
| 185 | -| Appareil partagé / oubli de déconnexion | L'utilisateur choisit explicitement "Se souvenir de moi" — pas activé par défaut | Par design | | |
| 186 | - | |
| 187 | -### Recommandation complémentaire (optionnelle, hors scope v1) | |
| 188 | - | |
| 189 | -Pour les applications sensibles, envisager à terme : | |
| 190 | -- **Révocation par appareil** : endpoint `DELETE /api/v1/auth/sessions/{token_id}` pour révoquer un refresh token spécifique depuis un écran "Appareils connectés" | |
| 191 | -- **Notification de nouvelle connexion** : email à l'utilisateur lors d'un login depuis un nouveau user-agent/IP | |
| 192 | - | |
| 193 | -## 6. Rétrocompatibilité | |
| 194 | - | |
| 195 | -| Composant | Impact | | |
| 196 | -|-----------|--------| | |
| 197 | -| API `POST /api/v1/auth/login` | Aucun — `remember_me` est optionnel (`#[serde(default)]`) | | |
| 198 | -| API `POST /api/v1/auth/refresh` | Aucun — le comportement par défaut reste 7 jours | | |
| 199 | -| `LoginResponse` / `RefreshResponse` | Nouveau champ `refresh_expires_in` — les clients qui ne le lisent pas ne sont pas affectés | | |
| 200 | -| `.env` | Aucune variable existante modifiée. `REMEMBER_ME_EXPIRATION_DAYS` est optionnelle | | |
| 201 | -| Applications consommatrices | Aucune modification requise. Le comportement est identique si `remember_me` n'est pas envoyé | | |
| 202 | - | |
| 203 | -## 7. Côté client (hors scope rustwarden-core, pour l'application consommatrice) | |
| 204 | - | |
| 205 | -Pour que la fonctionnalité soit complète, l'application consommatrice (ex : PasterMan) devra : | |
| 206 | - | |
| 207 | -1. **Checkbox dans le formulaire de login** : envoyer `"remember_me": true` dans le body JSON | |
| 208 | -2. **Stocker le flag** : `localStorage.setItem("remember_me", "true")` si coché | |
| 209 | -3. **Auto-refresh JS** : si `remember_me === "true"`, mettre en place un `setInterval` qui appelle `POST /api/v1/auth/refresh` toutes les `(JWT_EXPIRATION - 2 min)` (soit ~13 min par défaut) avec le refresh token stocké | |
| 210 | -4. **Mise à jour du refresh token** : à chaque réponse de refresh, remplacer l'ancien refresh token en localStorage par le nouveau (rotation) | |
| 211 | -5. **Nettoyage au logout** : supprimer `remember_me` et `refresh_token` du localStorage | |
| 212 | - | |
| 213 | -Exemple de timer auto-refresh : | |
| 214 | - | |
| 215 | -```javascript | |
| 216 | -function setupAutoRefresh(expiresInSeconds) { | |
| 217 | - const refreshInterval = (expiresInSeconds - 120) * 1000; // 2 min avant expiration | |
| 218 | - setInterval(async () => { | |
| 219 | - const refreshToken = localStorage.getItem("refresh_token"); | |
| 220 | - if (!refreshToken || localStorage.getItem("remember_me") !== "true") return; | |
| 221 | - | |
| 222 | - const response = await fetch("/api/v1/auth/refresh", { | |
| 223 | - method: "POST", | |
| 224 | - headers: { "Content-Type": "application/json" }, | |
| 225 | - credentials: "include", | |
| 226 | - body: JSON.stringify({ refresh_token: refreshToken }), | |
| 227 | - }); | |
| 228 | - | |
| 229 | - if (response.ok) { | |
| 230 | - const data = await response.json(); | |
| 231 | - if (data.refresh_token) { | |
| 232 | - localStorage.setItem("refresh_token", data.refresh_token); | |
| 233 | - } | |
| 234 | - // Le nouveau JWT est automatiquement set via le cookie HttpOnly par le serveur | |
| 235 | - } else { | |
| 236 | - // Token expiré ou révoqué — rediriger vers login | |
| 237 | - localStorage.removeItem("refresh_token"); | |
| 238 | - localStorage.removeItem("remember_me"); | |
| 239 | - window.location.href = "/login"; | |
| 240 | - } | |
| 241 | - }, refreshInterval); | |
| 242 | -} | |
| 243 | -``` | |
| 244 | - | |
| 245 | -> **Note** : l'endpoint `POST /api/v1/auth/refresh` ne set pas actuellement de cookie `jwt_token` dans la réponse (contrairement à `login()`). Il faudra soit ajouter ce set-cookie dans le handler `refresh_token()`, soit que le client utilise le `access_token` de la réponse JSON pour un header `Authorization: Bearer`. La première option (set-cookie) est recommandée pour la cohérence avec le flux web. | |
| 246 | - | |
| 247 | -## 8. Tests à ajouter | |
| 248 | - | |
| 249 | -### Tests unitaires (`crates/rustwarden-core`) | |
| 250 | - | |
| 251 | -1. **`test_login_without_remember_me`** — `remember_me` absent ou `false` → refresh token avec durée standard (7j) | |
| 252 | -2. **`test_login_with_remember_me`** — `remember_me: true` → refresh token avec durée étendue (30j) | |
| 253 | -3. **`test_remember_me_default_false`** — Désérialisation d'un body sans champ `remember_me` → `false` | |
| 254 | -4. **`test_refresh_preserves_duration`** — Rotation d'un token "remember me" → le nouveau token conserve la durée étendue | |
| 255 | -5. **`test_refresh_caps_duration`** — La durée du nouveau token ne dépasse jamais `remember_me_expiration_days` | |
| 256 | -6. **`test_remember_me_config_validation`** — `REMEMBER_ME_EXPIRATION_DAYS` < `REFRESH_TOKEN_EXPIRATION_DAYS` → erreur de configuration | |
| 257 | -7. **`test_login_response_includes_refresh_expires_in`** — Le champ `refresh_expires_in` est présent et correct | |
| 258 | - | |
| 259 | -### Tests d'intégration | |
| 260 | - | |
| 261 | -8. **`test_full_remember_me_flow`** — Login avec `remember_me: true` → attendre expiration JWT → refresh → vérifier nouveau JWT valide | |
| 262 | -9. **`test_logout_revokes_remember_me_tokens`** — Login "remember me" → logout → refresh échoue | |
| 263 | - | |
| 264 | -## 9. Fichiers impactés (résumé) | |
| 265 | - | |
| 266 | -| Fichier | Modification | | |
| 267 | -|---------|-------------| | |
| 268 | -| `crates/rustwarden-core/src/config/auth.rs` | Ajouter `remember_me_expiration_days` à `AuthConfig` | | |
| 269 | -| `crates/rustwarden-core/src/auth/handlers.rs` | `LoginRequest` + champ, `LoginResponse` + champ, `RefreshResponse` + champ, logique dans `login()` et `refresh_token()` | | |
| 270 | -| `.env.example` | Documenter `REMEMBER_ME_EXPIRATION_DAYS` | | |
| 271 | - | |
| 272 | -Aucune migration de base de données nécessaire — la table `refresh_token` stocke déjà `expires_at` par token. | |
| 273 | - | |
| 274 | -## 10. Critères d'acceptation | |
| 275 | - | |
| 276 | -- [ ] Un client envoyant `{"username":"x","password":"y"}` (sans `remember_me`) a un comportement identique à l'actuel | |
| 277 | -- [ ] Un client envoyant `{"username":"x","password":"y","remember_me":true}` reçoit un refresh token avec `expires_at` = now + `REMEMBER_ME_EXPIRATION_DAYS` | |
| 278 | -- [ ] La rotation du refresh token conserve la durée d'origine (pas de raccourcissement à 7j) | |
| 279 | -- [ ] `LoginResponse` et `RefreshResponse` incluent `refresh_expires_in` en secondes | |
| 280 | -- [ ] La durée maximale est plafonnée par configuration (défaut 30j, max recommandé 90j) | |
| 281 | -- [ ] Tous les mécanismes de sécurité existants (rotation, détection de réutilisation, révocation au logout, limite par utilisateur) s'appliquent sans modification | |
| 282 | -- [ ] Les tests existants passent sans modification |
GitRust