Compare commits

...

4 Commits

Author SHA1 Message Date
7a06d891b4 nettoyage 2026-10-21 09:02:37 +02:00
1ce33a69d1 lot4 2026-10-21 08:54:03 +02:00
4f375e6766 lot3 2026-10-21 08:50:47 +02:00
5d79d6c688 lot2 2026-10-21 08:49:37 +02:00
35 changed files with 137 additions and 1707 deletions

View File

@@ -1,55 +0,0 @@
# Swifty Companion — dépôt de travail
Espace **perso** : sujet traduit, pédagogie, notes. Le rendu à l’école ne contient **pas** tout ce dépôt.
## Deux dépôts Git distincts
| Rôle | Où | Contenu typique |
|------|-----|-----------------|
| **Travail** (ce repo) | Racine `swifty_companion/` | `docs/`, historique, éventuellement une copie du code |
| **École** (Intra / Vogosphere) | Racine = contenu de **`swiftyCompanion/`** | App mobile + `README.md` + `.gitignore` + `.env.example` uniquement |
Le sous-dossier [`swiftyCompanion/`](swiftyCompanion/) est le **paquet de rendu** : README et `.gitignore` **autonomes**, prêts pour le futur dépôt école.
### Pousser vers l’école (quand le code est prêt)
**Option A — Git dans le sous-dossier** (recommandé en local) :
```bash
cd swiftyCompanion
git init
git remote add origin <url-vogosphere-swifty_companion>
# premier push : uniquement les fichiers de ce dossier
```
Dans le dépôt de **travail**, ignorer le `.git` imbriqué (déjà dans `.gitignore` racine) pour éviter un repo dans un repo par accident.
**Option B — Clone Vogosphere vide** puis copier le contenu de `swiftyCompanion/` (sans `docs/`) à la racine du clone.
Ne pas pousser : `docs/`, traductions, PDF, dépôt perso.
## Documentation (travail uniquement)
| Document | Lien |
|----------|------|
| Sujet FR (v4) | [docs/sujets/swifty-companion.md](docs/sujets/swifty-companion.md) |
| PDF officiel | [docs/sujets/originaux/swifty_companion_subject.pdf](docs/sujets/originaux/swifty_companion_subject.pdf) |
| Plan pas à pas | [docs/pedagogie/README.md](docs/pedagogie/README.md) |
| Index doc | [docs/README.md](docs/README.md) |
| Piscine mobile (référence) | [../piscine_mobile/README.md](../piscine_mobile/README.md) |
## Rendu école
Tout ce qui concerne correcteurs et soutenance : **[swiftyCompanion/README.md](swiftyCompanion/README.md)** (à lire depuis le dépôt Intra une fois poussé).
## Reprise du projet
**État actuel :** app Flutter dans `swiftyCompanion/swifty_companion/`, partie obligatoire codée.
→ [docs/pedagogie/00-reprise-et-etat.md](docs/pedagogie/00-reprise-et-etat.md) · **correction :** [guide-soutenance-realisation.md](docs/pedagogie/guide-soutenance-realisation.md)
## Démarrage
1. Lire le [sujet traduit](docs/sujets/swifty-companion.md).
2. Suivre [00-reprise-et-etat.md](docs/pedagogie/00-reprise-et-etat.md) puis le [plan pédagogique](docs/pedagogie/README.md).
3. Coder uniquement dans `swiftyCompanion/` ([README rendu](swiftyCompanion/README.md)).

View File

