diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..31f3acc --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git check-ignore *)", + "Bash(ls src-tauri/.gitignore)", + "Bash(cat src-tauri/.gitignore)" + ] + } +} diff --git a/.env b/.env new file mode 100644 index 0000000..c17d87f --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +# Configuration Supabase intégrée à l'appli (compilée au build). +# Tes collègues n'ont RIEN à saisir : ils verront directement l'écran de connexion. +# La clé "anon" est publique par nature, c'est normal qu'elle soit ici. +VITE_SUPABASE_URL=https://cesjrlfqeqftayhnqjkb.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNlc2pybGZxZXFmdGF5aG5xamtiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODI4NDI1NDIsImV4cCI6MjA5ODQxODU0Mn0.jvTz1Lv8fPRNGljtXoCU54CUfofVgyCuVcaPGw81x5M diff --git a/.gitignore b/.gitignore index 8a07d1b..827a3c2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,9 @@ out/ dist/ *.local .DS_Store -.env .vite + +# .env est VOLONTAIREMENT suivi par git : il ne contient que l'URL Supabase +# et la clé "anon" (toutes deux publiques). Ça permet de passer d'une machine +# à l'autre sans le recréer. Ne mets JAMAIS de secret ici : pour un vrai +# secret, utilise .env.local (ignoré via *.local). diff --git a/GUIDE_ADMIN.md b/GUIDE_ADMIN.md new file mode 100644 index 0000000..968dc37 --- /dev/null +++ b/GUIDE_ADMIN.md @@ -0,0 +1,137 @@ +# Guide de mise en place — Comptes & Panel Admin + +Ce guide explique, **étape par étape**, comment activer le panel d'administration +(création / suppression de comptes, gestion des rôles) pour ton équipe. + +Tu ne fais ça **qu'une seule fois**. Ensuite, tout se gère depuis l'appli. + +--- + +## Vue d'ensemble + +| Qui | Voit quoi | +|-----|-----------| +| Toi (admin) | Tout + un menu **« Administration »** pour gérer les comptes | +| Tes collègues | L'appli normale, juste un écran **« Se connecter »** | + +Les clés Supabase sont **déjà intégrées** dans l'appli (fichier `.env`) : tes +collègues n'ont **rien** à configurer. + +--- + +## Étape 1 — Vérifier le fichier `.env` + +Le fichier `.env` (à la racine du projet) contient déjà ton URL et ta clé : + +``` +VITE_SUPABASE_URL=https://cesjrlfqeqftayhnqjkb.supabase.co +VITE_SUPABASE_ANON_KEY=eyJhbGci... +``` + +✅ Rien à faire si c'est correct. (L'URL ne doit PAS contenir `/rest/v1/`.) + +--- + +## Étape 2 — Mettre à jour la base de données + +1. Va sur [supabase.com](https://supabase.com) → ton projet. +2. Menu **SQL Editor** → **New query**. +3. Copie **tout** le contenu du fichier `supabase/schema.sql` et colle-le. +4. Clique **Run**. + +> C'est sans danger même si tu l'as déjà exécuté : tout est « ré-exécutable ». +> Ça ajoute le champ `is_admin` et les sécurités du panel admin. + +--- + +## Étape 3 — Créer TON compte administrateur + +Comme l'inscription publique est désactivée, on crée le **tout premier compte** +(le tien) à la main : + +1. Dans Supabase → menu **Authentication** → **Users** → bouton **Add user**. +2. Mets ton **email** + un **mot de passe**, et coche **Auto Confirm User**. +3. Clique **Create user**. + +Puis on te donne les droits admin : + +4. Retourne dans **SQL Editor** → **New query**, colle ceci en remplaçant l'email : + +```sql +update public.profiles set is_admin = true +where id = (select id from auth.users where email = 'TON_EMAIL@exemple.com'); +``` + +5. Clique **Run**. + +🎉 Tu es maintenant administrateur. + +--- + +## Étape 4 — Déployer l'Edge Function (le « serveur de confiance ») + +C'est elle qui crée/supprime les comptes en sécurité. **Deux méthodes au choix.** + +### Méthode A — En ligne de commande (recommandée) + +Dans un terminal, à la racine du projet : + +```bash +# 1. Se connecter à Supabase (ouvre le navigateur) +npx supabase login + +# 2. Déployer la fonction +npx supabase functions deploy admin-users ^ + --project-ref cesjrlfqeqftayhnqjkb --no-verify-jwt +``` + +> `--no-verify-jwt` est normal ici : la fonction vérifie elle-même que +> l'appelant est bien un admin connecté (voir le code). +> La clé `service_role` est fournie automatiquement par Supabase, tu n'as +> rien à copier. + +### Méthode B — Par le tableau de bord (sans terminal) + +1. Supabase → menu **Edge Functions** → **Create a new function**. +2. Nom : `admin-users`. +3. Colle le contenu de `supabase/functions/admin-users/index.ts`. +4. Déploie. +5. Dans les **réglages** de la fonction, **désactive « Enforce JWT Verification »**. + +--- + +## Étape 5 — Construire et distribuer l'appli + +```bash +npm run tauri:build +``` + +L'installateur se trouve ensuite dans : +`src-tauri/target/release/bundle/` + +Envoie ce fichier à tes collègues. Les clés sont dedans : ils n'ont **rien** à régler. + +--- + +## Au quotidien — Gérer les comptes + +1. Ouvre l'appli, connecte-toi avec ton compte admin. +2. Menu **Administration** (visible uniquement pour les admins). +3. Tu peux : + - **Créer un compte** : nom, email, mot de passe provisoire, rôle, (admin ?) + - **Changer le rôle** d'un membre + - **Promouvoir / rétrograder** un admin + - **Supprimer** un compte + +Communique l'email + le mot de passe provisoire à chaque membre. Il se connecte, +c'est tout. ✅ + +--- + +## Notes de sécurité + +- La clé `anon` dans l'appli est **publique par nature** : aucun risque. +- La clé `service_role` (toute-puissante) **n'est jamais dans l'appli** : elle + reste cachée dans l'Edge Function, côté serveur. +- Un membre normal **ne peut pas** se promouvoir admin lui-même (bloqué côté base). +- Tu ne peux pas supprimer ni rétrograder **ton propre** compte admin (anti-blocage). diff --git a/package-lock.json b/package-lock.json index 67129b7..cfec607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -903,9 +904,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -920,9 +918,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -937,9 +932,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -954,9 +946,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -971,9 +960,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -988,9 +974,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1005,9 +988,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1022,9 +1002,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1039,9 +1016,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1056,9 +1030,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1073,9 +1044,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1090,9 +1058,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1107,9 +1072,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1383,9 +1345,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1403,9 +1362,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1423,9 +1379,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1443,9 +1396,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1463,9 +1413,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "Apache-2.0 OR MIT", "optional": true, "os": [ @@ -1584,6 +1531,7 @@ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1601,6 +1549,7 @@ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1761,6 +1710,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.38", "caniuse-lite": "^1.0.30001799", @@ -2211,6 +2161,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2482,6 +2433,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -2651,6 +2603,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2663,6 +2616,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2983,6 +2937,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3081,6 +3036,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/App.tsx b/src/App.tsx index c3c9ef5..4533c12 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,16 +8,20 @@ import Login from './pages/Login' import Layout, { Page } from './components/Layout' import Dashboard from './pages/Dashboard' import Kanban from './pages/Kanban' +import Planning from './pages/Planning' import Milestones from './pages/Milestones' import Bugs from './pages/Bugs' import Members from './pages/Members' +import Admin from './pages/Admin' const PAGES: Record JSX.Element> = { dashboard: Dashboard, kanban: Kanban, + planning: Planning, milestones: Milestones, bugs: Bugs, - members: Members + members: Members, + admin: Admin } function Shell(): JSX.Element { @@ -26,7 +30,7 @@ function Shell(): JSX.Element { if (stage === 'loading') { return ( -
+
) diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index f3763de..f6c94ee 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -10,21 +10,35 @@ import { ChevronDown, LogOut, Settings, - Gamepad2 + Gamepad2, + ShieldCheck, + CalendarRange, + Sun, + Moon } from 'lucide-react' import { useApp } from '../lib/AppContext' import { supa } from '../lib/supabase' import { ROLE_LABELS } from '../lib/types' +import { Theme, getTheme, setTheme } from '../lib/theme' import { Avatar, Modal } from './ui' -export type Page = 'dashboard' | 'kanban' | 'milestones' | 'bugs' | 'members' +export type Page = + | 'dashboard' + | 'kanban' + | 'planning' + | 'milestones' + | 'bugs' + | 'members' + | 'admin' -const NAV: { id: Page; label: string; icon: typeof LayoutDashboard }[] = [ +const NAV: { id: Page; label: string; icon: typeof LayoutDashboard; adminOnly?: boolean }[] = [ { id: 'dashboard', label: 'Tableau de bord', icon: LayoutDashboard }, { id: 'kanban', label: 'Tâches (Kanban)', icon: KanbanSquare }, - { id: 'milestones', label: 'Jalons & planning', icon: Target }, + { id: 'planning', label: 'Planning', icon: CalendarRange }, + { id: 'milestones', label: 'Jalons', icon: Target }, { id: 'bugs', label: 'Bugs', icon: Bug }, - { id: 'members', label: 'Équipe', icon: Users } + { id: 'members', label: 'Équipe', icon: Users }, + { id: 'admin', label: 'Administration', icon: ShieldCheck, adminOnly: true } ] export default function Layout({ @@ -39,14 +53,21 @@ export default function Layout({ const { profile, projects, currentProject, setCurrentProject, refreshProjects, signOut } = useApp() const [picker, setPicker] = useState(false) const [showNew, setShowNew] = useState(false) + const [theme, setThemeState] = useState(getTheme()) + + const toggleTheme = (): void => { + const next: Theme = theme === 'dark' ? 'light' : 'dark' + setTheme(next) + setThemeState(next) + } return (
{/* Barre latérale */} -
@@ -129,10 +157,10 @@ export default function Layout({ {/* Contenu */}
- {currentProject ? ( + {currentProject || page === 'admin' ? ( children ) : ( -
+

Aucun projet pour le moment.