Compare commits
6 Commits
254497935d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 07feda8d3f | |||
| d8f2235e87 | |||
| a6ab13ee2f | |||
| 1ce33a69d1 | |||
| 4f375e6766 | |||
| 5d79d6c688 |
@@ -15,6 +15,7 @@ Documentation du projet, sur le modèle de [piscine_mobile/docs](../piscine_mobi
|
|||||||
|
|
||||||
| Document | Lien |
|
| Document | Lien |
|
||||||
|----------|------|
|
|----------|------|
|
||||||
|
| **Méthode pour un prochain projet** | [methode-demarrage-projet-ecole.md](methode-demarrage-projet-ecole.md) |
|
||||||
| **Reprise / état du projet** | [pedagogie/00-reprise-et-etat.md](pedagogie/00-reprise-et-etat.md) |
|
| **Reprise / état du projet** | [pedagogie/00-reprise-et-etat.md](pedagogie/00-reprise-et-etat.md) |
|
||||||
| Plan des étapes | [pedagogie/README.md](pedagogie/README.md) |
|
| Plan des étapes | [pedagogie/README.md](pedagogie/README.md) |
|
||||||
|
|
||||||
|
|||||||
193
docs/methode-demarrage-projet-ecole.md
Normal file
193
docs/methode-demarrage-projet-ecole.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Méthode — démarrer un projet école
|
||||||
|
|
||||||
|
Consigne à donner à Cursor au **premier message** d’un nouveau projet 42.
|
||||||
|
Elle reprend la méthode déjà utilisée sur la piscine mobile, Swifty Companion et Tokenizer.
|
||||||
|
|
||||||
|
Copier ce fichier dans `docs/` du nouveau dépôt, ou l’ouvrir dans le chat et demander : « mets en place la structure décrite dans ce document, sans coder la fonctionnalité ».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Deux espaces, un seul dossier de travail
|
||||||
|
|
||||||
|
Tout vit dans **un dépôt perso** (Gitea / Git privé). Le correcteur ne voit **jamais** ce dépôt entier.
|
||||||
|
|
||||||
|
| Espace | Chemin | Qui le lit | Git école |
|
||||||
|
|--------|--------|------------|-----------|
|
||||||
|
| **Travail** | racine du dépôt : `README.md`, `docs/` | Toi + Cursor | Non |
|
||||||
|
| **Rendu** | un sous-dossier dédié, ex. `swiftyCompanion/`, `tokenizerRendu/`, `mobileModule01/` | Le correcteur, une fois copié à la racine Intra | Oui — **ce contenu devient la racine** du dépôt Vogosphere |
|
||||||
|
|
||||||
|
Règle : on **code dans le paquet de rendu**. On **explique dans `docs/`**.
|
||||||
|
|
||||||
|
Le README à la racine du dépôt de travail dit en une page : où est le sujet, où est la pédagogie, où est le paquet école, et comment pousser plus tard. Il ne remplace pas le README du rendu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ordre de démarrage (avant le code métier)
|
||||||
|
|
||||||
|
Cursor crée l’arborescence, extrait le sujet, rédige le cadrage. Il n’implémente pas l’app tant que le cadrage (section 4) n’est pas relu.
|
||||||
|
|
||||||
|
```text
|
||||||
|
<projet>/
|
||||||
|
README.md # dépôt de travail
|
||||||
|
.gitignore # .env, builds, .git imbriqué du rendu
|
||||||
|
docs/
|
||||||
|
README.md # index
|
||||||
|
methode-demarrage-projet-ecole.md # cette consigne, recopiée
|
||||||
|
sujets/
|
||||||
|
README.md
|
||||||
|
<slug>.md # traduction FR fidèle + visuels décrits
|
||||||
|
originaux/
|
||||||
|
<slug>_subject.pdf # PDF Intra, renommé (pas en.subject.pdf)
|
||||||
|
captures/ # images extraites du PDF s’il y en a
|
||||||
|
pedagogie/
|
||||||
|
README.md # feuille de route (table des étapes)
|
||||||
|
00-reprise-et-etat.md # où on en est, prochaine action
|
||||||
|
cahier-des-charges.md # exigences du sujet → critère testable
|
||||||
|
architecture.md # modules, flux, données — calé sur le sujet
|
||||||
|
ui.md # seulement si le sujet a des écrans ou maquettes
|
||||||
|
protocole-test-manuel.md # checklist device / démo, cases vides
|
||||||
|
fiche-depot.md # plus tard : comment pousser le rendu
|
||||||
|
fiche-soutenance-orale.md # plus tard : déroulé oral, reste dans docs/
|
||||||
|
<paquetRendu>/
|
||||||
|
README.md # doc école (voir section 6)
|
||||||
|
.gitignore
|
||||||
|
.env.example # seulement si le sujet a des secrets
|
||||||
|
… code …
|
||||||
|
```
|
||||||
|
|
||||||
|
Nom du paquet de rendu : celui du sujet, ou un nom explicite défendable à l’oral. Pas de deuxième copie du code à la racine du dépôt de travail.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Extraction du sujet (première tâche)
|
||||||
|
|
||||||
|
1. Déposer le PDF Intra dans `docs/sujets/originaux/`, renommé (`swifty_companion_subject.pdf`, pas `en.subject.pdf`).
|
||||||
|
2. Extraire le texte **et** les figures (maquettes, schémas, arborescence imposée) dans `docs/sujets/captures/` quand le PDF en contient.
|
||||||
|
3. Rédiger `docs/sujets/<slug>.md` :
|
||||||
|
- traduction française **fidèle** (le PDF reste la source si un doute apparaît) ;
|
||||||
|
- obligatoire / bonus / interdit, chacun dans sa section ;
|
||||||
|
- arborescence de rendu si le sujet en impose une ;
|
||||||
|
- messages, noms de champs, libellés : **recopiés tels quels** (souvent en anglais).
|
||||||
|
4. `docs/sujets/README.md` pointe le markdown et le PDF.
|
||||||
|
|
||||||
|
Interdit d’inventer une fonctionnalité, un écran ou une techno absents du sujet pour « améliorer » le projet. Un bonus du PDF se fait après la partie obligatoire, et se note comme bonus dans le cahier des charges.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Doc de dev (cadrage)
|
||||||
|
|
||||||
|
Tout ceci reste dans `docs/pedagogie/`. Aucun de ces fichiers ne part sur Vogosphere.
|
||||||
|
|
||||||
|
### `cahier-des-charges.md`
|
||||||
|
|
||||||
|
Grille de conformité. Une ligne par exigence du PDF :
|
||||||
|
|
||||||
|
| # | Exigence (citation courte ou renvoi §) | Critère de validation | Hors scope | Statut |
|
||||||
|
|---|----------------------------------------|------------------------|------------|--------|
|
||||||
|
|
||||||
|
La colonne **Hors scope** sert à figer ce qu’on ne fera pas (ex. « pas d’API météo au module 1 », « pas de mainnet »). En cas de doute, on relit le sujet ; on n’élargit pas le périmètre.
|
||||||
|
|
||||||
|
### `architecture.md`
|
||||||
|
|
||||||
|
Découpage réel du code à venir : entrée, écrans ou commandes, services (API, auth, base), où vivent les secrets, ce qui est partagé entre exercices. Schéma simple (liste ou mermaid) suffit. Chaque bloc renvoie à une exigence du cahier des charges.
|
||||||
|
|
||||||
|
### `ui.md` (si le sujet a une interface)
|
||||||
|
|
||||||
|
Un écran = une section : ce que montre la maquette, les libellés exacts, navigation, états vides / erreur / chargement **déjà décrits par le sujet**. Les captures vivent dans `docs/sujets/captures/`. On ne redessine pas.
|
||||||
|
|
||||||
|
### Feuille de route
|
||||||
|
|
||||||
|
`docs/pedagogie/README.md` est la table des étapes. Une étape = un objectif vérifiable (ex. « OAuth + `/v2/me` », « ex00 géoloc »). Statut : à faire / en cours / fait.
|
||||||
|
|
||||||
|
`00-reprise-et-etat.md` tient en une page : ce qui est fait, la prochaine étape, les commandes pour relancer. C’est le fichier à ouvrir en reprenant le projet.
|
||||||
|
|
||||||
|
Les guides détaillés (`etape-02-….md`, `module-01/ex00-guide.md`) s’écrivent **quand l’étape démarre**, pas tous d’avance.
|
||||||
|
|
||||||
|
### Rituel de lot
|
||||||
|
|
||||||
|
Pour chaque lot (cadrage, README école, fiche orale, commentaires) : Cursor décrit ce qu’il va faire → tu valides → il exécute → la case est cochée dans `00-plan` ou la feuille de route. Pas de gros chantier non relu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Où coder
|
||||||
|
|
||||||
|
- Uniquement dans `<paquetRendu>/`.
|
||||||
|
- Commentaires de code en **français**. Les chaînes imposées par le sujet restent dans la langue du PDF.
|
||||||
|
- Secrets : `.env` local, listé dans `.gitignore`, jamais commité. `.env.example` versionné, sans valeur réelle. Le sujet qui punit un secret public (OAuth, clé API) est rappelé dans le README école.
|
||||||
|
- Pas de lien « pour aller plus vite » vers un autre dépôt école. La piscine peut servir de **référence perso** dans `docs/`, pas dans le paquet rendu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Doc école — indépendante en fin de projet
|
||||||
|
|
||||||
|
Le README de `<paquetRendu>/` est le seul mode d’emploi du correcteur. Il contient : prérequis, installation, variables (via `.env.example`), commandes `analyze` / `test` / `run`, ce qu’il faut montrer (exercices ou écrans), et les messages d’erreur attendus s’ils sont au sujet.
|
||||||
|
|
||||||
|
**Pendant la construction**, un lien vers `docs/pedagogie/` est toléré le temps de rédiger le README (pour ne pas réécrire deux fois). **Dès que le comportement est stable**, ces liens sont retirés.
|
||||||
|
|
||||||
|
Clôture, avant branche ou push école :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# depuis le paquet de rendu — doit rester vide
|
||||||
|
grep -RIn --include='*.md' -E 'docs/pedagogie|/home/|piscine_mobile|tokenizer/docs' .
|
||||||
|
```
|
||||||
|
|
||||||
|
Le correcteur clone le dépôt école, ouvre le README à **sa** racine, et lance les commandes **depuis ce dossier**. On vérifie la même chose en local :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <paquetRendu>
|
||||||
|
# commandes du README, sans sortir vers la racine du dépôt de travail
|
||||||
|
```
|
||||||
|
|
||||||
|
Checklist device : `docs/pedagogie/protocole-test-manuel.md` (cases à cocher). Le README école reprend les critères utiles au correcteur, sans renvoyer vers ce protocole.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Git — travail et rendu
|
||||||
|
|
||||||
|
Développer sur **`main`** du dépôt perso. La doc `docs/` se commit **là**. La perdre sur une branche de rendu, c’est la perdre au checkout.
|
||||||
|
|
||||||
|
Deux formes déjà utilisées ; en choisir **une** au cadrage et l’écrire dans `fiche-depot.md`.
|
||||||
|
|
||||||
|
**A — Paquet imbriqué** (Swifty, Tokenizer). Quand le rendu est prêt : `git init` dans `<paquetRendu>/` (ou copie vers un clone Vogosphere vide). Le `.git` imbriqué est ignoré par le dépôt de travail. On ne pousse pas `docs/`.
|
||||||
|
|
||||||
|
**B — Branche snapshot** (piscine, un module = un dépôt). Quand le module est validé sur `main`, créer `rendu-…` qui ne garde que le `.gitignore` racine et le dossier du module. Si `main` change ensuite : **supprimer et recréer** la branche. Pas de merge de `main` dedans.
|
||||||
|
|
||||||
|
Dans les deux cas, la fiche orale et les guides restent sur `main` / `docs/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Préparation soutenance (après le code)
|
||||||
|
|
||||||
|
Dans l’ordre, chacun validé avant le suivant :
|
||||||
|
|
||||||
|
| Lot | Livrable | Où |
|
||||||
|
|-----|----------|----|
|
||||||
|
| README autonome | plus aucun lien vers `docs/` | `<paquetRendu>/README.md` |
|
||||||
|
| Fiche orale | déroulé, commandes, démo, questions probables, renvois au code | `docs/pedagogie/` seulement |
|
||||||
|
| Commentaires | français, sur le code du rendu | `<paquetRendu>/` |
|
||||||
|
| Dépôt école | `fiche-depot.md` exécuté ; re-test **depuis** le paquet rendu | Vogosphere |
|
||||||
|
|
||||||
|
`analyze` et `test` doivent passer sur cette copie « comme à l’école » avant le push.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Consigne courte à coller dans Cursor
|
||||||
|
|
||||||
|
```text
|
||||||
|
Lis docs/methode-demarrage-projet-ecole.md et applique-la à ce dépôt.
|
||||||
|
|
||||||
|
1. Range le PDF dans docs/sujets/originaux/ et produis la traduction fidèle
|
||||||
|
docs/sujets/<slug>.md (obligatoire, bonus, interdit, libellés exacts,
|
||||||
|
captures si le PDF a des visuels).
|
||||||
|
2. Crée le cadrage dans docs/pedagogie/ : reprise, cahier des charges
|
||||||
|
(une exigence = une ligne testable + hors scope), architecture,
|
||||||
|
feuille de route, ui.md seulement si le sujet a une interface,
|
||||||
|
protocole de test manuel vide.
|
||||||
|
3. Crée le paquet de rendu avec README, .gitignore et .env.example
|
||||||
|
si des secrets existent. Le README école peut linker docs/ le temps
|
||||||
|
du développement ; il sera nettoyé avant le push école.
|
||||||
|
4. Ne code pas la fonctionnalité tant que je n’ai pas validé le cadrage.
|
||||||
|
5. Ensuite, une étape de la feuille de route à la fois, code uniquement
|
||||||
|
dans le paquet de rendu, commentaires en français, aucun ajout hors sujet.
|
||||||
|
6. Réponds en français. Décris le lot, attends ma validation, puis exécute.
|
||||||
|
```
|
||||||
73
docs/pedagogie/00-plan-preparation-soutenance.md
Normal file
73
docs/pedagogie/00-plan-preparation-soutenance.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Plan de préparation — dépôt soutenance, doc et code
|
||||||
|
|
||||||
|
> **Méthode :** pour chaque lot, l’agent décrit ce qu’il compte faire → **tu valides ou ajustes** → on exécute → on coche.
|
||||||
|
|
||||||
|
**Dernière mise à jour :** lot 4 livré (commentaires `lib/` + DEBUG conservé).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Décisions validées (lot 0)
|
||||||
|
|
||||||
|
| Point | Décision |
|
||||||
|
|-------|----------|
|
||||||
|
| Branche `soutenance` | Même contenu que rendu école ; `main` = travail perso ; présentation portable sur `soutenance` |
|
||||||
|
| PC école | Clone perso → copie `swiftyCompanion/` → supprimer repo perso → `git init` rendu → push Vogosphere (détail §6 fiche dépôt) |
|
||||||
|
| `.env` | **Uniquement** `swifty_companion/.env` en local ; **jamais** dans Git ; `.env.example` versionné (sujet : échec direct si secrets publics) |
|
||||||
|
| Fiche orale (lot 3) | **Déroulé de soutenance** détaillé (sujet, points critiques, réalisation, commandes expliquées) — un fichier structuré logiquement |
|
||||||
|
| Commentaires code | **Français** — sujet ne l’interdit pas |
|
||||||
|
| Bouton DEBUG | **Garder** — aide à démontrer le bonus refresh en `flutter run` ; absent en release (`kDebugMode`) — [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Où vit quoi
|
||||||
|
|
||||||
|
| Zone | Chemin | Git |
|
||||||
|
|------|--------|-----|
|
||||||
|
| Travail | racine `swifty_companion/` | Gitea privé (`main`) |
|
||||||
|
| Rendu / soutenance | `swiftyCompanion/` | Vogosphere + branche `soutenance` |
|
||||||
|
| Oral long | `docs/pedagogie/` | perso uniquement |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ordre des lots
|
||||||
|
|
||||||
|
| Phase | Lot | Statut |
|
||||||
|
|-------|-----|--------|
|
||||||
|
| A | 0 Cadrage | ✅ Validé |
|
||||||
|
| B | 1 Dépôt + branche | ✅ [fiche-depot-soutenance.md](fiche-depot-soutenance.md) |
|
||||||
|
| C | 2 README rendu autonome | ✅ fait |
|
||||||
|
| D | 3 Fiche soutenance orale | ✅ [fiche-soutenance-orale.md](fiche-soutenance-orale.md) |
|
||||||
|
| E | 4 Commentaires `lib/` | ✅ fait |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lot 2 — Livré
|
||||||
|
|
||||||
|
- `swiftyCompanion/README.md` réécrit (autonome, sans lien `docs/`).
|
||||||
|
- `.env.example` commenté (racine + `swifty_companion/.env.example`).
|
||||||
|
- `swifty_companion/README.md` : pointeur court vers le README racine.
|
||||||
|
|
||||||
|
## Lot 3 — Livré
|
||||||
|
|
||||||
|
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — déroulé oral, commandes commentées, démo, FAQ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lot 4 — Livré
|
||||||
|
|
||||||
|
- Commentaires / dartdoc en **français** dans `swiftyCompanion/swifty_companion/lib/`.
|
||||||
|
- Bouton **DEBUG** conservé (`kDebugMode`) pour la démo du bonus refresh — pas présent en APK release.
|
||||||
|
|
||||||
|
**Plan préparation :** tous les lots 0–4 sont cochés ; prochaine action opérationnelle = branche `soutenance` + push Vogosphere ([fiche-depot-soutenance.md](fiche-depot-soutenance.md)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suivi
|
||||||
|
|
||||||
|
| Lot | Fait ? |
|
||||||
|
|-----|--------|
|
||||||
|
| 0 | ✅ |
|
||||||
|
| 1 | ✅ |
|
||||||
|
| 2 | ✅ |
|
||||||
|
| 3 | ✅ |
|
||||||
|
| 4 | ✅ |
|
||||||
@@ -2,11 +2,21 @@
|
|||||||
|
|
||||||
Parcours pour construire le projet **étape par étape**.
|
Parcours pour construire le projet **étape par étape**.
|
||||||
|
|
||||||
|
Pour un **nouveau** projet école, la consigne à donner à Cursor est [methode-demarrage-projet-ecole.md](../methode-demarrage-projet-ecole.md).
|
||||||
|
|
||||||
## Reprise rapide
|
## Reprise rapide
|
||||||
|
|
||||||
→ **[00-reprise-et-etat.md](00-reprise-et-etat.md)** — état du projet.
|
→ **[00-reprise-et-etat.md](00-reprise-et-etat.md)** — état du projet.
|
||||||
|
|
||||||
→ **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** — **cartographie sujet ↔ code** pour la correction (ne rien oublier).
|
→ **[00-plan-preparation-soutenance.md](00-plan-preparation-soutenance.md)** — plan (valider lot par lot).
|
||||||
|
|
||||||
|
→ **[fiche-depot-soutenance.md](fiche-depot-soutenance.md)** — branche `soutenance`, `.env`, push PC école, bouton DEBUG.
|
||||||
|
|
||||||
|
→ **[fiche-soutenance-orale.md](fiche-soutenance-orale.md)** — déroulé soutenance, commandes, démo, FAQ.
|
||||||
|
|
||||||
|
→ **[protocole-test-manuel-device.md](protocole-test-manuel-device.md)** — checklist téléphone + FAQ (hidden, cache, logout, skills).
|
||||||
|
|
||||||
|
→ **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** — cartographie sujet ↔ code pour la correction.
|
||||||
|
|
||||||
## Prérequis
|
## Prérequis
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
| Exigence | Cette étape |
|
| Exigence | Cette étape |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet |
|
| ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet — photo : URL API puis GET CDN avec Bearer (`_IntraProfileAvatar`) |
|
||||||
| Skills | Niveau entier + **pourcentage** (partie décimale du `level` API) |
|
| Skills | Niveau entier + **pourcentage** (partie décimale du `level` API) |
|
||||||
| Projets | Statuts **`finished`** et **`failed`** |
|
| Projets | Statuts **`finished`** et **`failed`** |
|
||||||
| Erreurs | Réseau, 401, réponse API inattendue — message + réessai |
|
| Erreurs | Réseau, 401, réponse API inattendue — message + réessai |
|
||||||
@@ -46,6 +46,14 @@ Accept: application/json
|
|||||||
|
|
||||||
**Confidentialité :** certains champs peuvent être masqués par l’API selon le profil consulté — afficher `—` si absent (sujet OAuth).
|
**Confidentialité :** certains champs peuvent être masqués par l’API selon le profil consulté — afficher `—` si absent (sujet OAuth).
|
||||||
|
|
||||||
|
### Photo de profil
|
||||||
|
|
||||||
|
1. **URL** : extraite du JSON user (`_imageUrl` dans `profile_screen.dart`) — `image.link`, puis `image.versions`, puis `image_url`.
|
||||||
|
2. **Fichier** : `GET` sur `cdn.intra.42.fr` avec `Authorization: Bearer` (`IntraAuthService.cdnImageHeaders`) — le CDN refuse souvent les requêtes sans token.
|
||||||
|
3. **UI** : widget `_IntraProfileAvatar` (bytes en mémoire + `CircleAvatar`) ; sinon initiale du login.
|
||||||
|
|
||||||
|
Détail oral : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §5 « Photo de profil ».
|
||||||
|
|
||||||
### Skills — niveau et pourcentage
|
### Skills — niveau et pourcentage
|
||||||
|
|
||||||
L’API renvoie un `level` flottant (ex. `8.42`) :
|
L’API renvoie un `level` flottant (ex. `8.42`) :
|
||||||
|
|||||||
233
docs/pedagogie/fiche-depot-soutenance.md
Normal file
233
docs/pedagogie/fiche-depot-soutenance.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# Fiche — dépôt de soutenance, branche `soutenance`, PC école
|
||||||
|
|
||||||
|
> **Lot 1** du [plan de préparation](00-plan-preparation-soutenance.md).
|
||||||
|
> **Validé :** branche `soutenance` = même contenu que Vogosphere ; travail quotidien sur `main` ; `.env` local uniquement ; `.env.example` dans le rendu Git.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ce que dit le sujet (sécurité — **éliminatoire**)
|
||||||
|
|
||||||
|
Traduction du PDF v4, chapitre IV (encadré page 5) :
|
||||||
|
|
||||||
|
> Les identifiants, clés API, variables d’environnement, etc. doivent être stockés **localement** dans un fichier **`.env`** et **ignorés par git**. Des identifiants exposés **publiquement** entraînent **l’échec direct** du projet.
|
||||||
|
|
||||||
|
**À retenir pour le rendu :**
|
||||||
|
|
||||||
|
| Fichier | Dans le Git école (Vogosphere) ? |
|
||||||
|
|---------|----------------------------------|
|
||||||
|
| `.env` (avec vrais secrets) | **NON — jamais** |
|
||||||
|
| `.env.example` (modèle vide + explications) | **OUI** |
|
||||||
|
| `.gitignore` qui ignore `.env` | **OUI** |
|
||||||
|
|
||||||
|
Le correcteur crée **son** `.env` sur sa machine à partir de `.env.example` + **son** app OAuth Intra (ou la tienne en démo, **sans** committer les valeurs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Où mettre le `.env` pour que l’app tourne
|
||||||
|
|
||||||
|
Flutter charge le fichier déclaré dans `pubspec.yaml` :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
assets:
|
||||||
|
- .env
|
||||||
|
```
|
||||||
|
|
||||||
|
→ chemin **obligatoire pour l’exécution** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
swiftyCompanion/swifty_companion/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---------|------|
|
||||||
|
| `swifty_companion/.env` | **Secrets réels** — machine locale (portable, école en démo) — **gitignore** |
|
||||||
|
| `swiftyCompanion/.env.example` | **Modèle** à la racine du paquet rendu — **versionné** |
|
||||||
|
| `swiftyCompanion/.env` (racine paquet) | **Inutile** pour Flutter — à **ne pas** utiliser pour éviter la confusion (tu peux le supprimer en local) |
|
||||||
|
|
||||||
|
**Procédure correcteur / toi :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd swiftyCompanion/swifty_companion
|
||||||
|
cp ../.env.example .env
|
||||||
|
# Éditer .env : CLIENT_ID, CLIENT_SECRET, REDIRECT_URI
|
||||||
|
flutter pub get
|
||||||
|
flutter run -d <device>
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Le lot 2 enrichira `.env.example` avec commentaires détaillés par variable.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Contenu exact du dépôt école (Vogosphere)
|
||||||
|
|
||||||
|
**Racine du dépôt Intra** = contenu du dossier `swiftyCompanion/` (pas le repo perso entier).
|
||||||
|
|
||||||
|
```text
|
||||||
|
swiftyCompanion/ ← racine Git Vogosphere
|
||||||
|
├── README.md
|
||||||
|
├── .gitignore
|
||||||
|
├── .env.example
|
||||||
|
└── swifty_companion/ ← projet Flutter
|
||||||
|
├── pubspec.yaml
|
||||||
|
├── lib/
|
||||||
|
├── android/
|
||||||
|
├── test/
|
||||||
|
└── …
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ne doit pas apparaître dans Vogosphere :**
|
||||||
|
|
||||||
|
- `docs/` (sujet traduit, pédagogie, fiches orale)
|
||||||
|
- tout `.env` avec secrets
|
||||||
|
- dépôt Gitea perso, liens vers guides internes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Deux Git, deux rôles
|
||||||
|
|
||||||
|
| Dépôt | Remote typique | Contenu |
|
||||||
|
|-------|----------------|---------|
|
||||||
|
| **Perso** | Gitea privé | `docs/` + `swiftyCompanion/` (travail) |
|
||||||
|
| **École** | Vogosphere | **uniquement** `swiftyCompanion/` à la racine |
|
||||||
|
|
||||||
|
Sur le **portable**, tu continues à coder sur **`main`** (perso). Quand le rendu est prêt, tu mets à jour la branche **`soutenance`** (voir §5) — c’est la référence « propre » pour présenter et pour préparer le push école.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Branche `soutenance` (portable)
|
||||||
|
|
||||||
|
### Objectif
|
||||||
|
|
||||||
|
- Même **arbre utile** que Vogosphere : tout sous `swiftyCompanion/` à jour et fonctionnel.
|
||||||
|
- Tu **peux** continuer à développer sur `main` ; `soutenance` ne bouge que quand tu **décides** de figer une version pour correction.
|
||||||
|
- En soutenance sur **ton PC** : tu ouvres le projet dans `swiftyCompanion/swifty_companion/` (pas besoin d’afficher `docs/` à l’écran).
|
||||||
|
|
||||||
|
### Méthode recommandée (monorepo perso)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/projets/42School/swifty_companion
|
||||||
|
|
||||||
|
# 1) Finir les modifs sur main (swiftyCompanion + éventuellement docs)
|
||||||
|
git checkout main
|
||||||
|
git status # rien de secret à committer
|
||||||
|
|
||||||
|
# 2) Mettre à jour soutenance (merge depuis main)
|
||||||
|
git checkout soutenance # créer une fois : git checkout -b soutenance
|
||||||
|
git merge main # ou cherry-pick des commits rendu uniquement
|
||||||
|
|
||||||
|
# 3) Vérifier que seul le paquet rendu est cohérent
|
||||||
|
cd swiftyCompanion/swifty_companion
|
||||||
|
flutter analyze && flutter test
|
||||||
|
# .env présent en local, absent du git :
|
||||||
|
git -C ../.. ls-files | rg '\.env' # doit montrer seulement .env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
**Présentation :** `git checkout soutenance`, lancer `flutter run` depuis `swifty_companion/`.
|
||||||
|
|
||||||
|
**Travail après soutenance :** retour sur `main`, nouvelles features ; avant rendu final, re-merge vers `soutenance`.
|
||||||
|
|
||||||
|
### Alternative (si tu préfères un repo Git **uniquement** dans `swiftyCompanion/`)
|
||||||
|
|
||||||
|
- `git init` dans `swiftyCompanion/`, remote Vogosphere.
|
||||||
|
- Branche `main` = école, branche `dev` optionnelle.
|
||||||
|
- Le monorepo perso ne versionne plus le code Flutter (plus lourd à maintenir). **Tu as choisi le monorepo + branche `soutenance`** — la méthode ci-dessus suffit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Ton scénario **PC école** (push Vogosphere)
|
||||||
|
|
||||||
|
C’est le flux que tu as décrit — formalisé pour ne rien oublier.
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Branche `soutenance` à jour sur Gitea **ou** dossier `swiftyCompanion/` prêt sur clé USB / copie.
|
||||||
|
- `.env` **sur la clé ou la machine école** pour la démo (fichier local, **pas** dans le commit).
|
||||||
|
- URL du dépôt **Vogosphere** (Intra 42).
|
||||||
|
|
||||||
|
### Étapes sur le PC école
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# A — Récupérer le travail (une fois)
|
||||||
|
cd ~
|
||||||
|
git clone https://ton-gitea/.../swifty_companion.git
|
||||||
|
cd swifty_companion
|
||||||
|
git checkout soutenance
|
||||||
|
|
||||||
|
# B — Préparer le dossier de rendu (contenu école pur)
|
||||||
|
# Option simple : le rendu EST le sous-dossier swiftyCompanion
|
||||||
|
cd ~/chemin/attendu/par/l/ecole # répertoire de rendu du sujet
|
||||||
|
rm -rf ./* # seulement si dossier vide / dédié — attention !
|
||||||
|
cp -a ~/swifty_companion/swiftyCompanion/. .
|
||||||
|
|
||||||
|
# C — Retirer le dépôt perso de la machine école (optionnel mais ton choix)
|
||||||
|
rm -rf ~/swifty_companion
|
||||||
|
|
||||||
|
# D — Git ÉCOLE uniquement dans le répertoire de rendu
|
||||||
|
cd ~/chemin/attendu/par/l/ecole
|
||||||
|
git init
|
||||||
|
git remote add origin <url-vogosphere-swifty_companion>
|
||||||
|
git add .
|
||||||
|
git status # VÉRIFIER : pas de .env, pas de docs/
|
||||||
|
|
||||||
|
git commit -m "Rendu Swifty Companion"
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contrôles avant `git push` (checklist)
|
||||||
|
|
||||||
|
- [ ] `git status` ne liste **aucun** `.env`
|
||||||
|
- [ ] `.env.example` est présent et **sans** vraies clés
|
||||||
|
- [ ] Pas de dossier `docs/` du dépôt perso
|
||||||
|
- [ ] `flutter test` OK dans `swifty_companion/`
|
||||||
|
- [ ] README à la **racine** du clone Vogosphere (pas un niveau au-dessus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Portable = démo = même binaire « logique » que l’école
|
||||||
|
|
||||||
|
| Action | Portable (`soutenance`) | Vogosphere |
|
||||||
|
|--------|-------------------------|------------|
|
||||||
|
| Code Flutter | `swiftyCompanion/swifty_companion/` | idem |
|
||||||
|
| Secrets | `.env` local | jamais poussé |
|
||||||
|
| Doc longue oral | `docs/pedagogie/` (perso) | absent |
|
||||||
|
| README correcteur | `swiftyCompanion/README.md` | idem |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Point « DEBUG » dans l’app (éclaircissement)
|
||||||
|
|
||||||
|
Ce n’est **pas** un fichier `.env` ni un secret.
|
||||||
|
|
||||||
|
Dans `search_screen.dart`, un bouton **« DEBUG : simuler token expiré »** n’apparaît que si `kDebugMode` est vrai (build **debug**, ex. `flutter run`). Il **n’existe pas** dans un APK **release** (`flutter build apk --release`).
|
||||||
|
|
||||||
|
| Question | Réponse |
|
||||||
|
|----------|---------|
|
||||||
|
| Le sujet l’interdit ? | **Non** — rien dans le PDF sur les boutons de debug. |
|
||||||
|
| Risque en soutenance ? | L’évaluateur peut le voir en `flutter run` ; tu peux l’expliquer pour le **bonus refresh**, ou le retirer avant rendu final si tu veux une UI plus « propre ». |
|
||||||
|
| Décision (lot 4) | **Garder** — le bonus sujet = refresh token ; le bouton ne fait qu’**invalider l’access** en debug pour tester le refresh sans attendre l’expiration. Absent en `flutter build apk --release`. |
|
||||||
|
|
||||||
|
→ Démo orale : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Commentaires en français dans le code
|
||||||
|
|
||||||
|
Le sujet n’impose pas la langue des commentaires. **Français = OK** (lot 4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Prochaines étapes (autres lots)
|
||||||
|
|
||||||
|
| Lot | Document / action |
|
||||||
|
|-----|-------------------|
|
||||||
|
| 2 | README rendu + `.env.example` commenté (sans ton « IA ») |
|
||||||
|
| 3 | `fiche-soutenance-orale.md` — déroulé minute par minute + commandes |
|
||||||
|
| 4 | Commentaires `lib/` + décision finale bouton DEBUG |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Liens
|
||||||
|
|
||||||
|
- Plan global : [00-plan-preparation-soutenance.md](00-plan-preparation-soutenance.md)
|
||||||
|
- Sujet sécurité : [swifty-companion.md](../sujets/swifty-companion.md) § IV
|
||||||
|
- PDF : [originaux/swifty_companion_subject.pdf](../sujets/originaux/swifty_companion_subject.pdf)
|
||||||
342
docs/pedagogie/fiche-soutenance-orale.md
Normal file
342
docs/pedagogie/fiche-soutenance-orale.md
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
# Fiche soutenance orale — Swifty Companion
|
||||||
|
|
||||||
|
> **Usage :** répétition à voix haute, ordre de démo, **repères code** pour l’IDE.
|
||||||
|
> **Racine code à ouvrir en soutenance :** `swiftyCompanion/swifty_companion/`
|
||||||
|
> **Cartographie sujet ↔ code :** [guide-soutenance-realisation.md](guide-soutenance-realisation.md)
|
||||||
|
> **Dépôt / `.env` :** [fiche-depot-soutenance.md](fiche-depot-soutenance.md)
|
||||||
|
|
||||||
|
**Durée type peer-eval :** 5 à 15 min — viser **8 min** de démo + questions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Index rapide — où ouvrir quoi
|
||||||
|
|
||||||
|
| Sujet oral | Fichier (sous `swifty_companion/lib/` ou `test/`) |
|
||||||
|
|------------|---------------------------------------------------|
|
||||||
|
| Démarrage, `.env`, thème | `main.dart` |
|
||||||
|
| Secrets, URLs API v2 | `config/intra_config.dart` |
|
||||||
|
| Connexion OAuth, tokens, refresh | `services/intra_auth_service.dart` |
|
||||||
|
| GET + 401 → refresh + 429 | `services/intra_http.dart` |
|
||||||
|
| Recherche login | `services/intra_user_service.dart` + `screens/search_screen.dart` |
|
||||||
|
| Profil, skills, projets | `services/intra_profile_service.dart` + `screens/profile_screen.dart` |
|
||||||
|
| **Photo profil (URL + CDN Bearer)** | `profile_screen.dart` → `_imageUrl`, `_IntraProfileAvatar` · `intra_auth_service.dart` → `cdnImageHeaders` |
|
||||||
|
| Messages d’erreur | `services/intra_api_exception.dart` |
|
||||||
|
| Parse JSON token (bonus) | `services/intra_oauth_tokens.dart` + `test/intra_oauth_tokens_test.dart` |
|
||||||
|
| Layout responsive | `theme/swifty_ui.dart` |
|
||||||
|
| Redirect OAuth Android | `android/app/src/main/AndroidManifest.xml` |
|
||||||
|
| Bouton DEBUG (démo bonus) | `screens/search_screen.dart` (bloc `kDebugMode`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Avant d’entrer en soutenance (5 min)
|
||||||
|
|
||||||
|
| Check | Action |
|
||||||
|
|-------|--------|
|
||||||
|
| Device | Téléphone chargé, USB débogage ou Wi‑Fi ADB OK |
|
||||||
|
| `.env` | `swiftyCompanion/swifty_companion/.env`, **pas** affiché à l’écran |
|
||||||
|
| OAuth Intra | Scopes **public** + **projects**, redirect = `.env` |
|
||||||
|
| Terminal | `cd swiftyCompanion/swifty_companion` |
|
||||||
|
| Git école | Pas de `.env` dans `git status` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Introduction (~30 s)
|
||||||
|
|
||||||
|
**À dire :**
|
||||||
|
|
||||||
|
> « Swifty Companion : Flutter, OAuth2 Intra une fois, puis recherche par login et profil (photo, champs, skills avec %, projets validés et failed). Secrets dans `.env` local, ignoré par Git. Deux écrans, un seul access token pour toutes les requêtes API. »
|
||||||
|
|
||||||
|
| Point | Code |
|
||||||
|
|-------|------|
|
||||||
|
| Entrée + chargement `.env` | ```7:31:swiftyCompanion/swifty_companion/lib/main.dart``` |
|
||||||
|
| Vue 1 au lancement | `home: const SearchScreen()` — même fichier |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Sécurité et `.env` (~1 min)
|
||||||
|
|
||||||
|
**Sujet :** secrets en `.env` local, **ignorés par Git** — exposition publique = échec direct.
|
||||||
|
|
||||||
|
**Fichier exécuté :** `swiftyCompanion/swifty_companion/.env` (asset dans `pubspec.yaml`).
|
||||||
|
|
||||||
|
| Variable | Intra 42 |
|
||||||
|
|----------|----------|
|
||||||
|
| `CLIENT_ID` | UID |
|
||||||
|
| `CLIENT_SECRET` | SECRET |
|
||||||
|
| `REDIRECT_URI` | URI de redirection |
|
||||||
|
|
||||||
|
**Scopes** (Intra, pas dans `.env`) : `public` + `projects`.
|
||||||
|
|
||||||
|
| Rôle | Code |
|
||||||
|
|------|------|
|
||||||
|
| Lecture `.env` + `isConfigured` | ```9:14:swiftyCompanion/swifty_companion/lib/config/intra_config.dart``` |
|
||||||
|
| `dotenv.load` au boot | ```7:9:swiftyCompanion/swifty_companion/lib/main.dart``` |
|
||||||
|
| Message si `.env` incomplet | ```109:118:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
|
||||||
|
| Modèle versionné (rendu) | `swiftyCompanion/.env.example` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Connexion 42 — OAuth2 pas à pas (~1–2 min)
|
||||||
|
|
||||||
|
### Ce qui se passe quand tu appuies sur « Se connecter avec 42 »
|
||||||
|
|
||||||
|
1. **`SearchScreen._connect()`** appelle **`IntraAuthService.signIn()`**
|
||||||
|
→ ```37:45:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart```
|
||||||
|
|
||||||
|
2. **`authorizeAndExchangeCode`** (`flutter_appauth`) : navigateur Intra → tu te connectes → redirect `com.example.swiftycompanion://callback` → l’app reçoit les tokens.
|
||||||
|
→ ```40:50:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
|
||||||
|
Scopes : `public`, `projects` — ```46:46:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
|
||||||
|
|
||||||
|
3. **Stockage en mémoire** : `_accessToken` + `_refreshToken`
|
||||||
|
→ ```57:61:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
|
||||||
|
|
||||||
|
4. **Vérification session** : `GET /v2/me` avec le même service
|
||||||
|
→ ```128:133:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
|
||||||
|
|
||||||
|
5. **Redirect Android** (schéma + host)
|
||||||
|
→ ```26:38:swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml```
|
||||||
|
|
||||||
|
**Critère sujet n°9 :** après cette étape, **plus de OAuth** pour chaque recherche — seulement `Authorization: Bearer …`
|
||||||
|
→ ```117:125:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart``` (`authHeaders`)
|
||||||
|
|
||||||
|
**Erreurs OAuth** (annulation, plateforme) : ```50:68:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Tokens : access, refresh, et bouton DEBUG (à maîtriser pour le bonus)
|
||||||
|
|
||||||
|
> Section la plus importante si l’évaluateur demande le **bonus** ou le bouton **DEBUG**.
|
||||||
|
|
||||||
|
### Vocabulaire simple (à réciter)
|
||||||
|
|
||||||
|
| Jeton | Rôle | Analogie |
|
||||||
|
|-------|------|----------|
|
||||||
|
| **Access token** | Prouve à l’API que tu es autorisé ; envoyé sur chaque `GET` / `POST` API | **Badge** d’entrée — durée courte |
|
||||||
|
| **Refresh token** | Permet d’obtenir un **nouveau** access token **sans** rouvrir le navigateur OAuth | **Clé de renouvellement** du badge |
|
||||||
|
|
||||||
|
Les deux sont obtenus **une fois** au login OAuth, puis gardés dans **`IntraAuthService.instance`** (singleton).
|
||||||
|
|
||||||
|
### Flux normal (sans bouton DEBUG)
|
||||||
|
|
||||||
|
```text
|
||||||
|
[Connexion 42] → access + refresh en RAM
|
||||||
|
[Rechercher] → GET /users?filter[login]= avec Bearer access
|
||||||
|
[Profil] → GET /users/{login}, projects_users, etc. même Bearer
|
||||||
|
```
|
||||||
|
|
||||||
|
Recherche : ```82:95:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` → ```19:33:swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart```
|
||||||
|
|
||||||
|
### Quand l’access token expire (bonus sujet)
|
||||||
|
|
||||||
|
1. Un `GET` API renvoie **401 Unauthorized**.
|
||||||
|
2. **`intraApiGet`** appelle **`refreshAccessToken()`** une fois, puis **rejoue** le même `GET`.
|
||||||
|
→ ```19:24:swiftyCompanion/swifty_companion/lib/services/intra_http.dart```
|
||||||
|
3. Le refresh fait **`POST /oauth/token`** avec `grant_type=refresh_token`.
|
||||||
|
→ ```88:114:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
|
||||||
|
4. Si le refresh échoue → **`IntraUnauthorizedException`** (« Session expirée… »)
|
||||||
|
→ ```25:27:swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart```
|
||||||
|
|
||||||
|
**À dire :** « Le bonus n’est pas un second OAuth à chaque fois : c’est le refresh automatique sur 401, puis la même requête est relancée. »
|
||||||
|
|
||||||
|
### Le bouton « DEBUG : simuler token expiré » — qu’est-ce que ça fait **vraiment** ?
|
||||||
|
|
||||||
|
| Question | Réponse courte |
|
||||||
|
|----------|----------------|
|
||||||
|
| C’est demandé par le sujet ? | **Non.** C’est un **raccourci de démo** en développement. |
|
||||||
|
| C’est de la triche ? | **Non.** Il ne contourne pas l’API : il force la même situation qu’un access token périmé, pour tester le **vrai** flux refresh. |
|
||||||
|
| Visible en soutenance release ? | **Non.** Compilé seulement si `kDebugMode` (ex. `flutter run`). Absent en `flutter build apk --release`. |
|
||||||
|
|
||||||
|
**Mécanisme en 3 lignes de code :**
|
||||||
|
|
||||||
|
```143:146:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart
|
||||||
|
void debugInvalidateAccessToken() {
|
||||||
|
if (!canRefresh) return;
|
||||||
|
_accessToken = 'invalid_access_token_for_refresh_test';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Le **refresh token n’est pas touché**.
|
||||||
|
- L’**access token** devient une valeur volontairement fausse → la prochaine API répond **401** → le chemin **bonus** (§ ci-dessus) s’exécute.
|
||||||
|
|
||||||
|
**UI du bouton :**
|
||||||
|
|
||||||
|
```189:205:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart
|
||||||
|
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
...
|
||||||
|
_auth.debugInvalidateAccessToken();
|
||||||
|
...
|
||||||
|
child: const Text('DEBUG : simuler token expiré'),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script oral démo DEBUG (~45 s)
|
||||||
|
|
||||||
|
1. « Je me connecte une fois à l’API 42. »
|
||||||
|
2. « Une recherche fonctionne : un seul access token, pas de nouvelle fenêtre OAuth. »
|
||||||
|
3. « En build debug j’ai un bouton qui **simule** un access token expiré — il ne supprime pas le refresh token. » *(montrer le bouton)*
|
||||||
|
4. « Je recherche à nouveau : l’API renvoie 401, mon `intra_http` déclenche le refresh, puis relance le GET — le profil s’ouvre sans repasser par le navigateur. »
|
||||||
|
5. « En production le bouton n’existe pas ; le mécanisme reste le même quand le token expire vraiment. »
|
||||||
|
|
||||||
|
**Si pas de téléphone / pas de debug :** `flutter test` sur le parse token
|
||||||
|
→ `swiftyCompanion/swifty_companion/test/intra_oauth_tokens_test.dart`
|
||||||
|
→ ```12:20:swiftyCompanion/swifty_companion/lib/services/intra_oauth_tokens.dart```
|
||||||
|
|
||||||
|
**Limite à assumer :** tokens en **mémoire** — fermer l’app = nouvelle connexion OAuth (`signOut` / kill)
|
||||||
|
→ ```64:67:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Architecture des écrans et services
|
||||||
|
|
||||||
|
### Deux vues
|
||||||
|
|
||||||
|
| Vue | Fichier | Code clé |
|
||||||
|
|-----|---------|----------|
|
||||||
|
| Recherche + OAuth | `screens/search_screen.dart` | Classe ```14:19:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
|
||||||
|
| Profil | `screens/profile_screen.dart` | `fetchProfile` ```37:38:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
|
||||||
|
|
||||||
|
### Chaîne d’appels
|
||||||
|
|
||||||
|
```text
|
||||||
|
SearchScreen / ProfileScreen
|
||||||
|
→ IntraUserService / IntraProfileService
|
||||||
|
→ intraApiGet (401, 429)
|
||||||
|
→ IntraAuthService.instance
|
||||||
|
```
|
||||||
|
|
||||||
|
| Service | Fichier | Lignes utiles |
|
||||||
|
|---------|---------|---------------|
|
||||||
|
| Profil agrégé | `intra_profile_service.dart` | `fetchProfile` ~71 ; skills ~173 ; projets ~222 ; cache TTL ~65-66 |
|
||||||
|
| Erreurs typées | `intra_api_exception.dart` | fichier entier |
|
||||||
|
| Layout | `swifty_ui.dart` | `wideBreakpoint` ```13:13:swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart``` ; profil 2 col. ```103:103:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
|
||||||
|
|
||||||
|
### Photo de profil (critère sujet n°4) — à savoir expliquer
|
||||||
|
|
||||||
|
Le PDF demande **quatre détails + profile picture**. L’encadré confidentialité permet de ne pas afficher une donnée **si l’API ne la fournit pas** ; ton code **tente** d’afficher la photo dès qu’une URL existe.
|
||||||
|
|
||||||
|
#### Deux étapes (API JSON ≠ fichier image)
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /v2/users/{login} → JSON avec "image": { "link", "versions": { large, medium, … } }
|
||||||
|
↓
|
||||||
|
GET https://cdn.intra.42.fr/users/…/….jpg → bytes JPEG (hébergement CDN)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Étape | Qui | Code |
|
||||||
|
|-------|-----|------|
|
||||||
|
| 1. Lire l’URL dans le JSON | `ProfileScreen._imageUrl` | ```65:83:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
|
||||||
|
| 2. Télécharger l’image | `_IntraProfileAvatar._load` | ```222:236:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
|
||||||
|
| 3. Header Bearer (même access token) | `IntraAuthService.cdnImageHeaders` | ```128:136:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart``` |
|
||||||
|
| 4. Affichage | `CircleAvatar` + `MemoryImage` | ```244:251:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
|
||||||
|
| En-tête profil | `_ProfileHeader` passe `imageUrl` | cherche `_ProfileHeader` dans `profile_screen.dart` |
|
||||||
|
|
||||||
|
**Pourquoi pas `NetworkImage(url)` seul ?** Le CDN 42 renvoie souvent **403** sans `Authorization: Bearer`. Ce n’est **pas** un nouveau OAuth : c’est le **même** access token que pour `intraApiGet`.
|
||||||
|
|
||||||
|
**Pourquoi pas `authHeaders()` sur l’image ?** Ce helper envoie `Accept: application/json`, inadapté à un JPEG — d’où `cdnImageHeaders()` (Bearer seul).
|
||||||
|
|
||||||
|
#### Script oral photo (~30 s)
|
||||||
|
|
||||||
|
> « Le sujet demande la photo de profil. L’API me donne une URL sur `cdn.intra.42.fr` dans le champ `image`. Je parse d’abord `link` puis les `versions`. Ensuite je télécharge le fichier avec le même access token que pour l’API, via `cdnImageHeaders`. Si ça échoue ou s’il n’y a pas d’URL, j’affiche l’initiale du login — c’est prévu par l’encadré confidentialité du sujet. »
|
||||||
|
|
||||||
|
#### FAQ correcteur
|
||||||
|
|
||||||
|
| Question | Réponse |
|
||||||
|
|----------|---------|
|
||||||
|
| Un token par image ? | **Non** — réutilisation du access token déjà obtenu au login. |
|
||||||
|
| Régénérer l’app OAuth pour la photo ? | **Non** — la photo dépend du JSON + CDN, pas du SECRET (sauf si OAuth cassé). |
|
||||||
|
| `public` suffit pour la photo ? | **Oui** — la photo est dans les données user publiques. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Démo guidée — terminal et parcours
|
||||||
|
|
||||||
|
### Commandes (depuis `swiftyCompanion/swifty_companion/`)
|
||||||
|
|
||||||
|
| Commande | Oral |
|
||||||
|
|----------|------|
|
||||||
|
| `flutter pub get` | Dépendances |
|
||||||
|
| `flutter analyze` | *(orthographe anglaise)* analyse statique |
|
||||||
|
| `flutter test` | `test/widget_test.dart` + `intra_oauth_tokens_test.dart` |
|
||||||
|
| `flutter devices` / `flutter run -d …` | Device + lancement |
|
||||||
|
|
||||||
|
### Parcours écran
|
||||||
|
|
||||||
|
| Étape | Code à citer si demandé |
|
||||||
|
|-------|-------------------------|
|
||||||
|
| Connexion | `_connect` → `signIn` (§3) |
|
||||||
|
| Navigation profil | ```91:94:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
|
||||||
|
| Erreur profil + Réessayer | `_ErrorBody` dans `profile_screen.dart` |
|
||||||
|
| Login inconnu | `IntraUserNotFoundException` dans `intra_user_service.dart` |
|
||||||
|
| Déconnexion | `_disconnect` → `signOut` ```74:79:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Critères obligatoires — repères code
|
||||||
|
|
||||||
|
| # | Critère | Où montrer |
|
||||||
|
|---|---------|------------|
|
||||||
|
| 1 | 2 vues | `search_screen.dart` + `profile_screen.dart` |
|
||||||
|
| 2 | Erreurs | `intra_api_exception.dart` ; catches `search_screen` / `profile_screen` |
|
||||||
|
| 3 | Profil si login existe | Liste vide → exception, pas de `Navigator.push` — `intra_user_service.dart` ~53-55 |
|
||||||
|
| 4 | ≥4 champs + photo | `_DetailsCard` + `_imageUrl` / `_IntraProfileAvatar` — `profile_screen.dart` (§5 photo) |
|
||||||
|
| 5 | Skills niveau + % | `_parseSkillsFromCursus` ```173:207:swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart``` |
|
||||||
|
| 6 | Projets + failed | `_parseProjects` ```222:246:swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart``` |
|
||||||
|
| 7 | Retour vue 1 | `showBack: true` sur AppBar profil ; `Navigator.pop` implicite |
|
||||||
|
| 8 | Layout flexible | `ResponsiveScrollBody`, `LayoutBuilder` wide 720px |
|
||||||
|
| 9 | Un token | `IntraAuthService.instance` ; pas de `signIn` dans `_search` |
|
||||||
|
|
||||||
|
**Champs `—` :** confidentialité OAuth — conforme sujet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Erreurs — quoi montrer
|
||||||
|
|
||||||
|
| Cas | Code / message |
|
||||||
|
|-----|----------------|
|
||||||
|
| Login vide | `intra_user_service.dart` ~20-22 |
|
||||||
|
| Non connecté | `IntraNotAuthenticatedException` |
|
||||||
|
| Réseau | `IntraNetworkException` dans user/profile services |
|
||||||
|
| 403 scopes | `IntraForbiddenException` |
|
||||||
|
| 429 | Retry ```27:32:swiftyCompanion/swifty_companion/lib/services/intra_http.dart``` |
|
||||||
|
| Session morte | `IntraUnauthorizedException` après échec refresh |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Questions fréquentes
|
||||||
|
|
||||||
|
| Question | Réponse + code |
|
||||||
|
|----------|----------------|
|
||||||
|
| Pourquoi pas `/experiences` ? | Skills depuis `cursus_users` — `_parseSkillsFromCursus` |
|
||||||
|
| Pourquoi scope `projects` ? | `projects_users` ; sinon 403 |
|
||||||
|
| Pourquoi le bouton DEBUG ? | Démo du bonus refresh en debug — §4 |
|
||||||
|
| Régénérer SECRET pour la photo ? | **Non** — voir §5 photo (CDN + Bearer) |
|
||||||
|
| Comment charge-t-on la photo ? | `_imageUrl` puis `_IntraProfileAvatar` + `cdnImageHeaders` |
|
||||||
|
| Tests bonus ? | `test/intra_oauth_tokens_test.dart` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Script condensé (1 page)
|
||||||
|
|
||||||
|
1. Intro — Flutter, OAuth une fois, 2 vues, `.env` hors Git (`main.dart`, `intra_config.dart`).
|
||||||
|
2. `flutter test`.
|
||||||
|
3. Connexion — `signIn` + `AndroidManifest` redirect.
|
||||||
|
4. Recherche → profil — `intra_user_service`, `intra_profile_service`.
|
||||||
|
5. Retour → 2ᵉ recherche (même token).
|
||||||
|
6. Erreurs — login faux, réseau.
|
||||||
|
7. Bonus — expliquer access/refresh ; option DEBUG → `debugInvalidateAccessToken` + `intra_http` 401.
|
||||||
|
8. Rappel — 9 critères, scopes, `.env.example` au rendu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Liens doc perso
|
||||||
|
|
||||||
|
| Document | Usage |
|
||||||
|
|----------|--------|
|
||||||
|
| [guide-soutenance-realisation.md](guide-soutenance-realisation.md) | Tableau sujet ↔ fichiers détaillé |
|
||||||
|
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | Refresh approfondi |
|
||||||
|
| [fiche-depot-soutenance.md](fiche-depot-soutenance.md) | DEBUG §8, Vogosphere |
|
||||||
|
| [swiftyCompanion/README.md](../../swiftyCompanion/README.md) | README correcteurs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dernière mise à jour : photo CDN (Bearer), commentaires code, guide + README rendu.*
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
| Donnée affichée | Source API | Fichier UI |
|
| Donnée affichée | Source API | Fichier UI |
|
||||||
|-----------------|------------|------------|
|
|-----------------|------------|------------|
|
||||||
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
|
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
|
||||||
| **Photo** | `image.link` ou `image_url` | `CircleAvatar` + `NetworkImage` — `_ProfileHeader` |
|
| **Photo** | `image.link`, `image.versions`, `image_url` | `_imageUrl` + `_IntraProfileAvatar` (`http.get` + Bearer via `cdnImageHeaders`) — `profile_screen.dart` |
|
||||||
| **Email** | `user['email']` | `_DetailsCard` |
|
| **Email** | `user['email']` | `_DetailsCard` |
|
||||||
| **Téléphone** | `user['phone']` | `_DetailsCard` |
|
| **Téléphone** | `user['phone']` | `_DetailsCard` |
|
||||||
| **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` |
|
| **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` |
|
||||||
|
|||||||
200
docs/pedagogie/protocole-test-manuel-device.md
Normal file
200
docs/pedagogie/protocole-test-manuel-device.md
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
# Protocole de test manuel sur téléphone — Swifty Companion
|
||||||
|
|
||||||
|
> **Objectif :** valider la partie obligatoire (et le bonus) **sur device** avant de figer la branche `soutenance` et le rendu Vogosphere.
|
||||||
|
> **Code :** `swiftyCompanion/swifty_companion/` · **Branche travail :** `main` · **Branche portable / rendu :** `soutenance`.
|
||||||
|
|
||||||
|
**Prérequis PC :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd swiftyCompanion/swifty_companion
|
||||||
|
flutter pub get && flutter analyze && flutter test
|
||||||
|
flutter devices
|
||||||
|
flutter run -d <id_téléphone>
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env` dans `swifty_companion/.env` · OAuth Intra : scopes **public** + **projects** · redirect = `com.example.swiftycompanion://callback`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Réponses aux points soulevés (à lire avant de cocher)
|
||||||
|
|
||||||
|
### A — Déconnexion (icône logout) vs annulation OAuth
|
||||||
|
|
||||||
|
| Action | Comportement actuel | Faut-il un message « connexion annulée » ? |
|
||||||
|
|--------|---------------------|---------------------------------------------|
|
||||||
|
| **Annuler** dans le navigateur OAuth (pendant « Se connecter avec 42 ») | Message **« Connexion annulée »** | Déjà géré — `search_screen.dart` (`FlutterAppAuthUserCancelledException`). |
|
||||||
|
| **Déconnexion** (icône logout en haut à droite quand connecté) | Retour à l’écran **« Connecte-toi »** sans message | **Non obligatoire** au sujet. C’est une **déconnexion volontaire**, pas une annulation OAuth. Optionnel : SnackBar « Déconnecté » — pas requis pour valider. |
|
||||||
|
|
||||||
|
**Test A3** du protocole = annuler **pendant** le flux navigateur, pas appuyer sur logout.
|
||||||
|
|
||||||
|
### B3 — Champs profil : `hidden`, vide, ≥ 4 détails
|
||||||
|
|
||||||
|
Le sujet demande **au moins quatre détails** parmi login, email, mobile, level, location, wallet, evaluations, etc. **+ photo**.
|
||||||
|
|
||||||
|
| Ce que tu vois | Conforme ? |
|
||||||
|
|----------------|------------|
|
||||||
|
| Email affiché | Oui — compte comme 1 détail |
|
||||||
|
| Téléphone = **`hidden`** | Oui — c’est la **valeur renvoyée par l’API** (confidentialité), pas un bug |
|
||||||
|
| Niveau cursus affiché | Oui |
|
||||||
|
| Localisation vide → **`—`** | Oui — souvent pas de poste connecté / pas sur PC école ; encadré sujet confidentialité |
|
||||||
|
| Wallet (points) | Oui |
|
||||||
|
| **Login** + **photo** (en-tête) | Comptent aussi pour le critère |
|
||||||
|
|
||||||
|
Tu as **plus de 4 lignes utiles** même si certaines sont `hidden` ou `—`. **Pas besoin d’ajouter** d’autres champs (evaluation_points, etc.) sauf choix perso.
|
||||||
|
|
||||||
|
Autres champs possibles dans l’API (non requis) : `displayname`, `correction_point`, `pool_month`, … — le sujet ne impose pas de tout afficher.
|
||||||
|
|
||||||
|
### B4 — « Niveau entier » pour les skills
|
||||||
|
|
||||||
|
L’API envoie un niveau **décimal** (ex. `8.42`).
|
||||||
|
|
||||||
|
| Affichage app | Signification |
|
||||||
|
|---------------|----------------|
|
||||||
|
| **Niveau 8** | Partie **entière** (`floor`) — c’est le « niveau entier » du sujet |
|
||||||
|
| **(42 %)** | Partie décimale × 100 — le **pourcentage** du sujet |
|
||||||
|
|
||||||
|
Exemple ligne UI : `Algorithms — niveau 8 (42 %)`.
|
||||||
|
|
||||||
|
Code : `IntraProfileService._skillFromLevel` · affichage `_SkillsCard` dans `profile_screen.dart`.
|
||||||
|
|
||||||
|
Plusieurs skills affichés = **mieux** ; le sujet exige le **format** niveau + % sur les skills montrés.
|
||||||
|
|
||||||
|
### C5 — Profil ouvert + réseau coupé (cache)
|
||||||
|
|
||||||
|
| Comportement | Explication |
|
||||||
|
|--------------|-------------|
|
||||||
|
| Profil **déjà chargé** reste visible sans réseau | Cache mémoire ~2 min (`IntraProfileService`) + données déjà en RAM — **normal** |
|
||||||
|
| Erreur réseau au **retour** puis nouvelle recherche | Couvre le critère « erreur réseau » |
|
||||||
|
| Erreur + **Réessayer** si chargement profil échoue | Déjà implémenté (`_ErrorBody`) |
|
||||||
|
|
||||||
|
Le sujet **n’exige pas** un rafraîchissement automatique du profil quand le réseau tombe. Test **C5 = optionnel** (amélioration possible plus tard, pas bloquant soutenance).
|
||||||
|
|
||||||
|
### F — Après déconnexion
|
||||||
|
|
||||||
|
Comportement attendu et **correct** :
|
||||||
|
|
||||||
|
- Plus de session → écran **connexion OAuth** uniquement ;
|
||||||
|
- **Pas** de recherche tant que tu ne t’es pas reconnecté (`IntraNotAuthenticatedException` si tu forçais une recherche — l’UI masque le formulaire tant que déconnecté).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grille de test — à cocher sur le téléphone
|
||||||
|
|
||||||
|
Légende : **O** = OK · **N** = échec · **—** = non testé / optionnel
|
||||||
|
|
||||||
|
### Préparation
|
||||||
|
|
||||||
|
| ID | Vérification | O / N / — |
|
||||||
|
|----|--------------|-----------|
|
||||||
|
| P1 | `flutter test` vert sur la branche testée | |
|
||||||
|
| P2 | `.env` présent, app démarre sans erreur config | |
|
||||||
|
| P3 | App installée via `flutter run` (debug) | |
|
||||||
|
|
||||||
|
### A — OAuth
|
||||||
|
|
||||||
|
| ID | Action | Résultat attendu | O / N / — |
|
||||||
|
|----|--------|------------------|-----------|
|
||||||
|
| A1 | **Se connecter avec 42** (succès) | Session affichée, formulaire recherche visible | |
|
||||||
|
| A2 | **Déconnexion** (icône logout) | Écran connexion, **sans** message obligatoire | |
|
||||||
|
| A3 | Reconnecter, lancer OAuth puis **annuler** dans le navigateur | Message **« Connexion annulée »** (pas confondre avec A2) | |
|
||||||
|
| A4 | Reconnexion réussie | Session OK | |
|
||||||
|
|
||||||
|
### B — Parcours nominal
|
||||||
|
|
||||||
|
| ID | Action | Résultat attendu | O / N / — |
|
||||||
|
|----|--------|------------------|-----------|
|
||||||
|
| B1 | Recherche login valide | Écran profil | |
|
||||||
|
| B2 | Photo | Image visible (ou initiale si API sans URL — expliquer à l’oral) | |
|
||||||
|
| B3 | Carte informations | ≥ 4 lignes (email, tel même `hidden`, niveau, wallet, etc.) | |
|
||||||
|
| B4 | Skills | Au moins une ligne `niveau X (Y %)` | |
|
||||||
|
| B5 | Projets | Au moins un validé et idéalement un **failed** | |
|
||||||
|
| B6 | Retour arrière | Recherche, **toujours connecté** | |
|
||||||
|
| B7 | 2ᵉ recherche (autre login) | Profil sans nouveau OAuth | |
|
||||||
|
|
||||||
|
### C — Erreurs
|
||||||
|
|
||||||
|
| ID | Action | Résultat attendu | O / N / — |
|
||||||
|
|----|--------|------------------|-----------|
|
||||||
|
| C1 | Recherche login vide | Message « Saisis un login » | |
|
||||||
|
| C2 | `zzzzinvalid` | Message, pas de profil vide | |
|
||||||
|
| C3 | Mode avion → **rechercher** | Message réseau | |
|
||||||
|
| C4 | Réseau OK → recherche à nouveau | Profil OK | |
|
||||||
|
| C5 | *(Optionnel)* Profil ouvert → couper réseau | Données en cache OK ; pas exigé par le sujet | — |
|
||||||
|
|
||||||
|
### D — Layout
|
||||||
|
|
||||||
|
| ID | Action | Résultat attendu | O / N / — |
|
||||||
|
|----|--------|------------------|-----------|
|
||||||
|
| D1 | Rotation paysage (profil) | Lisible / 2 colonnes si large | |
|
||||||
|
| D2 | Clavier sur écran recherche | Scroll OK | |
|
||||||
|
|
||||||
|
### E — Bonus (optionnel)
|
||||||
|
|
||||||
|
| ID | Action | Résultat attendu | O / N / — |
|
||||||
|
|----|--------|------------------|-----------|
|
||||||
|
| E1 | DEBUG simuler token expiré | Message debug | |
|
||||||
|
| E2 | Rechercher ensuite | Profil sans navigateur OAuth | |
|
||||||
|
|
||||||
|
### F — Session (complément)
|
||||||
|
|
||||||
|
| ID | Action | Résultat attendu | O / N / — |
|
||||||
|
|----|--------|------------------|-----------|
|
||||||
|
| F1 | Déconnecté : pas de champ recherche utilisable | Uniquement bouton connexion | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation sujet (après grille)
|
||||||
|
|
||||||
|
| # | Critère | Validé si… | O / N |
|
||||||
|
|---|---------|------------|-------|
|
||||||
|
| 1 | 2 vues | B1, B6 | |
|
||||||
|
| 2 | Erreurs | C1–C4 (+ A3) | |
|
||||||
|
| 3 | Profil si login existe | C2 vs B1 | |
|
||||||
|
| 4 | 4 détails + photo | B2, B3 | |
|
||||||
|
| 5 | Skills niveau + % | B4 | |
|
||||||
|
| 6 | Projets + failed | B5 | |
|
||||||
|
| 7 | Retour vue 1 | B6 | |
|
||||||
|
| 8 | Layout | D1 ou D2 | |
|
||||||
|
| 9 | Un token | B7 | |
|
||||||
|
| Bonus | Refresh | E1–E2 | |
|
||||||
|
|
||||||
|
**Décision :** si tous les **O / N** obligatoires sont **O** → passer à la **mise à jour branche `soutenance`** (voir ci-dessous).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Après validation manuelle — branche `soutenance`
|
||||||
|
|
||||||
|
1. `git checkout main` → `git push origin main` (si pas déjà fait).
|
||||||
|
2. Aligner `main` / `soutenance` si besoin (ex. un seul `.env.example` sur `main`).
|
||||||
|
3. `git checkout soutenance` → vérifier commit photo (`eaacef0` ou plus récent).
|
||||||
|
4. **Re-test sur `soutenance` :**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd swiftyCompanion/swifty_companion
|
||||||
|
flutter pub get && flutter analyze && flutter test
|
||||||
|
flutter run -d <device>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Refaire **minimum** : A1, B1–B7, C2, B2 (photo).
|
||||||
|
6. `git push origin soutenance`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Améliorations **non** bloquantes (ne pas faire avant validation sauf si tu veux)
|
||||||
|
|
||||||
|
| Idée | Priorité |
|
||||||
|
|------|----------|
|
||||||
|
| SnackBar « Déconnecté » au logout | Cosmétique |
|
||||||
|
| Pull-to-refresh profil quand réseau revient | Hors sujet |
|
||||||
|
| Afficher `evaluation_points` | Optionnel sujet |
|
||||||
|
| Invalider cache profil à la perte réseau | Optionnel |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens
|
||||||
|
|
||||||
|
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — oral + repères code
|
||||||
|
- [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) — rendu Git
|
||||||
|
- [guide-soutenance-realisation.md](guide-soutenance-realisation.md) — sujet ↔ fichiers
|
||||||
|
|
||||||
|
*Dernière mise à jour : retours test device (logout, hidden, cache profil, skills).*
|
||||||
@@ -1,7 +1,37 @@
|
|||||||
# Renommer / copier en .env — ne jamais committer .env
|
# =============================================================================
|
||||||
# Application OAuth : https://profile.intra.42.fr/oauth/applications
|
# Swifty Companion — modèle de configuration (PAS de secrets réels ici)
|
||||||
|
# =============================================================================
|
||||||
|
#
|
||||||
|
# 1) Copier ce fichier vers le package Flutter :
|
||||||
|
# cp .env.example swifty_companion/.env
|
||||||
|
# (depuis la racine de ce dépôt, dossier swiftyCompanion/)
|
||||||
|
#
|
||||||
|
# 2) Remplir les valeurs depuis ton application OAuth sur l’Intra 42 :
|
||||||
|
# https://profile.intra.42.fr/oauth/applications
|
||||||
|
#
|
||||||
|
# 3) Ne JAMAIS committer le fichier .env (le sujet prévoit l’échec direct si
|
||||||
|
# des identifiants sont exposés publiquement). Ce .env.example peut être
|
||||||
|
# versionné : il ne contient que des noms de variables et des exemples.
|
||||||
|
#
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# CLIENT_ID — sur l’Intra, champ « UID » de l’application OAuth.
|
||||||
|
# C’est l’identifiant public de ton app (pas ton login 42).
|
||||||
|
# Exemple de forme : u-s4t2ud-xxxxxxxx...
|
||||||
CLIENT_ID=
|
CLIENT_ID=
|
||||||
|
|
||||||
|
# CLIENT_SECRET — sur l’Intra, champ « SECRET » de l’application OAuth.
|
||||||
|
# Sert à échanger le code OAuth contre un access token. À garder strictement
|
||||||
|
# privé : uniquement dans .env local, jamais sur Git ni dans le chat.
|
||||||
|
# Exemple de forme : s-s4t2ud-xxxxxxxx...
|
||||||
CLIENT_SECRET=
|
CLIENT_SECRET=
|
||||||
# Identique au champ « URI de redirection » sur l’Intra (une seule ligne, sans guillemets)
|
|
||||||
|
# REDIRECT_URI — URI de redirection enregistrée sur l’Intra (caractère pour
|
||||||
|
# caractère identique dans le formulaire et ici).
|
||||||
|
# Pour un téléphone Android avec ce projet : schéma custom + host callback
|
||||||
|
# (voir aussi android/app/src/main/AndroidManifest.xml).
|
||||||
REDIRECT_URI=com.example.swiftycompanion://callback
|
REDIRECT_URI=com.example.swiftycompanion://callback
|
||||||
|
|
||||||
|
# Scopes à cocher sur l’Intra lors de la création de l’app (pas dans .env) :
|
||||||
|
# - public (données publiques utilisateur)
|
||||||
|
# - projects (liste projects_users sur le profil — sans cela, erreur 403)
|
||||||
|
|||||||
@@ -1,87 +1,102 @@
|
|||||||
# Swifty Companion
|
# Swifty Companion
|
||||||
|
|
||||||
Application mobile — profil étudiant 42 via l’**API Intra** (OAuth2).
|
Application mobile Flutter : recherche d’un login 42 et affichage du profil (API Intra, OAuth2).
|
||||||
|
|
||||||
> **Dépôt de rendu école** : ce dossier est la **racine** du dépôt Git Vogosphere. Pas de `docs/` ni de sujet traduit ici (dépôt perso séparé).
|
## Prérequis
|
||||||
|
|
||||||
**État :** partie obligatoire + **bonus refresh token** (401 → refresh → retry API).
|
- Flutter SDK (voir [flutter.dev](https://flutter.dev))
|
||||||
|
- Un téléphone ou émulateur Android configuré (`flutter doctor`)
|
||||||
|
- Une application OAuth créée sur l’Intra : [Applications API](https://profile.intra.42.fr/oauth/applications)
|
||||||
|
|
||||||
## Lancer le projet
|
## Installation
|
||||||
|
|
||||||
|
À la racine de **ce** dépôt Git (dossier qui contient `swifty_companion/`) :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd swifty_companion
|
cd swifty_companion
|
||||||
# Créer .env ici (voir .env.example à la racine du dépôt école)
|
cp ../.env.example .env
|
||||||
flutter pub get
|
|
||||||
flutter run -d <device>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Le fichier **`.env`** doit être dans **`swifty_companion/.env`** (déclaré comme asset dans `pubspec.yaml`).
|
Édite `swifty_companion/.env` : renseigne `CLIENT_ID`, `CLIENT_SECRET` et `REDIRECT_URI` (voir commentaires dans `.env.example` à la racine).
|
||||||
|
|
||||||
Vérifications :
|
Puis :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
flutter doctor
|
flutter pub get
|
||||||
flutter devices
|
flutter run -d <identifiant_appareil>
|
||||||
|
```
|
||||||
|
|
||||||
|
Lister les appareils : `flutter devices`.
|
||||||
|
|
||||||
|
## Configuration OAuth (résumé)
|
||||||
|
|
||||||
|
| Variable `.env` | Sur l’Intra 42 |
|
||||||
|
|-----------------|----------------|
|
||||||
|
| `CLIENT_ID` | **UID** de l’application |
|
||||||
|
| `CLIENT_SECRET` | **SECRET** de l’application |
|
||||||
|
| `REDIRECT_URI` | **URI de redirection** (ex. `com.example.swiftycompanion://callback`) |
|
||||||
|
|
||||||
|
Scopes à activer sur l’application : **public** et **projects**.
|
||||||
|
|
||||||
|
La redirect URI doit être la même sur l’Intra, dans `.env`, et dans la config Android du projet.
|
||||||
|
|
||||||
|
**Important :** le fichier `.env` reste sur ta machine uniquement. Ce dépôt contient `.env.example` sans vraies clés. Ne pousse jamais `.env` sur le Git de rendu.
|
||||||
|
|
||||||
|
Documentation API : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
1. Ouvrir l’app → **Se connecter avec 42** (une fois par session).
|
||||||
|
2. Saisir un **login** → **Rechercher**.
|
||||||
|
3. Consulter le **profil** (photo, informations, skills, projets validés ou en échec).
|
||||||
|
4. **Retour** → nouvelle recherche avec le même token (pas de nouvelle connexion OAuth à chaque fois).
|
||||||
|
|
||||||
|
Certaines données peuvent s’afficher comme « — » : l’API peut masquer des champs selon la confidentialité.
|
||||||
|
|
||||||
|
**Photo :** l’URL est fournie par l’API (`image` sur la fiche user) ; le fichier JPEG est chargé depuis `cdn.intra.42.fr` avec le **même** access token OAuth (pas un second login).
|
||||||
|
|
||||||
|
## Vérifications avant soutenance
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd swifty_companion
|
||||||
flutter analyze
|
flutter analyze
|
||||||
flutter test
|
flutter test
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration (`.env`)
|
## Critères du sujet (v4) couverts par l’app
|
||||||
|
|
||||||
1. App OAuth : [profile.intra.42.fr/oauth/applications](https://profile.intra.42.fr/oauth/applications)
|
|
||||||
2. Scopes : **`public`** + **`projects`** (projets sur le profil)
|
|
||||||
3. Redirect URI (ex. téléphone) : `com.example.swiftycompanion://callback` — identique sur l’Intra et dans `.env`
|
|
||||||
4. Ne **jamais** committer `.env`
|
|
||||||
|
|
||||||
API : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
|
|
||||||
|
|
||||||
## Parcours app
|
|
||||||
|
|
||||||
1. **Se connecter avec 42** (token réutilisé pour toutes les requêtes)
|
|
||||||
2. **Rechercher** un login
|
|
||||||
3. **Profil** : photo, champs, skills (niveau + %), projets validés et failed
|
|
||||||
4. **Retour** → nouvelle recherche
|
|
||||||
|
|
||||||
## Partie obligatoire (rappel sujet v4)
|
|
||||||
|
|
||||||
| # | Exigence |
|
| # | Exigence |
|
||||||
|---|----------|
|
|---|----------|
|
||||||
| 1 | Au moins **2 vues** |
|
| 1 | Deux écrans : recherche et profil |
|
||||||
| 2 | **Toutes** les erreurs gérées (login inconnu, réseau, …) |
|
| 2 | Erreurs gérées (login inconnu, réseau, session, etc.) |
|
||||||
| 3 | Vue 2 : infos du login s’il **existe** |
|
| 3 | Profil affiché si le login existe |
|
||||||
| 4 | **≥ 4 détails** + **photo de profil** |
|
| 4 | Au moins quatre informations + photo |
|
||||||
| 5 | **Skills** : niveau + pourcentage |
|
| 5 | Skills avec niveau et pourcentage |
|
||||||
| 6 | **Projets** réalisés, **y compris les failed** |
|
| 6 | Projets terminés, y compris en échec |
|
||||||
| 7 | Retour possible vers la **vue 1** |
|
| 7 | Retour vers l’écran de recherche |
|
||||||
| 8 | Layout **adaptatif** |
|
| 8 | Mise en page adaptée (scroll, grands écrans) |
|
||||||
| 9 | **OAuth2** — **un token** réutilisé |
|
| 9 | Un seul flux OAuth, token réutilisé pour les appels API |
|
||||||
|
|
||||||
Certaines données peuvent être masquées par l’API (confidentialité).
|
|
||||||
|
|
||||||
## Bonus
|
## Bonus
|
||||||
|
|
||||||
- Refresh token : `IntraAuthService.refreshAccessToken()` + retry dans `intraApiGet` sur 401.
|
Rafraîchissement du token d’accès lorsque l’API renvoie 401, puis nouvelle tentative de la requête (sans refaire l’écran de connexion 42).
|
||||||
|
|
||||||
## Soutenance — checklist peer-eval
|
## Déroulé de démonstration (peer-eval)
|
||||||
|
|
||||||
| # | Test | Démo |
|
1. `flutter test`
|
||||||
|---|------|------|
|
2. `flutter run` → connexion 42 → login valide → profil complet
|
||||||
| 1 | `flutter test` | Tests OK |
|
3. Login invalide → message d’erreur
|
||||||
| 2 | `flutter run -d <device>` | Vue 1 → login valide → vue 2 |
|
4. Mode avion (ou réseau coupé) → message réseau
|
||||||
| 3 | Profil | Photo + ≥ 4 champs + skills + projets (dont failed) |
|
5. Retour à la recherche → nouvelle recherche
|
||||||
| 4 | Login invalide | Message clair, pas de crash |
|
6. Rotation ou petit écran → interface lisible
|
||||||
| 5 | Réseau coupé | Erreur réseau gérée |
|
7. Bonus : expliquer ou montrer le refresh token si demandé
|
||||||
| 6 | Navigation | Retour vue 1, nouvelle recherche |
|
|
||||||
| 7 | Petit écran / rotation | UI lisible |
|
|
||||||
| 8 | Bonus | Refresh token si implémenté |
|
|
||||||
|
|
||||||
**Ordre suggéré :** connexion → profil → login faux → hors ligne → retour → `flutter test`.
|
## Structure du dépôt
|
||||||
|
|
||||||
## Fichiers de ce dépôt
|
```text
|
||||||
|
.
|
||||||
| Fichier | Rôle |
|
├── README.md # ce fichier
|
||||||
|---------|------|
|
├── .gitignore
|
||||||
| `README.md` | Ce document |
|
├── .env.example # modèle (sans secrets)
|
||||||
| `.gitignore` | Exclut `.env`, build, etc. |
|
└── swifty_companion/ # code Flutter (lib/, android/, test/, …)
|
||||||
| `.env.example` | Modèle sans secrets |
|
```
|
||||||
| `swifty_companion/` | Projet Flutter (`lib/`, `android/`, …) |
|
|
||||||
|
|||||||
9
swiftyCompanion/swifty_companion/.env.example
Normal file
9
swiftyCompanion/swifty_companion/.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copier en .env dans CE dossier (swifty_companion/) :
|
||||||
|
# cp .env.example .env
|
||||||
|
# Puis éditer .env avec ton UID, SECRET et REDIRECT_URI Intra.
|
||||||
|
#
|
||||||
|
# Modèle détaillé (mêmes variables) : ../.env.example à la racine du dépôt.
|
||||||
|
|
||||||
|
CLIENT_ID=
|
||||||
|
CLIENT_SECRET=
|
||||||
|
REDIRECT_URI=com.example.swiftycompanion://callback
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
# swifty_companion
|
# Package Flutter — Swifty Companion
|
||||||
|
|
||||||
A new Flutter project.
|
Le projet s’exécute depuis ce dossier.
|
||||||
|
|
||||||
## Getting Started
|
## Démarrage rapide
|
||||||
|
|
||||||
This project is a starting point for a Flutter application.
|
```bash
|
||||||
|
cp .env.example .env # ou : cp ../.env.example .env
|
||||||
|
# Éditer .env (CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
|
||||||
|
flutter pub get
|
||||||
|
flutter run -d <device>
|
||||||
|
```
|
||||||
|
|
||||||
A few resources to get you started if this is your first Flutter project:
|
Instructions complètes (OAuth, soutenance, structure du dépôt) : **README.md à la racine du dépôt** (dossier parent).
|
||||||
|
|
||||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
|
||||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
|
||||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
|
||||||
|
|
||||||
For help getting started with Flutter development, view the
|
|
||||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
|
||||||
samples, guidance on mobile development, and a full API reference.
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Point d'entrée : charge les secrets depuis `.env` (asset, non versionné) puis vue recherche.
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../services/intra_api_exception.dart';
|
import '../services/intra_api_exception.dart';
|
||||||
|
import '../services/intra_auth_service.dart';
|
||||||
import '../services/intra_profile_service.dart';
|
import '../services/intra_profile_service.dart';
|
||||||
import '../theme/swifty_ui.dart';
|
import '../theme/swifty_ui.dart';
|
||||||
|
|
||||||
/// Vue 2 — profil complet (étape 4).
|
/// Vue 2 — photo, ≥ 4 champs, skills (niveau + %), projets validés et failed.
|
||||||
class ProfileScreen extends StatefulWidget {
|
class ProfileScreen extends StatefulWidget {
|
||||||
const ProfileScreen({super.key, required this.user});
|
const ProfileScreen({super.key, required this.user});
|
||||||
|
|
||||||
@@ -56,11 +60,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extrait l’URL photo depuis la fiche `GET /v2/users/{login}` (critère sujet : photo de profil).
|
||||||
|
///
|
||||||
|
/// Ordre : `image.link` → `image.versions` (large…micro) → champ legacy `image_url`.
|
||||||
String? _imageUrl(Map<String, dynamic> user) {
|
String? _imageUrl(Map<String, dynamic> user) {
|
||||||
final image = user['image'];
|
final image = user['image'];
|
||||||
if (image is Map<String, dynamic>) {
|
if (image is Map<String, dynamic>) {
|
||||||
final link = image['link'];
|
final link = image['link'];
|
||||||
if (link is String && link.isNotEmpty) return link;
|
if (link is String && link.isNotEmpty) return link;
|
||||||
|
|
||||||
|
final versions = image['versions'];
|
||||||
|
if (versions is Map<String, dynamic>) {
|
||||||
|
for (final key in ['large', 'medium', 'small', 'micro']) {
|
||||||
|
final v = versions[key];
|
||||||
|
if (v is String && v.isNotEmpty) return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final url = user['image_url'];
|
final url = user['image_url'];
|
||||||
if (url is String && url.isNotEmpty) return url;
|
if (url is String && url.isNotEmpty) return url;
|
||||||
@@ -172,6 +187,71 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Avatar profil : charge l’image via HTTP avec le Bearer OAuth (`IntraAuthService.cdnImageHeaders`).
|
||||||
|
///
|
||||||
|
/// Un simple `NetworkImage(url)` sans token échoue souvent sur le CDN 42. Si le chargement
|
||||||
|
/// échoue ou l’URL est absente, affiche l’initiale du login (conforme encadré confidentialité sujet).
|
||||||
|
class _IntraProfileAvatar extends StatefulWidget {
|
||||||
|
const _IntraProfileAvatar({required this.login, required this.imageUrl});
|
||||||
|
|
||||||
|
final String login;
|
||||||
|
final String? imageUrl;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_IntraProfileAvatar> createState() => _IntraProfileAvatarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IntraProfileAvatarState extends State<_IntraProfileAvatar> {
|
||||||
|
Uint8List? _bytes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant _IntraProfileAvatar oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.imageUrl != widget.imageUrl) {
|
||||||
|
_bytes = null;
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GET binaire sur l’URL CDN ; même session que les appels `api.intra.42.fr`.
|
||||||
|
Future<void> _load() async {
|
||||||
|
final url = widget.imageUrl;
|
||||||
|
if (url == null || url.isEmpty) return;
|
||||||
|
|
||||||
|
final headers = IntraAuthService.instance.cdnImageHeaders();
|
||||||
|
try {
|
||||||
|
final response = await http.get(Uri.parse(url), headers: headers);
|
||||||
|
if (response.statusCode == 200 && mounted) {
|
||||||
|
setState(() => _bytes = response.bodyBytes);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Initiale affichée en repli.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _initial() {
|
||||||
|
final letter =
|
||||||
|
widget.login.isNotEmpty ? widget.login[0].toUpperCase() : '?';
|
||||||
|
return Text(letter, style: const TextStyle(fontSize: 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 56,
|
||||||
|
backgroundImage:
|
||||||
|
_bytes != null ? MemoryImage(_bytes!) : null,
|
||||||
|
child: _bytes == null ? _initial() : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ErrorBody extends StatelessWidget {
|
class _ErrorBody extends StatelessWidget {
|
||||||
const _ErrorBody({required this.message, required this.onRetry});
|
const _ErrorBody({required this.message, required this.onRetry});
|
||||||
|
|
||||||
@@ -206,17 +286,7 @@ class _ProfileHeader extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
_IntraProfileAvatar(login: login, imageUrl: imageUrl),
|
||||||
radius: 56,
|
|
||||||
backgroundImage:
|
|
||||||
imageUrl != null ? NetworkImage(imageUrl!) : null,
|
|
||||||
child: imageUrl == null
|
|
||||||
? Text(
|
|
||||||
login.isNotEmpty ? login[0].toUpperCase() : '?',
|
|
||||||
style: const TextStyle(fontSize: 32),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
login,
|
login,
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
loading: _searching,
|
loading: _searching,
|
||||||
onPressed: _search,
|
onPressed: _search,
|
||||||
),
|
),
|
||||||
|
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ class IntraAuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// En-têtes pour télécharger la photo sur `cdn.intra.42.fr`.
|
||||||
|
///
|
||||||
|
/// L’URL vient du JSON user (`image.link` / `versions`). Le CDN refuse souvent
|
||||||
|
/// les requêtes sans Bearer (403). On réutilise le **même** access token que pour
|
||||||
|
/// l’API — pas un second OAuth. On n’envoie pas `Accept: application/json` ici.
|
||||||
|
Map<String, String> cdnImageHeaders() {
|
||||||
|
if (!isAuthenticated) return const {};
|
||||||
|
return {'Authorization': 'Bearer $_accessToken'};
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> fetchMe() async {
|
Future<Map<String, dynamic>> fetchMe() async {
|
||||||
final response = await intraApiGet(
|
final response = await intraApiGet(
|
||||||
Uri.parse('${IntraConfig.apiBaseUrl}/me'),
|
Uri.parse('${IntraConfig.apiBaseUrl}/me'),
|
||||||
@@ -139,8 +149,7 @@ class IntraAuthService {
|
|||||||
_refreshToken = refresh;
|
_refreshToken = refresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simule un access token expiré (garde le refresh) — tests bonus en debug.
|
/// Simule un access token expiré (garde le refresh) — démo bonus, appelé depuis l’UI en `kDebugMode`.
|
||||||
@visibleForTesting
|
|
||||||
void debugInvalidateAccessToken() {
|
void debugInvalidateAccessToken() {
|
||||||
if (!canRefresh) return;
|
if (!canRefresh) return;
|
||||||
_accessToken = 'invalid_access_token_for_refresh_test';
|
_accessToken = 'invalid_access_token_for_refresh_test';
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Duration _retryDelay(http.Response response, int attempt) {
|
|||||||
return Duration(milliseconds: ms.clamp(800, 8000));
|
return Duration(milliseconds: ms.clamp(800, 8000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lance une [IntraApiException] si le code HTTP n'est pas 200.
|
||||||
void ensureIntraOk(http.Response response) {
|
void ensureIntraOk(http.Response response) {
|
||||||
if (response.statusCode == 401) {
|
if (response.statusCode == 401) {
|
||||||
throw const IntraUnauthorizedException();
|
throw const IntraUnauthorizedException();
|
||||||
@@ -67,6 +68,7 @@ void ensureIntraOk(http.Response response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Décode le corps JSON après contrôle du statut HTTP.
|
||||||
dynamic decodeIntraBody(http.Response response) {
|
dynamic decodeIntraBody(http.Response response) {
|
||||||
ensureIntraOk(response);
|
ensureIntraOk(response);
|
||||||
return jsonDecode(response.body);
|
return jsonDecode(response.body);
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ class _ProfileCacheEntry {
|
|||||||
final DateTime fetchedAt;
|
final DateTime fetchedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Agrège fiche user, skills (cursus, pas `/experiences`) et `projects_users` (scope `projects`).
|
||||||
class IntraProfileService {
|
class IntraProfileService {
|
||||||
IntraProfileService._();
|
IntraProfileService._();
|
||||||
static final IntraProfileService instance = IntraProfileService._();
|
static final IntraProfileService instance = IntraProfileService._();
|
||||||
|
|
||||||
|
/// Cache court + délai entre GET pour limiter les 429 sur l'API 42.
|
||||||
static const Duration _cacheTtl = Duration(minutes: 2);
|
static const Duration _cacheTtl = Duration(minutes: 2);
|
||||||
static const Duration _betweenRequests = Duration(milliseconds: 450);
|
static const Duration _betweenRequests = Duration(milliseconds: 450);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'intra_api_exception.dart';
|
|||||||
import 'intra_auth_service.dart';
|
import 'intra_auth_service.dart';
|
||||||
import 'intra_http.dart';
|
import 'intra_http.dart';
|
||||||
|
|
||||||
|
/// Recherche d'un login via `GET /v2/users?filter[login]=` (même token OAuth que le profil).
|
||||||
class IntraUserService {
|
class IntraUserService {
|
||||||
IntraUserService._();
|
IntraUserService._();
|
||||||
static final IntraUserService instance = IntraUserService._();
|
static final IntraUserService instance = IntraUserService._();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Constantes UI partagées (étape 5 — layout).
|
/// Constantes UI partagées (layout responsive, thème 42).
|
||||||
abstract final class SwiftyUi {
|
abstract final class SwiftyUi {
|
||||||
static const Color brand = Color(0xFF00BABC);
|
static const Color brand = Color(0xFF00BABC);
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ class ResponsiveScrollBody extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Message d'erreur centré (recherche ou profil).
|
||||||
class ErrorMessage extends StatelessWidget {
|
class ErrorMessage extends StatelessWidget {
|
||||||
const ErrorMessage({super.key, required this.message});
|
const ErrorMessage({super.key, required this.message});
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ class ErrorMessage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bouton principal avec indicateur de chargement.
|
||||||
class LoadingFilledButton extends StatelessWidget {
|
class LoadingFilledButton extends StatelessWidget {
|
||||||
const LoadingFilledButton({
|
const LoadingFilledButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
Reference in New Issue
Block a user