@@ -1,25 +0,0 @@
# Documentation — Swifty Companion
Documentation du projet, sur le modèle de [piscine_mobile/docs](../piscine_mobile/docs/README.md).
## Sujet officiel
→ [sujets/README.md](sujets/README.md)
| Document | Lien |
|----------|------|
| Traduction française (v4) | [sujets/swifty-companion.md](sujets/swifty-companion.md) |
| PDF Intra | [sujets/originaux/swifty_companion_subject.pdf](sujets/originaux/swifty_companion_subject.pdf) |
## Pédagogie
| Document | Lien |
|----------|------|
| **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) |
## Dépôt de rendu 42 (hors dépôt de travail)
Le code et le README correcteurs vivent dans **`swiftyCompanion/`** — racine du futur dépôt Intra, séparé de ce dépôt perso.
→ [../swiftyCompanion/README.md](../swiftyCompanion/README.md) · workflow : [../README.md](../README.md#deux-dépôts-git-distincts)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,109 +0,0 @@
# Reprise du projet — où nous en sommes
> Dernière mise à jour : étape 7 bonus (refresh token) codée.
## En bref
| | |
|---|---|
| **Fait** | Structure dépôt perso, sujet FR, paquet rendu, **projet Flutter** (`swifty_companion/`) |
| **Fait** | OAuth + token + `/v2/me` (login connecté) |
| **Fait** | Vue 1 recherche + vue 2 profil (skills, projets, ≥ 4 champs) |
| **Prochaine action** | **Rendu Vogosphere** + test manuel bonus (token invalide) |
## Arborescence actuelle
```
swifty_companion/ ← dépôt Git de TRAVAIL (perso)
├── README.md ← explique les 2 dépôts Git
├── .gitignore ← ignore docs + swiftyCompanion/.git/
├── docs/
│ ├── README.md
│ ├── sujets/
│ │ ├── swifty-companion.md ← sujet traduit (référence)
│ │ ├── originaux/swifty_companion_subject.pdf
│ │ └── captures/ ← visuels PDF (couverture uniquement)
│ └── pedagogie/
│ ├── README.md ← plan des étapes
│ └── 00-reprise-et-etat.md ← ce fichier
└── swiftyCompanion/ ← futur dépôt ÉCOLE (racine Vogosphere)
├── README.md
├── .env.example
└── swifty_companion/ ← projet Flutter (lib/, android/, pubspec.yaml)
```
## Décisions déjà prises
1. **Deux dépôts Git** : tout `docs/` et la traduction restent dans le repo perso ; l’Intra ne reçoit que le contenu de `swiftyCompanion/`.
2. **Stack** : Flutter, `flutter_dotenv`, `flutter_appauth`, `http`.
3. **Secrets** : `.env` local uniquement, jamais commité (modèle dans `swiftyCompanion/.env.example`).
4. **Redirect OAuth mobile** : `com.example.swiftycompanion://callback` (pas `localhost` sur téléphone physique).
5. **Sujet PDF** : pas de maquettes d’écrans — l’UI suit la liste des critères obligatoires ([sujet FR](../sujets/swifty-companion.md)).
## Avancement par étape
| Étape | Contenu | Statut |
|-------|---------|--------|
| **0** | Structure, traduction sujet, séparation travail / école, README rendu | ✅ **Terminé** |
| **1** | Projet Flutter `swiftyCompanion/swifty_companion/` | ✅ **Terminé** |
| **2** | OAuth2 (`IntraAuthService`, `/v2/me`) | ✅ **Validé** |
| **3** | Vue 1 : `SearchScreen` + `IntraUserService` + navigation profil | ✅ **Codé** — tester sur téléphone |
| **4** | Vue 2 : `IntraProfileService` + profil (skills, projets, champs) | ✅ **Codé** — tester sur téléphone |
| **5** | Retour vue 1 + layout adaptatif (`swifty_ui.dart`) | ✅ **Codé** — [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| **6** | Checklist soutenance + rendu école | ✅ **Guide** — [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| **7** | Bonus : refresh token à expiration | ✅ **Codé** — [etape-07-bonus-token.md](etape-07-bonus-token.md) |
## Code applicatif (`swifty_companion/lib/`)
| Fichier | Rôle |
|---------|------|
| `config/intra_config.dart` | `.env`, URLs OAuth / API |
| `services/intra_auth_service.dart` | OAuth + refresh token + `GET /v2/me` |
| `services/intra_oauth_tokens.dart` | Parse réponse `/oauth/token` |
| `services/intra_user_service.dart` | `GET /v2/users?filter[login]=` |
| `services/intra_profile_service.dart` | Profil : user, cursus_users, projects_users |
| `services/intra_api_exception.dart` | Erreurs métier |
| `screens/search_screen.dart` | **Vue 1** |
| `screens/profile_screen.dart` | **Vue 2** |
| `theme/swifty_ui.dart` | Layout responsive partagé |
| `main.dart` | `home: SearchScreen()` |
## Reprendre le travail (checklist)
1. Ouvrir le dépôt : `~/projets/42School/swifty_companion`
2. Relire le [sujet traduit](../sujets/swifty-companion.md) (section obligatoire).
3. Vérifier Flutter : `flutter doctor` et `flutter devices`.
4. Lancer l’app (après modification `.env`, relancer `flutter run`) :
```bash
cd swiftyCompanion/swifty_companion
flutter run -d <device>
```
5. Tester le parcours : OAuth → recherche → profil → retour → erreurs réseau.
## Fichiers utiles selon le contexte
| Tu veux… | Fichier |
|----------|---------|
| **Préparer la correction (sujet ↔ fichiers)** | **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** |
| Comprendre le sujet | [docs/sujets/swifty-companion.md](../sujets/swifty-companion.md) |
| Plan complet des étapes | [docs/pedagogie/README.md](README.md) |
| Vue profil (étape 4) | [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| Workflow Git travail / école | [README racine](../../README.md) |
| Soutenance / peer-eval | [swiftyCompanion/README.md](../../swiftyCompanion/README.md) |
## Guides détaillés
| Fichier | Statut |
|---------|--------|
| [etape-02-oauth-intra.md](etape-02-oauth-intra.md) | ✅ |
| [etape-03-vue-recherche.md](etape-03-vue-recherche.md) | ✅ |
| [etape-04-vue-profil.md](etape-04-vue-profil.md) | ✅ |
| [etape-05-navigation-layout.md](etape-05-navigation-layout.md) | ✅ |
| [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) | ✅ |
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | ✅ piste bonus |
## Notes pour la session suivante
- Partie obligatoire : prête pour peer-eval si la checklist étape 6 est OK sur device.
- `git init` dans `swiftyCompanion/` quand tu pousses sur Vogosphere (voir README racine).
- Bonus refresh token : étape 7 si tu as le temps après rendu.

View File

@@ -1,50 +0,0 @@
# Pédagogie — Swifty Companion
Parcours pour construire le projet **étape par étape**.
## Reprise rapide
→ **[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).
## Prérequis
Base via la [piscine mobile](../../../piscine_mobile/README.md) sur le même disque :
- [Flutter — Ubuntu natif](../../../piscine_mobile/docs/pedagogie/flutter/09-ubuntu-natif-dev-android.md)
- [Stratégie VM école / correction](../../../piscine_mobile/docs/pedagogie/flutter/07-strategie-wsl-vm-ecole-correction.md)
## Étapes du projet
| Étape | Objectif | Statut |
|-------|----------|--------|
| **0** | Dépôt travail + `docs/`, sujet FR, paquet `swiftyCompanion/` | ✅ **Fait** — [00-reprise-et-etat.md](00-reprise-et-etat.md) |
| **1** | Projet `swiftyCompanion/swifty_companion/` + `flutter run` | ✅ **Fait** |
| **2** | OAuth2 Intra : token + `/v2/me` | ✅ **Fait** — [etape-02-oauth-intra.md](etape-02-oauth-intra.md) |
| **3** | **Vue 1** : recherche par login + erreurs | ✅ **Codé** — [etape-03-vue-recherche.md](etape-03-vue-recherche.md) |
| **4** | **Vue 2** : profil (≥ 4 champs, skills, projets failed) | ✅ **Codé** — [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| **5** | Navigation retour + layout responsive | ✅ **Codé** — [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| **6** | Tests + checklist peer-eval + rendu Git école | ✅ **Guide** — [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| **7** | **Bonus** : refresh token à l’expiration | ✅ **Codé** — [etape-07-bonus-token.md](etape-07-bonus-token.md) |
README correcteurs (dépôt école) : [swiftyCompanion/README.md](../../swiftyCompanion/README.md)
## Sujet de référence
→ [Traduction FR v4](../sujets/swifty-companion.md) · PDF : [originaux/swifty_companion_subject.pdf](../sujets/originaux/swifty_companion_subject.pdf)
## Guides détaillés
| Fichier | Statut |
|---------|--------|
| [00-reprise-et-etat.md](00-reprise-et-etat.md) | ✅ À jour |
| [etape-02-oauth-intra.md](etape-02-oauth-intra.md) | ✅ Guide étape 2 |
| [etape-03-vue-recherche.md](etape-03-vue-recherche.md) | ✅ Guide étape 3 |
| [etape-04-vue-profil.md](etape-04-vue-profil.md) | ✅ Guide étape 4 |
| [etape-05-navigation-layout.md](etape-05-navigation-layout.md) | ✅ Guide étape 5 |
| [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) | ✅ Guide étape 6 |
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | ✅ Bonus refresh |
| **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** | ✅ **Guide correction / soutenance** |
Demande dans le chat l’étape à ouvrir : le guide correspondant sera rédigé au moment où tu l’attaques.

View File

@@ -1,109 +0,0 @@
# Audit conformité usage API 42 — Swifty Companion
> **Date :** audit local dépôt de travail
> **Contraintes respectées :** aucune lecture du fichier `.env` local actuel ; **aucune requête** vers `api.intra.42.fr`.
Objectif : vérifier si le **code et le dépôt** peuvent expliquer une **extraction massive** (~140k profils) du type décrit par le mail 42 — ou seulement un usage **pédagogique ponctuel**.
---
## 1. Verdict global
| Question | Réponse |
|----------|---------|
| Le **code Flutter** ressemble-t-il à un scraper massif ? | **Non** |
| Usage prévu du sujet Swifty Companion ? | **Oui** (1 login → 1 profil) |
| Risque d’être l’**auteur principal** de la fuite nationale décrite ? | **Très improbable** si tu n’as pas lancé d’autres scripts |
| Point **sécurité** à corriger dans **ton** Git ? | **Oui** — voir §4 |
---
## 2. Analyse du code (`swiftyCompanion/swifty_companion/lib/`)
### Appels API présents
| Fichier | Comportement | Volume typique |
|---------|--------------|----------------|
| `intra_user_service.dart` | `GET /v2/users?filter[login]=<un login>` | **1 utilisateur** par recherche |
| `intra_profile_service.dart` | `GET /users/{login}`, évent. `cursus_users`, `projects_users` (page 100) | **1 profil** par navigation |
| `intra_auth_service.dart` | OAuth + `GET /me` + refresh token | Session |
| `intra_http.dart` | GET avec retry 429, refresh sur 401 | Par requête ci-dessus |
### Absent du dépôt (recherché)
- Pas de script `sh` / `py` d’export massif
- Pas de boucle sur campus / pagination `page[number]` sur tous les users
- Pas de `filter[campus]`, pas de dump CSV/JSON de milliers de profils
- Pas de stockage fichier des profils (cache mémoire **2 min**, **1 id** à la fois)
**Conclusion technique :** l’application ne peut pas, **seule**, extraire 140k profils sans modification majeure du code.
---
## 3. Lien avec le mail école
Le mail décrit :
- extraction **automatisée** à très grande échelle ;
- **multi-campus**, candidats + étudiants ;
- via un **compte légitime** (pas forcément ton app OAuth).
Ton projet :
- recherche **manuelle** par login ;
- quelques requêtes par profil ;
- aligné sur le sujet (« consulter un pair »).
**Ce n’est pas le même profil d’usage**, sauf si tu as **en plus** utilisé un autre outil, un script perso, ou partagé des données.
---
## 4. Risque sécurité — dépôt Git (hors fuite massive)
Audit Git **sans afficher les valeurs** des secrets :
| Élément | État |
|---------|------|
| `swifty_companion/.env` | **Ignoré** par `.gitignore` (OK) |
| `swifty_companion/.env copy` | **Versionné dans Git** (commit `06fe43b`) |
| Contenu versionné | Présence de `CLIENT_ID` et `CLIENT_SECRET` dans ce fichier (valeurs **non** recopiées dans ce document) |
| Remote | `origin` → **Gitea privé** perso (pas Vogosphere 42) |
**Risque :** si le dépôt est **privé** et que **toi seul** y as accès, l’exposition des clés via `.env copy` est **limitée** (pas équivalent à un repo public). Ça ne te relie **pas** à l’incident ~140k profils. Reste une **mauvaise pratique** (historique Git + sujet 42) : régénérer le SECRET si tu as déjà poussé ce fichier, retirer `.env copy` du suivi Git pour l’avenir.
### Actions recommandées (à faire de ton côté)
1. **Régénérer le SECRET** sur [Applications OAuth Intra](https://profile.intra.42.fr/oauth/applications) (même si déjà fait après le chat).
2. **Retirer** `swifty_companion/.env copy` du suivi Git (`git rm --cached`, supprimer le fichier, commit).
3. Si le dépôt distant a déjà reçu ce fichier : considérer **réécriture d’historique** ou invalidation des anciens secrets (SECRET régénéré = ancien jeton mort).
4. Ne jamais committer `.env`, captures avec clés, ou dumps de profils.
---
## 5. Ce que cet audit n’a pas fait
- Pas d’appel à l’API 42 (pas de logs côté 42 consultés).
- Pas de lecture du `.env` actuel sur disque.
- Pas d’accès aux serveurs 42 / pas de corrélation avec l’enquête interne.
Seul **42** ou la police pourrait lier un compte OAuth à un volume de requêtes précis.
---
## 6. En cas de contact DPO / campus
Tu peux décrire factuellement :
- projet **Swifty Companion** (Flutter) ;
- recherche **par login** uniquement ;
- pas d’export massif dans le code livré ;
- app OAuth **pédagogique** avec scopes `public` + `projects`.
Contact mail incident : **dpo@42.tech** (comme indiqué par l’école).
---
## 7. Références
- [guide-soutenance-realisation.md](guide-soutenance-realisation.md) — cartographie sujet ↔ code
- [sujet traduit](../sujets/swifty-companion.md) — usage autorisé vs extraction massive (CGU 42)

View File

@@ -1,178 +0,0 @@
# Étape 2 — OAuth2 Intra + token unique
> **Prérequis :** étape 1 OK (`swiftyCompanion/swifty_companion/` créé, `flutter run` fonctionne).
## Objectif de l’étape
1. Obtenir un **access token** via **OAuth2 de l’Intra** (pas un token par requête API).
2. Stocker les secrets dans **`.env`** (racine `swiftyCompanion/` ou dossier `swifty_companion/` — un seul endroit, documenté dans le README).
3. Avoir un **service d’auth** réutilisable pour toutes les futures requêtes `api.intra.42.fr`.
Les **vues** (recherche login, profil) viennent aux étapes 3–4 ; ici on pose l’authentification API.
---
## 2.1 — Vérifier l’app
```bash
cd ~/projets/42School/swifty_companion/swiftyCompanion/swifty_companion
flutter pub get
flutter run -d <device>
```
L’écran compteur par défaut suffit pour valider l’étape 1.
---
## 2.2 — Créer l’application OAuth sur l’Intra
1. [profile.intra.42.fr/oauth/applications](https://profile.intra.42.fr/oauth/applications) → **New application**.
2. Renseigner :
- **Name** : ex. `swifty_companion_dev`
- **Website** : URL factice ou repo perso (souvent requis)
- **Redirect URI** : doit correspondre **exactement** à ce que l’app utilisera (voir 2.3)
3. Noter **UID** (= `CLIENT_ID`) et **SECRET** (= `CLIENT_SECRET`).
Copier `swiftyCompanion/.env.example` → `.env` (même niveau que le README rendu ou dans `swifty_companion/`) :
```env
CLIENT_ID=...
CLIENT_SECRET=...
REDIRECT_URI=...
```
---
## 2.3 — Choisir le `REDIRECT_URI` (mobile)
### Erreur Intra : « must be a valid URI »
Le formulaire **42 Intra** accepte en pratique surtout des URI **`http://` ou `https://`**.
Les schémas du type `com.example.swifty_companion://callback` sont souvent **refusés** (validateur strict + `_` dans le schéma).
**Type d’application Intra :** `42 pedagogic project` (normal pour Swifty Companion — pas de type « mobile »).
**URI de redirection** — format **http(s)://host:port/chemin** uniquement (pas de `com.example://…`).
Essayer **dans cet ordre** (une seule ligne dans le champ, sans guillemets ni espace) :
1. `http://localhost:8080/callback` ← le plus courant sur l’Intra 42
2. `http://localhost:3000/callback`
3. `http://127.0.0.1:8080/callback`
Exemple tiré de la [doc 42](https://api.intra.42.fr/apidoc/guides/web_application_flow) :
`http://localhost:1919/users/auth/ft/callback` (tu peux t’en inspirer, mais garde une URI simple type `/callback`).
Copier **exactement** la ligne acceptée dans `.env` et dans Flutter.
Avec `flutter_appauth`, Android/iOS interceptent cette URL de boucle locale après le login dans le navigateur — pas besoin d’un vrai serveur web.
| Approche | Redirect URI | Intra 42 |
|----------|--------------|----------|
| **Recommandé (Flutter + 42)** | `http://127.0.0.1:42800/callback` | ✅ en général accepté |
| Schéma custom | `com.example.swifty_companion://callback` | ❌ souvent refusé |
| Site web classique | `https://ton-domaine.com/callback` | ✅ si tu as un backend |
Le même URI doit être : formulaire Intra, `.env`, et paramètre `redirect_uri` dans l’app.
**Android** : `applicationId` = `com.example.swifty_companion` (`android/app/build.gradle.kts`) — indépendant du `REDIRECT_URI` loopback.
Endpoints 42 (référence) :
| Étape | URL |
|-------|-----|
| Autorisation | `GET https://api.intra.42.fr/oauth/authorize` |
| Token | `POST https://api.intra.42.fr/oauth/token` |
| API | `GET https://api.intra.42.fr/v2/...` avec header `Authorization: Bearer <token>` |
Paramètres authorize usuels : `client_id`, `redirect_uri`, `response_type=code`, `scope=public projects` (voir [intra-oauth-formulaire.md](intra-oauth-formulaire.md)).
---
## 2.4 — Dépendances Flutter suggérées
Dans `swifty_companion/` :
```bash
flutter pub add flutter_dotenv http
# selon le flux choisi, par ex. :
# flutter pub add flutter_appauth
```
| Package | Rôle |
|---------|------|
| `flutter_dotenv` | Charger `.env` au démarrage |
| `http` | Échange code → token + appels API |
| `flutter_appauth` | Flux OAuth2 + redirect natif (optionnel mais pratique) |
---
## 2.5 — Architecture minimale (fichiers à créer)
```
lib/
├── main.dart # dotenv.load + MaterialApp
├── config/
│ └── env.dart # lecture CLIENT_ID, etc.
└── services/
└── intra_auth_service.dart # token en mémoire (singleton)
```
Règles sujet :
- **Un seul token** : le service le garde après le login OAuth ; les futures requêtes réutilisent `getAccessToken()`.
- **Ne pas** appeler `/oauth/token` avant chaque `GET /v2/users/...`.
Pseudo-contrat du service :
```dart
class IntraAuthService {
String? _accessToken;
String? get accessToken => _accessToken;
bool get isAuthenticated => _accessToken != null;
Future<void> signIn(); // ouvre le flux OAuth, stocke le token
Future<String> getValidToken(); // retourne le token (bonus : refresh plus tard)
}
```
Test manuel étape 2 : bouton « Se connecter à l’Intra » → navigateur / WebView → retour app → afficher temporairement « Connecté » ou le login `/v2/me`.
---
## 2.6 — `.env` et assets
Si `flutter_dotenv` charge depuis la racine du package Flutter :
1. Placer `.env` dans `swifty_companion/` (ignoré par git).
2. Déclarer dans `pubspec.yaml` :
```yaml
flutter:
assets:
- .env
```
> En production soutenance : les correcteurs créent **leur** `.env` avec **leur** app OAuth ; ne jamais committer le fichier.
---
## 2.7 — Critère « terminé » pour l’étape 2
| Check | OK si |
|-------|--------|
| OAuth Intra | Connexion obtient un access token |
| Token unique | Plusieurs appels API utilisent le **même** token sans re-login |
| Secrets | Uniquement dans `.env`, pas dans le code en dur |
| Doc | README rendu rappelle où mettre `.env` et comment lancer l’app |
---
## Ensuite — étape 3
→ [etape-03-vue-recherche.md](etape-03-vue-recherche.md) (à rédiger quand l’étape 2 est validée) : **vue 1** — champ login, appel API, erreurs 404 / réseau.
Documentation API utile :
- [apidoc](https://api.intra.42.fr/apidoc)
- User : `GET /v2/users/:id` ou recherche par login (voir doc)

View File

@@ -1,106 +0,0 @@
# Étape 3 — Vue 1 : recherche par login
> **Prérequis :** étape 2 OK — OAuth actif, token unique (`IntraAuthService`).
## Objectif sujet
| Exigence | Cette étape |
|----------|-------------|
| Au moins 2 vues | **Vue 1** : recherche · **Vue 2** : profil (étape 4) |
| Erreurs gérées | Login introuvable, réseau, non connecté |
| Token unique | Réutiliser `IntraAuthService.instance` (ne pas refaire OAuth) |
---
## Parcours utilisateur (vue 1)
1. L’utilisateur est déjà connecté à l’API (étape 2) — ou bouton « Se connecter » si token absent.
2. **Champ texte** + bouton **Rechercher** (login 42, ex. `jdoe`).
3. Appel API avec `Authorization: Bearer <token>`.
4. **Succès** → navigation vers **vue 2** (profil).
5. **Échec** → message clair, pas de crash.
---
## API 42 utile
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
| Action | Requête typique |
|--------|-----------------|
| Utilisateur par login | `GET /v2/users/{login}` ou `GET /v2/users?filter[login]=<login>` |
| Toi (déjà fait) | `GET /v2/me` |
Vérifier dans l’apidoc la forme exacte supportée ; beaucoup de projets utilisent :
```http
GET https://api.intra.42.fr/v2/users?filter[login]=jdoe
Authorization: Bearer <access_token>
```
Réponse : tableau JSON ; **vide** = login inconnu.
---
## Fichiers à ajouter (étape 3)
```
lib/
├── services/
│ ├── intra_auth_service.dart # existant
│ └── intra_user_service.dart # GET user(s) avec token
├── screens/
│ ├── search_screen.dart # VUE 1
│ └── profile_screen.dart # VUE 2 (squelette → étape 4)
└── main.dart # home → SearchScreen si token OK
```
---
## `IntraUserService` (idée)
- Méthode `Future<Map<String, dynamic>> fetchUserByLogin(String login)`.
- Utilise `IntraAuthService.instance.authHeaders()`.
- **404 / liste vide** → exception métier `UserNotFoundException`.
- **Socket / timeout** → `NetworkException`.
- **401** → token expiré → message « reconnecte-toi ».
---
## Gestion des erreurs (sujet)
| Situation | Message utilisateur (exemple) |
|-----------|-------------------------------|
| Login inconnu | `Aucun utilisateur pour ce login` |
| Pas de réseau | `Connexion impossible. Vérifie le réseau.` |
| Non connecté OAuth | `Connecte-toi d’abord à l’API 42` |
| Champ vide | `Saisis un login` |
---
## Navigation
```text
SearchScreen --[login trouvé]--> ProfileScreen(login)
ProfileScreen --[retour]--> SearchScreen
```
`Navigator.push` / `pop` ou `go_router` — rester simple (Navigator 1.0 suffit).
---
## Critères « étape 3 terminée »
| # | Test |
|---|------|
| 1 | Login valide (ex. `fgras-ca`) → ouvre vue 2 |
| 2 | Login `zzzzzzzz` → message erreur |
| 3 | Mode avion → message réseau |
| 4 | Retour depuis vue 2 → nouvelle recherche possible |
| 5 | Un seul OAuth ; les recherches réutilisent le même token |
---
## Ensuite — étape 4
→ [etape-04-vue-profil.md](etape-04-vue-profil.md) : photo, ≥ 4 champs, skills, projets (dont failed), layout adaptatif.

View File

@@ -1,113 +0,0 @@
# Étape 4 — Vue 2 : profil complet
> **Prérequis :** [étape 3 — recherche](etape-03-vue-recherche.md) (navigation vers vue 2 avec un login valide).
## Objectif sujet
| Exigence | Cette étape |
|----------|-------------|
| ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet |
| Skills | Niveau entier + **pourcentage** (partie décimale du `level` API) |
| Projets | Statuts **`finished`** et **`failed`** |
| Erreurs | Réseau, 401, réponse API inattendue — message + réessai |
| Token | Un seul OAuth : `IntraAuthService` pour tous les `GET` |
| Retour vue 1 | `AppBar` + bouton retour système (`Navigator.pop`) |
| Layout | `LayoutBuilder` : colonne sur téléphone, 2 colonnes si largeur ≥ 720 px |
---
## Parcours utilisateur (vue 2)
1. Arrivée depuis la recherche avec un objet `user` minimal (`id`, `login`, parfois `image`).
2. **Chargement** : appels API en parallèle (spinner plein écran).
3. **Succès** : photo, carte « Informations », liste skills, liste projets.
4. **Échec** : message rouge + bouton **Réessayer**.
5. **Retour** : flèche AppBar → vue 1, nouvelle recherche possible.
---
## API 42 utilisée
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
| Donnée | Endpoint | Notes |
|--------|----------|--------|
| Fiche user | *(déjà renvoyée par la recherche étape 3)* | email, phone, location, wallet, image — pas de 2ᵉ `GET /users/{id}` pour limiter le quota |
| Fiche complète | `GET /v2/users/{login}` | inclut souvent `cursus_users` + `skills` par cursus |
| Niveau / skills | `cursus_users[].skills` | pas `/experiences` (souvent **403** avec token étudiant) |
| Projets | `GET /v2/users/{id}/projects_users?page[size]=100` | scope OAuth **`projects`** ; filtrer côté app : `finished` / `failed` |
Tous les appels :
```http
Authorization: Bearer <access_token>
Accept: application/json
```
**Confidentialité :** certains champs peuvent être masqués par l’API selon le profil consulté — afficher `—` si absent (sujet OAuth).
### Skills — niveau et pourcentage
L’API renvoie un `level` flottant (ex. `8.42`) :
- **Niveau affiché** : partie entière (`8`)
- **Pourcentage** : partie décimale × 100 (`42 %`)
---
## Fichiers (étape 4)
```
lib/
├── services/
│ ├── intra_auth_service.dart # existant — headers Bearer
│ ├── intra_user_service.dart # existant — recherche
│ └── intra_profile_service.dart # NEW — agrégation profil
└── screens/
└── profile_screen.dart # StatefulWidget — chargement + UI
```
### `IntraProfileService`
- `fetchProfile(Map<String, dynamic> summaryUser)` → `IntraUserProfile`
- Requêtes **séquentielles** (pause ~450 ms) + retry sur 429 (`intra_http.dart`)
- Cache profil 2 min par `user_id` pour éviter de re-taper l’API au retour arrière
- Mêmes exceptions que l’étape 3 : `IntraNetworkException`, `IntraUnauthorizedException`, etc.
### `ProfileScreen`
- Reçoit le `user` de l’étape 3
- `initState` → `_load()`
- Pas de second OAuth
---
## Gestion des erreurs (vue 2)
| Situation | Comportement |
|-----------|----------------|
| Pas de réseau | Message + **Réessayer** |
| Token expiré (401) | « Session expirée… » — reconnecter depuis vue 1 |
| Trop de requêtes (429) | Message dédié ; retry automatique côté client + attendre avant de réessayer |
| Accès refusé (403) | Scopes `public` + `projects` sur l’app OAuth, puis **reconnexion** dans l’app |
| ID manquant | Erreur métier explicite |
| Listes vides skills / projets | Texte informatif, pas de crash |
---
## Critères « étape 4 terminée »
| # | Test |
|---|------|
| 1 | Login valide → photo + au moins 4 champs remplis ou `—` |
| 2 | Section skills avec niveau **et** pourcentage |
| 3 | Au moins un projet `finished` ou `failed` visible (si l’étudiant en a) |
| 4 | Mode avion sur vue 2 → erreur + réessai |
| 5 | Retour → vue 1 sans perdre la session OAuth |
| 6 | Largeur tablette / fenêtre large → mise en page 2 colonnes |
---
## Ensuite — étape 5
→ Affiner navigation, contraintes et polish responsive sur **les deux vues** (`etape-05-navigation-layout.md` à rédiger).

View File

@@ -1,74 +0,0 @@
# Étape 5 — Navigation et layout adaptatif
> **Prérequis :** [étape 4 — profil](etape-04-vue-profil.md) validée sur téléphone (OAuth `public` + `projects`).
## Objectif sujet
| Critère | Cette étape |
|---------|-------------|
| Retour vue 1 | `Navigator.push` / `pop` — flèche AppBar sur la vue 2 |
| Layout flexible | Scroll, marges, largeur max, 2 colonnes profil si écran large |
| Lisibilité | Rotation, clavier ouvert, petits écrans sans overflow |
---
## Navigation
```text
SearchScreen --push--> ProfileScreen
ProfileScreen --pop--> SearchScreen (session OAuth conservée)
```
- Pas de `go_router` obligatoire : **Navigator 1.0** suffit.
- La vue 1 garde le token ; une nouvelle recherche ne refait pas OAuth.
---
## Fichiers ajoutés / modifiés
```
lib/
├── theme/
│ └── swifty_ui.dart # couleur 42, padding, AppBar, scroll responsive
├── screens/
│ ├── search_screen.dart # formulaire scrollable, centré
│ └── profile_screen.dart # lignes détail adaptatives, UI partagée
└── main.dart # thème Material 3 (seed 42)
```
### `swifty_ui.dart`
| Widget / helper | Rôle |
|-----------------|------|
| `SwiftyUi.appBar` | AppBar turquoise identique sur les 2 vues |
| `ResponsiveScrollBody` | `SingleChildScrollView` + padding + `maxWidth` |
| `ErrorMessage` | Texte d’erreur sélectionnable, centré |
| `LoadingFilledButton` | Bouton avec spinner (connexion / recherche) |
### Vue recherche
- Contenu dans `ResponsiveScrollBody` (`maxWidth` ~480 px).
- Évite l’overflow quand le **clavier** est ouvert.
### Vue profil
- Breakpoint **720 px** : 1 colonne (téléphone) / 2 colonnes (tablette, paysage).
- `_DetailRow` : label + valeur en **colonne** si la ligne est trop étroite (< 400 px).
---
## Critères « étape 5 terminée »
| # | Test |
|---|------|
| 1 | Profil → retour → recherche sans déconnexion |
| 2 | Rotation paysage : pas de bande jaune / overflow |
| 3 | Clavier ouvert sur la recherche : boutons accessibles (scroll) |
| 4 | Tablette ou fenêtre large : profil en 2 colonnes lisible |
| 5 | Erreurs toujours visibles (couleur + message lisible) |
---
## Ensuite — étape 6
→ [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) : `flutter test`, démo peer-eval, préparation dépôt école.

View File

@@ -1,118 +0,0 @@
# Étape 6 — Tests, soutenance et rendu école
> **Prérequis :** étapes 2–5 OK sur device (OAuth `public` + `projects`, recherche, profil, retour, layout).
## Objectif
1. Valider la **partie obligatoire** du sujet avant peer-eval.
2. Préparer le **dépôt Vogosphere** (sans `docs/`, sans `.env`).
3. Savoir **quoi montrer** à l’évaluateur et dans quel ordre.
Référence correcteurs : [swiftyCompanion/README.md](../../swiftyCompanion/README.md).
---
## Commandes avant soutenance
Depuis le package Flutter :
```bash
cd ~/projets/42School/swifty_companion/swiftyCompanion/swifty_companion
flutter pub get
flutter analyze
flutter test
flutter doctor
```
Sur téléphone (USB ou Wi‑Fi ADB) :
```bash
flutter devices
flutter run -d <device>
```
Après changement de `.env` ou de scopes OAuth : **arrêter** `flutter run` et relancer ; **reconnexion** 42 dans l’app.
---
## Checklist peer-eval (détaillée)
| # | Test | Comment le montrer | Où c’est géré dans l’app |
|---|------|--------------------|---------------------------|
| 1 | `flutter test` | Terminal sur la machine de dev | `test/widget_test.dart` |
| 2 | Parcours nominal | Connexion → login valide → profil | `SearchScreen` → `ProfileScreen` |
| 3 | Profil complet | Photo, ≥ 4 champs, skills (niveau + %), projets validés **et** failed | `IntraProfileService` + `profile_screen.dart` |
| 4 | Login invalide | `zzzzinvalid` → message, pas de crash | `IntraUserNotFoundException` |
| 5 | Hors ligne | Mode avion sur recherche **ou** sur profil (erreur + réessai) | `IntraNetworkException`, retry profil |
| 6 | Navigation | Retour AppBar → nouvelle recherche, session conservée | `Navigator.pop` |
| 7 | Petit écran / rotation | Paysage, clavier ouvert sur la recherche | `ResponsiveScrollBody`, `swifty_ui.dart` |
| 8 | Bonus | Access invalide → refresh → recherche OK | [étape 7](etape-07-bonus-token.md) |
**Ordre de démo suggéré :** connexion → profil riche (skills + failed) → login faux → mode avion → retour → `flutter test`.
---
## Correspondance sujet v4 ↔ app
| Critère sujet | Statut attendu |
|---------------|----------------|
| 2 vues | `SearchScreen` + `ProfileScreen` |
| Erreurs | Login vide, inconnu, réseau, 401, 403, 429 |
| Token unique | `IntraAuthService.instance` partout |
| OAuth Intra | `flutter_appauth` + `.env` |
| Layout adaptatif | Étape 5 (`wideBreakpoint` 720 px) |
Champs masqués par l’API (`—`) : normal (confidentialité sujet).
---
## Rendu Git école
### Ce qui part sur Vogosphere
Racine du dépôt Intra = contenu de **`swiftyCompanion/`** :
- `README.md`, `.gitignore`, `.env.example`
- `swifty_companion/` (code Flutter)
### Ce qui ne part **pas**
- `docs/` (dépôt perso uniquement)
- `.env` (secrets)
- Fichiers de backup type `.env copy`
### Vérifier avant push
```bash
cd ~/projets/42School/swifty_companion/swiftyCompanion
git status # si repo école initialisé ici
```
- Aucun `.env` listé
- `REDIRECT_URI` dans `.env.example` = URI enregistrée sur l’Intra
- README école à jour pour les correcteurs
Workflow détaillé : [README racine](../../README.md) (section « Pousser vers l’école »).
---
## Parler en soutenance (30 s)
> « Swifty Companion : OAuth2 Intra une fois, puis recherche par login. Le profil charge cursus, skills et projects_users avec le même token. Erreurs réseau et API gérées ; layout scrollable et deux colonnes sur grand écran. Secrets dans `.env` local, non versionné. »
---
## Critères « étape 6 terminée »
| # | Fait ? |
|---|--------|
| 1 | `flutter analyze` + `flutter test` verts |
| 2 | Checklist peer-eval cochée sur device |
| 3 | `.env` absent du futur commit école |
| 4 | Tu sais lancer la démo sans notes |
---
## Ensuite — étape 7 (bonus)
→ [etape-07-bonus-token.md](etape-07-bonus-token.md) : refresh token à l’expiration (optionnel si obligatoire parfaite).

View File

@@ -1,93 +0,0 @@
# Étape 7 — Bonus : refresh token
> **Prérequis :** partie obligatoire validée ([étape 6](etape-06-checklist-soutenance.md)).
## Objectif sujet
Quand l’**access token** expire, l’app **rafraîchit** la session via le **refresh token** et continue à fonctionner **sans** relancer l’écran OAuth à chaque requête.
---
## Implémentation (ce dépôt)
| Élément | Fichier |
|---------|---------|
| Stockage `access` + `refresh` après login | `intra_auth_service.dart` |
| `POST /oauth/token` `grant_type=refresh_token` | `IntraAuthService.refreshAccessToken()` |
| Un seul refresh en parallèle | `_refreshInFlight` (évite 4 refresh si 4 GET en 401) |
| Sur **401** API → refresh → **rejouer** le GET | `intra_http.dart` → `intraApiGet(..., auth:)` |
| Parsing JSON token | `intra_oauth_tokens.dart` |
Les scopes OAuth restent `public` + `projects` à la connexion initiale.
---
## Flux
```text
signIn() → access + refresh en mémoire
GET /v2/users/... → 401
→ refreshAccessToken() → nouveaux tokens
→ même GET avec nouveau Bearer
sinon → IntraUnauthorizedException (« Reconnecte-toi »)
```
`signOut()` efface les deux tokens.
**Persistance** (secure storage) : non fait — le bonus du sujet vise surtout l’expiration **pendant** une session ; tu peux l’ajouter plus tard.
---
## Tests automatisés
```bash
cd swiftyCompanion/swifty_companion
flutter test
```
- `test/intra_oauth_tokens_test.dart` — parsing réponse `/oauth/token`
- `test/widget_test.dart` — démarrage app
---
## Test manuel (soutenance bonus)
1. Connexion 42 → recherche OK.
2. **Simuler** un access token expiré :
- en debug, appeler une recherche après avoir invalidé le token en mémoire **sans** effacer le refresh (voir ci‑dessous), **ou**
- attendre l’expiration réelle (souvent long).
3. Nouvelle recherche → refresh silencieux → profil OK **sans** repasser par le navigateur OAuth.
### Test sur device (build debug)
Sur l’écran recherche, bouton **« DEBUG : simuler token expiré »** (`kDebugMode` uniquement — absent en release).
1. Connexion 42 OK.
2. Appuyer sur le bouton debug.
3. **Rechercher** un login → refresh silencieux → profil OK sans repasser par OAuth.
En release / soutenance APK release : expliquer le flux ou montrer `flutter test`.
---
## Critères « bonus OK »
| # | Test |
|---|------|
| 1 | 401 sur API → refresh → requête réussie |
| 2 | Refresh impossible → message « session expirée », pas de crash |
| 3 | `flutter test` vert |
---
## Fichiers modifiés (récap)
```
lib/services/
├── intra_auth_service.dart # refresh + stockage refresh_token
├── intra_http.dart # 401 → refresh → retry
├── intra_oauth_tokens.dart # parse JSON token
└── … (user / profile passent auth à intraApiGet)
test/intra_oauth_tokens_test.dart
```

View File

@@ -1,289 +0,0 @@
# Guide de correction / soutenance — ce qui a été réalisé
> **Objectif :** suivre le [sujet Swifty Companion v4](../sujets/swifty-companion.md) point par point, avec **fichiers**, **comportement** et **démo** à ne pas oublier en peer-eval.
> **Code évalué :** dépôt école = dossier [`swiftyCompanion/`](../../swiftyCompanion/) (pas `docs/`).
---
## 1. Vue d’ensemble
| Élément | Détail |
|---------|--------|
| Framework | **Flutter** (Dart), Material 3 |
| API | **42 Intra v2** — `https://api.intra.42.fr/v2` |
| Auth | **OAuth2** — `flutter_appauth` 12.x |
| Secrets | **`.env`** dans `swiftyCompanion/swifty_companion/.env` (asset, non versionné) |
| Vues | **2** — recherche (`SearchScreen`) + profil (`ProfileScreen`) |
| Bonus | **Refresh token** sur 401 + retry des requêtes API |
**Point d’entrée :** [`swiftyCompanion/swifty_companion/lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) → `home: SearchScreen()`.
---
## 2. Chapitre IV — Instructions générales & sécurité
| Exigence sujet | Réalisation | Fichiers |
|----------------|-------------|----------|
| Langage mobile (Flutter cité) | Projet Flutter standard | `swifty_companion/pubspec.yaml` |
| Dernière API 42 | Base URL v2 | [`lib/config/intra_config.dart`](../../swiftyCompanion/swifty_companion/lib/config/intra_config.dart) — `apiBaseUrl` |
| Secrets en **`.env`**, **pas dans Git** | `CLIENT_ID`, `CLIENT_SECRET`, `REDIRECT_URI` | `.env` (local) · modèle [`swiftyCompanion/.env.example`](../../swiftyCompanion/.env.example) · ignore [`swiftyCompanion/.gitignore`](../../swiftyCompanion/.gitignore) et [`.gitignore` racine travail](../../.gitignore) |
| Chargement `.env` au démarrage | `dotenv.load` | [`lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) + asset dans `pubspec.yaml` (`assets: - .env`) |
**À dire en correction :** les clés OAuth ne sont jamais dans le code source ; l’évaluateur configure son propre `.env` à partir de `.env.example`.
---
## 3. Chapitre V.1 — Partie obligatoire (9 critères)
### Critère 1 — Au moins 2 vues
| Vue | Rôle | Fichier |
|-----|------|---------|
| **Vue 1** | Connexion API + recherche par login | [`lib/screens/search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) |
| **Vue 2** | Profil étudiant | [`lib/screens/profile_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart) |
**Démo :** montrer explicitement les deux écrans après une recherche réussie.
---
### Critère 2 — Gérer tous les cas d’erreur
| Situation | Message utilisateur (ex.) | Où c’est géré |
|-----------|---------------------------|---------------|
| Pas connecté OAuth | « Connecte-toi d’abord à l’API 42 » | [`intra_api_exception.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart) — `IntraNotAuthenticatedException` · [`intra_user_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart) |
| Login vide | « Saisis un login » | `IntraUserService.fetchUserByLogin` |
| Login inconnu | « Aucun utilisateur pour ce login » | `IntraUserNotFoundException` |
| Pas de réseau | « Connexion impossible. Vérifie le réseau. » | `IntraNetworkException` · `SocketException` / `ClientException` dans user + profile services |
| OAuth annulé / échec | Messages OAuth / plateforme | [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_connect()` |
| Session expirée (401) | « Session expirée. Reconnecte-toi… » | `IntraUnauthorizedException` · [`intra_http.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_http.dart) |
| Accès refusé (403) | Scopes `public` + `projects` | `IntraForbiddenException` |
| Trop de requêtes (429) | Message + attente / retry | `IntraRateLimitException` · retry dans `intra_http.dart` |
| Erreur profil (réseau, API) | Message + bouton **Réessayer** | `profile_screen.dart` — `_ErrorBody` |
| `.env` manquant | Texte explicite au lancement | `search_screen.dart` — branche `!IntraConfig.isConfigured` |
**Démo suggérée :** login `zzzzinvalid` → mode avion → (optionnel) profil en erreur + réessai.
---
### Critère 3 — Vue 2 : infos du login s’il existe
- Navigation **uniquement** si l’API renvoie un utilisateur : [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_search()` → `Navigator.push(ProfileScreen(user: …))`.
- Si liste vide → exception, **pas** d’ouverture du profil.
---
### Critère 4 — ≥ 4 détails + photo de profil
| Donnée affichée | Source API | Fichier UI |
|-----------------|------------|------------|
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
| **Photo** | `image.link` ou `image_url` | `CircleAvatar` + `NetworkImage` — `_ProfileHeader` |
| **Email** | `user['email']` | `_DetailsCard` |
| **Téléphone** | `user['phone']` | `_DetailsCard` |
| **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` |
| **Localisation** | `user['location']` | `_DetailsCard` |
| **Wallet** | `user['wallet']` | `_DetailsCard` |
Chargement profil : [`lib/services/intra_profile_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart) — `GET /users/{login}`, puis `projects_users`.
**Note sujet :** champs masqués par l’API → affichage `—` (confidentialité OAuth).
**Non affiché (optionnel sujet) :** `evaluation_points` — tu as déjà **> 4** champs sans lui.
---
### Critère 5 — Skills : niveau + pourcentage
- **Pas** `/experiences` (souvent 403).
- Skills depuis **`cursus_users[].skills`** sur la fiche user.
- Niveau entier = partie entière du `level` API ; **%** = partie décimale × 100.
- Parsing : `IntraProfileService._parseSkillsFromCursus` · affichage : `_SkillsCard` + barre de progression.
**Démo :** lire une ligne du type `Algorithms — niveau 8 (42 %)`.
---
### Critère 6 — Projets réalisés, y compris failed
- `GET /users/{id}/projects_users?page[size]=100`
- Filtre app : statuts **`finished`** et **`failed`** — `_parseProjects` dans `intra_profile_service.dart`.
- Liste : `_ProjectsCard` (libellés Validé / Échec + note si présente).
**Scope OAuth requis :** `projects` (sinon 403).
---
### Critère 7 — Revenir à la vue 1
- `MaterialPageRoute` + **retour système** / flèche AppBar sur le profil.
- `ProfileScreen` : `SwiftyUi.appBar(..., showBack: true)`.
- Session OAuth **conservée** sur `SearchScreen` (pas de nouveau login).
**Démo :** retour → nouvelle recherche sans « Se connecter avec 42 ».
---
### Critère 8 — Layout flexible / moderne
| Mécanisme | Fichier |
|-----------|---------|
| Scroll + clavier | `ResponsiveScrollBody` — [`lib/theme/swifty_ui.dart`](../../swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart) |
| Formulaire centré (vue 1) | `searchMaxWidth` 480 px |
| Profil 2 colonnes si large | `wideBreakpoint` 720 px — `profile_screen.dart` `LayoutBuilder` |
| Lignes détail empilées si étroit | `_DetailRow` si largeur < 400 px |
| Thème 42 turquoise | `SwiftyUi.brand` · `main.dart` `ColorScheme.fromSeed` |
**Démo :** rotation paysage ou grand écran → 2 colonnes sur le profil.
---
### Critère 9 — Un token OAuth, pas un token par requête
| Règle | Implémentation |
|-------|----------------|
| Un seul login OAuth | `IntraAuthService.instance` — [`intra_auth_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart) |
| Bearer sur chaque GET | `authHeaders()` |
| Pas de OAuth à chaque recherche | `IntraUserService` / `IntraProfileService` réutilisent le même service |
**À dire :** un flux `authorizeAndExchangeCode` au début ; ensuite uniquement des `GET` API avec le même access token.
---
## 4. OAuth2 Intra (encadrés sujet)
| Élément | Valeur / choix | Fichiers |
|---------|----------------|----------|
| Authorize | `https://api.intra.42.fr/oauth/authorize` | `intra_config.dart` |
| Token | `https://api.intra.42.fr/oauth/token` | `intra_config.dart` + refresh dans `intra_auth_service.dart` |
| Scopes | `public`, `projects` | `intra_auth_service.dart` — `AuthorizationTokenRequest` |
| Redirect (device) | `com.example.swiftycompanion://callback` | `.env` · [`intra-oauth-formulaire.md`](intra-oauth-formulaire.md) |
| Android redirect | `RedirectUriReceiverActivity` | [`android/app/src/main/AndroidManifest.xml`](../../swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml) |
| Gradle scheme | `appAuthRedirectScheme` | [`android/app/build.gradle.kts`](../../swiftyCompanion/swifty_companion/android/app/build.gradle.kts) |
**Doc perso (non rendue Intra) :** [etape-02-oauth-intra.md](etape-02-oauth-intra.md), [oauth-test-manuel.md](oauth-test-manuel.md).
---
## 5. Chapitre V.2 — Bonus (refresh token)
| Exigence | Réalisation | Fichiers |
|----------|-------------|----------|
| Stocker refresh au login | `result.refreshToken` | `intra_auth_service.dart` — `signIn()` |
| Rafraîchir à expiration | `POST` `grant_type=refresh_token` | `refreshAccessToken()` |
| App continue | Sur **401** → refresh → **rejouer** le GET | `intra_http.dart` — `intraApiGet(..., auth:)` |
| Parsing JSON token | | `intra_oauth_tokens.dart` + tests |
| Test manuel debug | Bouton « DEBUG : simuler token expiré » | `search_screen.dart` (`kDebugMode` uniquement) puis **Rechercher** |
**Limite assumée :** tokens en **mémoire** seulement — après kill de l’app, reconnexion OAuth (acceptable si tu l’expliques).
**Guide détaillé :** [etape-07-bonus-token.md](etape-07-bonus-token.md).
---
## 6. Inventaire des fichiers applicatifs (lib/)
Fichiers **métier** à connaître pour la soutenance :
```
swiftyCompanion/swifty_companion/lib/
├── main.dart # App + thème + dotenv
├── config/
│ └── intra_config.dart # URLs + variables .env
├── services/
│ ├── intra_auth_service.dart # OAuth, access + refresh
│ ├── intra_user_service.dart # Recherche login
│ ├── intra_profile_service.dart # Profil agrégé
│ ├── intra_http.dart # GET + 401/429
│ ├── intra_oauth_tokens.dart # Parse token JSON (bonus)
│ └── intra_api_exception.dart # Erreurs métier
├── screens/
│ ├── search_screen.dart # Vue 1
│ └── profile_screen.dart # Vue 2
└── theme/
└── swifty_ui.dart # Layout partagé (étape 5)
```
**Tests :**
```
test/
├── widget_test.dart # Démarre app + bouton connexion
└── intra_oauth_tokens_test.dart # Parse refresh (bonus)
```
**Fichier supprimé (nettoyage) :** `auth_screen.dart` (écran test obsolète, non utilisé).
**Plateforme Android (OAuth physique) :** `AndroidManifest.xml`, `build.gradle.kts` (voir §4).
---
## 7. Endpoints API utilisés
| Usage | Méthode | Appelé depuis |
|-------|---------|---------------|
| Session courante | `GET /v2/me` | `IntraAuthService.fetchMe` (après login) |
| Recherche | `GET /v2/users?filter[login]=` | `IntraUserService` |
| Fiche complète | `GET /v2/users/{login}` | `IntraProfileService` |
| Cursus (secours) | `GET /v2/users/{id}/cursus_users` | `IntraProfileService` si pas embarqué |
| Projets | `GET /v2/users/{id}/projects_users` | `IntraProfileService` |
| Refresh token | `POST /oauth/token` | `IntraAuthService.refreshAccessToken` |
Requêtes **séquentielles** + cache profil 2 min + retry 429 : voir [etape-04-vue-profil.md](etape-04-vue-profil.md).
---
## 8. Chapitre VI — Rendu Git
| À pousser sur Vogosphere | À ne **pas** pousser |
|--------------------------|----------------------|
| Contenu de `swiftyCompanion/` | Dossier `docs/` (dépôt perso) |
| `README.md`, `.gitignore`, `.env.example` | Fichier `.env` |
| `swifty_companion/` (code Flutter) | `.env copy`, secrets dans l’historique Git |
Checklist : [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) · [swiftyCompanion/README.md](../../swiftyCompanion/README.md).
---
## 9. Script de démo (5–8 min)
1. `cd swiftyCompanion/swifty_companion` → `flutter test` (rapide).
2. `flutter run -d <device>` — montrer `.env` configuré **sans** afficher les secrets.
3. **Se connecter avec 42** — une fois.
4. Recherche login valide → **profil** : photo, champs, skills, projet failed si dispo.
5. Retour → **deuxième** recherche (même session).
6. Login invalide → message.
7. Mode avion → erreur réseau.
8. **Bonus :** DEBUG simuler token expiré → **Rechercher** → OK sans OAuth navigateur.
9. Rappeler : 2 vues, 1 token, `.env` ignoré par git.
---
## 10. Parcours pédagogique (docs perso uniquement)
| Étape | Guide |
|-------|--------|
| 0–1 | [00-reprise-et-etat.md](00-reprise-et-etat.md) |
| 2 OAuth | [etape-02-oauth-intra.md](etape-02-oauth-intra.md) |
| 3 Recherche | [etape-03-vue-recherche.md](etape-03-vue-recherche.md) |
| 4 Profil | [etape-04-vue-profil.md](etape-04-vue-profil.md) |
| 5 Layout | [etape-05-navigation-layout.md](etape-05-navigation-layout.md) |
| 6 Soutenance | [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) |
| 7 Bonus | [etape-07-bonus-token.md](etape-07-bonus-token.md) |
| **Ce document** | **Cartographie sujet ↔ code pour la correction** |
---
## 11. Checklist « je n’ai rien oublié » (avant peer-eval)
- [ ] App OAuth Intra : scopes **public** + **projects**, redirect = `.env`
- [ ] `.env` présent sur la machine de démo, **absent** du Git école
- [ ] `flutter analyze` / `flutter test` OK
- [ ] Parcours nominal + erreurs + retour testés sur **device**
- [ ] Bonus refresh testé (DEBUG → Rechercher)
- [ ] Prêt à expliquer **où** est chaque critère du sujet (ce guide §3)
- [ ] SECRET régénéré si jamais exposé (chat, commit accidentel)
- [ ] Fichier **`.env copy`** retiré du Git — voir [audit-conformite-usage-api.md](audit-conformite-usage-api.md)
---
*Dernière alignement : sujet PDF v4 — traduction [swifty-companion.md](../sujets/swifty-companion.md).*

View File

@@ -1,59 +0,0 @@
# Formulaire OAuth Intra — valeurs pour Swifty Companion
À copier-coller sur [Nouvelle application API](https://profile.intra.42.fr/oauth/applications/new) (ou édition).
## Paramètres obligatoires
| Champ (FR) | Valeur proposée |
|------------|-----------------|
| **Nom** * | `Swifty Companion` |
| **Image** | `docs/assets/swifty_companion_oauth_icon_256.png` ou `swifty_companion_oauth_icon_256.jpg` (vrai PNG/JPEG, 256×256). Ne pas uploader l’ancien fichier mal nommé (`.png` qui était en réalité du JPEG). |
| **Description** * | `Projet pédagogique Mobile Initiation : application Flutter pour afficher le profil d’un étudiant 42 via l’API Intra.` |
| **Type d’application** * | **`42 pedagogic project`** (choix correct ; il n’y a pas de type « mobile ») |
| **Site web** | `https://www.42.fr` ou l’URL de ton dépôt GitHub / Git perso (placeholder accepté) |
| **Publique** | **Décoché** (app perso de projet) |
## URI de redirection
**Téléphone physique (recommandé Swifty)** — une ligne :
```text
com.example.swiftycompanion://callback
```
(Sans `_` dans le schéma — souvent accepté par l’Intra ; sinon voir [oauth-test-manuel.md](oauth-test-manuel.md).)
**Émulateur seulement** (optionnel) :
```text
http://localhost:8080/callback
```
**À ne pas mettre :**
- `com.example.swifty_companion://callback`
- lignes vides ou plusieurs URI mélangées
- espaces avant/après
## Champs d’application (scopes)
Cocher :
- [x] **Accéder aux données publiques de l'utilisateur** (`public`)
- [x] **Accéder aux données des projets de l'utilisateur** (`projects`) — requis pour `projects_users` (sinon **403** sur le profil)
Laisser **décoché** le reste sauf besoin explicite (équipes, médias, forum, etc.).
Après ajout du scope `projects`, **déconnexion puis nouvelle connexion** dans l’app pour obtenir un token à jour.
## Après création
Sur la page de l’application :
| Intra | `.env` |
|-------|--------|
| **UID** | `CLIENT_ID` |
| **SECRET** | `CLIENT_SECRET` |
| URI enregistrée | `REDIRECT_URI` (caractère pour caractère identique) |
Fichier : `swiftyCompanion/.env` (ne pas committer).

View File

@@ -1,78 +0,0 @@
# OAuth 42 — tests manuels des URLs
## Pourquoi pas `http://localhost` sur téléphone ?
| Environnement | `http://localhost:8080/callback` |
|---------------|----------------------------------|
| Émulateur Android | Parfois OK (localhost = l’émulateur) |
| **Téléphone physique** | ❌ « Site inaccessible » — localhost = le téléphone, pas ton PC |
**Swifty Companion (mobile réel)** : utiliser un **schéma d’app** :
```text
com.example.swiftycompanion://callback
```
À enregistrer sur l’Intra **et** dans `.env` (`REDIRECT_URI`).
---
## Endpoints 42 (référence sujet)
| Rôle | URL |
|------|-----|
| Autorisation | `https://api.intra.42.fr/oauth/authorize` |
| Token | `https://api.intra.42.fr/oauth/token` |
| API (ex. moi) | `https://api.intra.42.fr/v2/me` |
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
---
## 1. Tester l’API (sans app)
Dans un navigateur (connecté à l’Intra ou non pour cette étape) :
```text
https://api.intra.42.fr/
```
Réponse JSON attendue (message API) → l’API est joignable.
---
## 2. Tester la page d’autorisation (navigateur)
Remplace `TON_CLIENT_ID` par l’**UID** de ton app OAuth.
**Redirect actuel (mobile)** :
```text
https://api.intra.42.fr/oauth/authorize?client_id=TON_CLIENT_ID&redirect_uri=com.example.swiftycompanion%3A%2F%2Fcallback&response_type=code&scope=public
```
| Résultat | Signification |
|----------|----------------|
| Page login / autorisation 42 | ✅ URL d’auth OK |
| Erreur `redirect_uri` | ❌ URI pas enregistrée sur l’Intra ou différente de `.env` |
| Erreur `invalid_client` | ❌ `client_id` incorrect |
Après validation, le navigateur tente d’ouvrir `com.example.swiftycompanion://callback?code=...` → sur PC ça échoue (normal) ; **sur le téléphone avec l’app installée**, l’app doit s’ouvrir.
---
## 3. Alignement obligatoire (3 endroits identiques)
1. [Application OAuth Intra](https://profile.intra.42.fr/oauth/applications) → **URI de redirection**
2. `swifty_companion/.env` → `REDIRECT_URI=...`
3. Code Flutter → même valeur via `IntraConfig.redirectUri`
---
## 4. Ancien redirect (à retirer sur l’Intra si présent)
```text
http://localhost:8080/callback
```
Remplacer par `com.example.swiftycompanion://callback` (une ligne dans le formulaire).

View File

@@ -1,7 +0,0 @@
# Sujets — Swifty Companion
| Document | Description |
|----------|-------------|
| [swifty-companion.md](swifty-companion.md) | Traduction FR complète (v4) + interprétation des visuels |
| [originaux/swifty_companion_subject.pdf](originaux/swifty_companion_subject.pdf) | PDF officiel Intra |
| [captures/](captures/) | Images extraites du PDF (couverture) |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 870 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

View File

@@ -1,164 +0,0 @@
# Mobile Initiation — Swifty Companion
> **Version :** 4
> **Résumé :** Ce projet vise à vous initier au développement d’applications mobiles.
> **Traduction française** — source : [swifty_companion_subject.pdf](originaux/swifty_companion_subject.pdf)
---
## Table des matières
1. [Chapitre I — Avant-propos](#chapitre-i--avant-propos)
2. [Chapitre II — Introduction](#chapitre-ii--introduction)
3. [Chapitre III — Objectifs](#chapitre-iii--objectifs)
4. [Chapitre IV — Instructions générales](#chapitre-iv--instructions-générales)
5. [Chapitre V — Le projet](#chapitre-v--le-projet)
- [V.1 Partie obligatoire](#v1-partie-obligatoire)
- [V.2 Bonus](#v2-bonus)
6. [Chapitre VI — Rendu et évaluation par les pairs](#chapitre-vi--rendu-et-évaluation-par-les-pairs)
7. [Éléments visuels du PDF](#éléments-visuels-du-pdf)
---
## Page de garde (PDF page 1)
| Champ | Texte officiel (EN) | Traduction |
|-------|---------------------|------------|
| Série | Mobile Initiation | Initiation mobile |
| Titre | Swifty Companion | Swifty Companion |
| Résumé | This project aims to introduce you to the development of mobile application. | Ce projet vise à vous initier au développement d’applications mobiles. |
| Version | 4 | 4 |
### Référence visuelle — couverture
![Page de garde du sujet](captures/page-01-couverture.png)
| Zone | Description |
|------|-------------|
| Illustration centrale | Motif vertical en facettes (style low-poly), tons gris/noir — **élément graphique 42**, pas une maquette d’application |
| Fond | Motif géométrique léger (triangles) sur toute la page |
| Texte | « Mobile Initiation » puis « Swifty Companion », résumé en italique, « Version: 4 » |
Extrait haute résolution de l’illustration : [illustration-motif-42.png](captures/illustration-motif-42.png)
> **Note :** le sujet PDF (9 pages) **ne contient pas de captures d’écran de l’app à reproduire**. Les pages 2 à 9 reprennent le même fond géométrique que la couverture, avec le texte des chapitres. L’UI de votre application est définie **uniquement par la liste de critères** de la partie obligatoire.
---
## Chapitre I — Avant-propos
*(Traduction du texte Wikipedia sur le dodo — page 2 du PDF.)*
Voici ce que Wikipedia dit du **dodo** :
Le dodo (*Raphus cucullatus*) est un oiseau incapable de voler, aujourd’hui éteint, endémique de l’île Maurice, à l’est de Madagascar dans l’océan Indien. Son plus proche parent génétique était le solitaire de Rodrigues, également éteint ; les deux formaient la sous-famille des Raphinae, au sein de la famille des columbidés. Le plus proche parent encore vivant du dodo est le pigeon de Nicobar. On a un temps cru à tort qu’un dodo blanc existait sur l’île voisine de La Réunion.
Les restes subfossiles montrent que le dodo mesurait environ 1 mètre et pouvait peser 10,6 à 21,1 kg à l’état sauvage. Son apparence réelle n’est connue que par des dessins, peintures et récits du XVIIe siècle. Ces sources varient beaucoup et peu d’illustrations proviennent de spécimens vivants : l’apparence exacte reste débattue. On le représente souvent avec un plumage gris-brun, des pattes jaunes, une touffe de plumes à la queue, une tête nue grise et un bec noir, jaune et vert. Il utilisait des gastrolithes pour digérer, mangeait probablement des fruits, et vivait dans les bois des zones côtières plus sèches de Maurice. Une source mentionne un seul œuf par couvée. Il est supposé qu’il a perdu le vol face à l’abondance de nourriture et au peu de prédateurs.
Les Hollandais le mentionnent pour la première fois en 1598. Chasse, animaux domestiques et espèces invasives ont contribué à sa disparition ; la dernière observation largement acceptée date de 1662. Son extinction ne fut pas tout de suite remarquée ; certains le considéraient comme mythique. Au XIXe siècle, des recherches portèrent sur quelques spécimens rapportés en Europe au début du XVIIe siècle, dont une tête séchée — seul tissu mou encore conservé. Depuis, beaucoup de matériel subfossile a été extrait de Maurice (notamment du marais Mare aux Songes). La rapidité de l’extinction a mis en lumière le rôle de l’homme dans la disparition d’espèces entières. Le dodo est célèbre via *Alice au pays des merveilles* et est devenu un symbole d’extinction et d’obsolescence, ainsi qu’une mascotte de Maurice.
---
## Chapitre II — Introduction
Dans ce projet, vous découvrirez comment développer une **application mobile**.
L’objectif est de construire une application qui récupère les **informations des étudiants de 42** via l’**API 42**.
Une **API** (*Application Programming Interface*) est un ensemble de routines, protocoles et outils pour construire des applications. Souvent, une API se présente comme une bibliothèque avec des spécifications pour routines, structures de données, classes et variables. Dans d’autres cas — notamment les services **SOAP** et **REST** — une API est simplement une spécification d’appels distants exposés aux consommateurs.
---
## Chapitre III — Objectifs
Ce projet vise à vous familiariser avec :
- un **langage mobile** (Dart, Swift, Kotlin, Java, etc.) ;
- un **IDE** de votre choix (Android Studio est un bon candidat) ;
- des **frameworks mobiles** (le sujet cite **Flutter**) ;
- l’**API 42**.
---
## Chapitre IV — Instructions générales
- Le projet est évalué **uniquement par des humains** (soutenance).
- Vous pouvez utiliser **n’importe quel langage mobile**, en gardant à l’esprit la **dernière version de l’IDE** disponible sur les machines de l’école.
- Frameworks et bibliothèques compatibles autorisés si leur usage est **justifié** en soutenance — soyez pertinent.
- Le projet doit utiliser la **dernière version disponible de l’API 42**.
- Vos choix techniques peuvent vous rendre plus attractif sur le marché du travail.
### ⚠️ Sécurité — encadré page 5
> Pour des raisons de sécurité évidentes, 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.
---
## Chapitre V — Le projet
### V.1 Partie obligatoire
Pour valider la partie obligatoire, vous devez respecter **tous** les critères suivants :
| # | Critère (EN) | Exigence en français |
|---|--------------|----------------------|
| 1 | At least 2 views | Au moins **2 vues** (écrans) |
| 2 | Handle all cases of errors | Gérer **tous** les cas d’erreur (login introuvable, erreur réseau, etc.) |
| 3 | Second view shows login info | La **2ᵉ vue** affiche les infos du login **s’il existe** |
| 4 | Four details + profile picture | Afficher **au moins 4 détails** parmi : login, email, mobile, level, location, wallet, evaluations, etc. **+ photo de profil** |
| 5 | Skills with level and percentage | Afficher les **compétences (skills)** avec **niveau** et **pourcentage** |
| 6 | Completed projects including failed | Afficher les **projets réalisés**, **y compris les échecs** |
| 7 | Navigate back to first view | Permettre de **revenir à la 1ʳᵉ vue** |
| 8 | Flexible / modern layout | Mise en page **flexible ou moderne** (contraintes, etc.) pour **plusieurs tailles d’écran** et plateformes |
| 9 | Do not create a token for each query | **Ne pas créer un token à chaque requête** — réutiliser le flux OAuth2 |
### OAuth2 — encadrés page 6
> Se référer à la **documentation OAuth2**.
> Vous devez utiliser **OAuth2 de l’Intra**. Pour des raisons de **confidentialité** et conformément aux lois de certains pays, il peut être **nécessaire de ne pas afficher** certaines informations.
**Liens utiles (hors PDF, à consulter pendant le projet) :**
- [Documentation développeur API 42](https://api.intra.42.fr/apidoc)
- [Applications OAuth sur l’Intra](https://profile.intra.42.fr/oauth/applications)
---
### V.2 Bonus
Bonus disponible :
| Bonus | Description |
|-------|-------------|
| Rafraîchissement du token | Recréer / **rafraîchir le token à l’expiration**. Si le token expire, l’app doit le renouveler et **continuer à fonctionner** dans tous les cas. |
### ⚠️ Règle bonus — encadré page 7
> La partie bonus n’est évaluée **que si la partie obligatoire est PARFAITE**. « Parfait » signifie : partie obligatoire **entièrement** réalisée et **sans dysfonctionnement**. Si **une seule** exigence obligatoire manque, le bonus **n’est pas évalué**.
---
## Chapitre VI — Rendu et évaluation par les pairs
Déposez le travail dans votre **dépôt Git** comme d’habitude. **Seul le contenu du dépôt** sera évalué en soutenance. Vérifiez les **noms de dossiers et de fichiers**.
→ Checklist soutenance (dépôt école) : [../../swiftyCompanion/README.md](../../swiftyCompanion/README.md) — dossier **non** inclus dans le rendu Intra si tu ne pousses que ce sous-répertoire.
→ Guide détaillé **sujet ↔ code** (dépôt perso) : [../pedagogie/guide-soutenance-realisation.md](../pedagogie/guide-soutenance-realisation.md)
---
## Éléments visuels du PDF
| Page PDF | Fichier extrait | Contenu visuel |
|----------|-----------------|----------------|
| 1 | [page-01-couverture.png](captures/page-01-couverture.png) | Page de garde complète |
| 1 (détail) | [illustration-motif-42.png](captures/illustration-motif-42.png) | Illustration centrale seule |
| 2–9 | *(pas d’image métier)* | Fond géométrique répété + texte des chapitres |
---
## Prochaine étape pédagogique
→ [Plan de réalisation pas à pas](../pedagogie/README.md)

View File

@@ -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)

View File

@@ -1,87 +1,100 @@
# 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é.
## 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/`, …) |

View 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

View File

@@ -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.

View File

@@ -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';

View File

@@ -4,7 +4,7 @@ import '../services/intra_api_exception.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});

View File

@@ -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(

View File

@@ -139,8 +139,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';

View File

@@ -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);

View File

@@ -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);

View File

@@ -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._();

View File

@@ -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,