Chapitre 1419 minIntermédiaire
Une application mobile à l'E6 et à l'E7 SLAM
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- présenter une réalisation mobile qui respecte l'annexe II.E ;
- préparer ton environnement pour l'heure de réalisation ;
- mener une évolution de la base jusqu'à l'écran ;
- répondre aux questions de sécurité mobile d'un sujet d'E7.
3 exercices corrigés
À l'E6 SLAM, une application mobile apporte la situation « code exécuté sur un terminal mobile » de l'annexe II.E et se prête bien aux évolutions demandées pendant l'heure de réalisation ; à l'écrit E7 SLAM, l'application mobile revient sous l'angle de la sécurité : stockage des secrets, biométrie et RGPD, communications, journalisation. Ce chapitre prépare les deux : présenter ton application à l'E6, préparer l'environnement de l'heure de réalisation, traiter une évolution complète de bout en bout, puis reconnaître et rédiger les réponses aux questions mobiles de l'E7. Le déroulement des épreuves est détaillé dans les cours Épreuve E6 SLAM et Épreuve E7 SLAM.
L'application mobile dans tes réalisations d'E6
Une réalisation mobile convaincante pour la commission a les mêmes qualités qu'une autre réalisation : une base relationnelle avec des contraintes, des rôles, des règles de gestion, de quoi faire évoluer. Elle y ajoute des points que la commission regarde en particulier :
| Ce que la commission regarde | Ce que tu montres avec AppliFrais Mobile |
|---|---|
| L'architecture | Le schéma à trois étages : application, API REST, base MySQL ; l'application ne touche jamais la base (chapitre 6) |
| L'appel à un service distant | Le client d'API, les jetons, la gestion des erreurs réseau et HTTP |
| La sécurité | SecureStore, HTTPS, contrôles refaits par l'API, revue MASVS (chapitre 13) |
| Les tests | npm test qui passe, le cahier de recette avec les appareils testés |
| Les outils | Git et ses tickets, Expo et Expo Build, Jest, l'émulateur |
| Ce qui est propre au mobile | Le mode hors ligne, les permissions, la publication ou l'APK |
Sur la fiche descriptive de la réalisation, cite les technologies avec leurs versions : « React Native 0.88 avec Expo SDK 58 et Expo Router, TypeScript ; API Node.js 24 et Express 5 ; MySQL 8.4 ; Jest et React Native Testing Library ; Expo Build pour l'APK de recette ». Sans versions, la commission ne peut pas vérifier la conformité de ton environnement (le dossier E6).
Préparer l'environnement de l'heure de réalisation
Pendant l'heure de réalisation, tu n'as pas le temps de déboguer un réseau ou d'attendre une compilation. Vérifie, la veille et en arrivant :
- l'API démarre en une commande, sa base de démonstration est remplie, et elle est joignable depuis l'émulateur (
10.0.2.2) ou le téléphone ; - l'émulateur Android est créé, démarre, et l'application s'y ouvre avec
npx expo startpuisa; - Expo Go, sur le téléphone, a le même SDK que le projet ; sinon, un development build est déjà installé ;
- le mode tunnel (
npx expo start --tunnel) fonctionne, en secours si le réseau de la salle isole les appareils ; - l'APK de recette est installé, et appelle une API en HTTPS ;
-
npm testpasse,npx tsc --noEmitne signale rien ; - le dépôt Git est propre, sur la bonne branche ;
- tu sais montrer l'écran du téléphone à la commission : l'émulateur sur ton écran, ou une recopie de l'écran du téléphone sur l'ordinateur (scrcpy, un outil libre, le fait pour Android par USB).
L'émulateur est souvent plus sûr que le téléphone pour l'épreuve : il ne dépend pas du Wi-Fi de la salle, et la commission voit l'écran sans se pencher sur ton téléphone.
Les évolutions types sur une application mobile
Les besoins exprimés par la commission touchent rarement une seule couche. Sur une application mobile reliée à une API, une évolution traverse souvent toute la pile.
| Évolution | Couches touchées |
|---|---|
| Ajouter un champ à un formulaire | Base (colonne), API (validation, requête), client (type), validation locale, écran, file hors ligne |
| Filtrer ou trier une liste | Écran (filtre local) ou API (paramètre ?etat=), selon le volume |
| Afficher un total, un compteur | Écran (calcul) ou API (champ calculé) |
| Ajouter un écran | Un fichier dans src/app, sa déclaration dans le layout, la navigation vers lui |
| Ajouter une règle de gestion (« pas de frais de plus de 3 mois ») | Validation locale et API, message d'erreur, test |
| Restreindre une action à un rôle | API d'abord (vérification du rôle), puis affichage conditionnel dans l'écran |
La méthode reste celle de l'épreuve : le vital d'abord, de bout en bout, testé ; le confort ensuite (l'heure de réalisation). Pour une évolution qui traverse la pile, avance de la base vers l'écran : chaque couche se vérifie avant de passer à la suivante (la requête SQL dans le client MySQL, la route avec curl, puis l'écran).
Cas corrigé : ajouter un commentaire à un frais
Besoin exprimé par la commission. Les comptables refusent souvent des frais faute d'explication. Le visiteur doit pouvoir ajouter, depuis l'application mobile, un commentaire facultatif de 200 caractères au plus à un frais hors forfait (« Repas avec le Dr Morel, cardiologue »). Le commentaire apparaît dans le détail de la fiche.
1. La base. Une colonne facultative :
ALTER TABLE frais_hors_forfait ADD COLUMN commentaire VARCHAR(200) NULL;
2. L'API. Dans l'API du cours Node.js (validation Zod), le schéma Zod de création gagne un champ facultatif, le dépôt l'enregistre avec une requête préparée, et la lecture d'une fiche le renvoie.
// src/schemas/fiches.schemas.js
export const schemaFraisHorsForfait = z.strictObject({
date: z.iso.date(),
libelle: z.string().trim().min(3).max(100),
montant: z.number().positive().max(5000).multipleOf(0.01),
commentaire: z.string().trim().max(200).nullish(), // absent, null, ou 200 caractères au plus
});
// src/depots/fiches.depot.js
export async function ajouterFraisHorsForfait(id, { date, libelle, montant, commentaire }) {
const texte = commentaire || null; // absent, null ou vide après trim : NULL en base
const [resultat] = await pool.execute(
'INSERT INTO frais_hors_forfait (id_fiche, date_frais, libelle, montant, commentaire) VALUES (?, ?, ?, ?, ?)',
[id, date, libelle, montant, texte],
);
return { id: resultat.insertId, date, libelle, montant, justificatif: false, commentaire: texte };
}
Dans trouverDetail, le SELECT des frais hors forfait ajoute la colonne commentaire. nullish() accepte un champ absent ou null : une ancienne version de l'application, qui n'envoie pas le champ, continue de fonctionner.
Vérifie avec curl avant de toucher à l'application (tester une API).
3. Les types du client d'API, dans api.ts :
export type FraisHorsForfait = { id: number; date: string; libelle: string; montant: number; justificatif: boolean; commentaire: string | null };
export type NouveauFrais = { date: string; libelle: string; montant: number; commentaire?: string };
4. La validation locale, dans validation.ts. SaisieFrais gagne commentaire: string, Frais gagne commentaire?: string, et validerFrais se termine ainsi :
const commentaire = saisie.commentaire.trim();
if (commentaire.length > 200) erreurs.commentaire = '200 caractères au maximum.';
if (Object.keys(erreurs).length > 0) return { erreurs };
const frais: Frais = { date: saisie.date, libelle, montant: montant as number };
if (commentaire) frais.commentaire = commentaire;
return { frais, erreurs };
Un commentaire vide ou fait d'espaces n'est pas envoyé : le champ reste absent, et l'API enregistre NULL. Comme commentaire devient obligatoire dans SaisieFrais, les appels existants à validerFrais, dans l'écran et dans les tests, reçoivent commentaire: ''.
5. L'écran, dans le formulaire « Nouveau frais » : l'état initial reçoit commentaire: '', et un champ de plusieurs lignes s'ajoute :
<Champ
libelle="Commentaire (facultatif)"
value={saisie.commentaire}
onChangeText={modifier('commentaire')}
erreur={erreurs.commentaire}
multiline
maxLength={200}
/>
Dans le détail de la fiche, sous le libellé : {item.commentaire && <Text style={styles.date}>{item.commentaire}</Text>}.
6. La file hors ligne. Un frais saisi sans réseau doit garder son commentaire. C'est l'oubli classique : tout marche en ligne, et le commentaire disparaît des frais saisis dans le train. Dans db.ts, la table SQLite locale passe à la version 2 du schéma, et la file prend la colonne :
// Dans migrer(), après le bloc de la version 1
if (version < 2) {
await db.execAsync(`
ALTER TABLE frais_en_attente ADD COLUMN commentaire TEXT;
PRAGMA user_version = 2;
`);
}
// Le commentaire lu en base vaut NULL quand il n'y en a pas
export type FraisEnAttente = Omit<Frais, 'commentaire'> & {
commentaire: string | null; cle: string; idFiche: number; photo: string | null;
};
export async function mettreEnAttente(idFiche: number, frais: Frais, cle: string, photo: string | null) {
const db = await ouvrirBase();
await db.runAsync(
'INSERT INTO frais_en_attente (cle, id_fiche, date, libelle, montant, commentaire, photo) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (cle) DO NOTHING',
[cle, idFiche, frais.date, frais.libelle, frais.montant, frais.commentaire ?? null, photo],
);
}
export async function lireEnAttente(): Promise<FraisEnAttente[]> {
const db = await ouvrirBase();
return db.getAllAsync<FraisEnAttente>(
'SELECT cle, id_fiche AS idFiche, date, libelle, montant, commentaire, photo FROM frais_en_attente ORDER BY rowid',
);
}
Et synchroniser doit transmettre le commentaire (avec import type { NouveauFrais } from './api') :
const nouveau: NouveauFrais = { date: f.date, libelle: f.libelle, montant: f.montant };
if (f.commentaire) nouveau.commentaire = f.commentaire; // NULL en base : champ absent
const cree = await api.ajouterFrais(f.idFiche, nouveau, f.cle);
Le frais renvoyé par la file est ainsi identique à celui qu'aurait envoyé le formulaire : même corps, donc même empreinte pour la clé d'idempotence. Trois fichiers à toucher pour la file : db.ts (migration, type, insertion, lecture), synchro.ts, et côté API le schéma qui accepte l'absence de commentaire. Les tests de synchroniser du chapitre 11 ajoutent alors commentaire: null aux frais qu'ils fabriquent.
7. Le test, dans validation.test.ts :
it('accepte un commentaire facultatif et refuse plus de 200 caractères', () => {
const base = { date: '2026-10-14', libelle: 'Repas', montant: '23,50' };
expect(validerFrais({ ...base, commentaire: ' ' }, '2026-11-02').frais).toEqual({ date: '2026-10-14', libelle: 'Repas', montant: 23.5 });
expect(validerFrais({ ...base, commentaire: ' Client Dr Morel ' }, '2026-11-02').frais?.commentaire).toBe('Client Dr Morel');
expect(validerFrais({ ...base, commentaire: 'x'.repeat(201) }, '2026-11-02').erreurs.commentaire).toBeDefined();
});
Le vital de ce besoin : la colonne, l'API, le champ du formulaire, l'affichage dans le détail. Le confort : un compteur de caractères sous le champ, la colonne dans la file hors ligne si le temps manque (à signaler à la commission comme limite connue, plutôt qu'à cacher).
Les questions d'application mobile à l'E7
Le sujet de cybersécurité SLAM de 2024 (alors épreuve E6, unité U6, devenue E7 en 2025) consacrait tout un dossier à l'application mobile d'un service d'incendie et de secours (le corrigé). Ses questions reviennent sous d'autres formes d'une session à l'autre :
| Question type | Ce qu'on attend | Chapitre |
|---|---|---|
| Comparer un code PIN et un mot de passe | Le calcul du nombre de combinaisons (10⁴ pour 4 chiffres, environ 70⁸ pour 8 caractères parmi 70) et la résistance à la force brute | Cours Authentification |
| Distinguer biométrie et mot de passe au regard du RGPD | Donnée biométrique : catégorie particulière (article 9), alternative obligatoire, analyse d'impact | 9 |
| Écrire l'insertion dans un journal SQLite local | Une requête préparée avec des ? et un tableau de valeurs | 7 |
| Compléter les rappels de succès et d'échec de l'authentification biométrique | Journaliser l'échec avec le code d'erreur, le succès avec « OK », puis ouvrir l'écran suivant | 9 |
| Vérifier qu'un journal respecte les recommandations de la CNIL | Auteur identifié, horodatage, équipement, nature de l'opération ; conservation limitée | 9 |
| Où stocker un jeton ou une clé sur le téléphone | Le Keystore ou le trousseau (SecureStore), jamais les préférences en clair ni le code | 7 et 13 |
| Pourquoi une clé d'API ne doit pas être dans l'application | Le code se décompile ; un client public ne garde aucun secret ; passer par le serveur | 6 et 13 |
| Sécuriser les échanges | HTTPS, vérification des certificats, jeton dans l'en-tête et non dans l'adresse ; épinglage pour une application sensible | 13 |
Les sujets donnent le code dans le langage de l'application étudiée, souvent du Java ou du Kotlin pour Android, parfois du C#. Tu n'as pas à connaître ce langage : les noms des fonctions (onAuthenticationSucceeded, execSQL, getWritableDatabase) et la logique suffisent, et les mêmes principes s'appliquent qu'en TypeScript (lire du code sur papier). Rédige chaque réponse en situant le problème, en le nommant avec le vocabulaire juste (stockage non sécurisé, client public, catégorie particulière de données), en l'expliquant dans le contexte du sujet, puis en corrigeant.
Exercice 1 sur 3 Cas type E7 : l'application de suivi des tournées Réussi À revoir
La société Transval, un transporteur de 60 chauffeurs, développe une application mobile pour ses tournées. Lis les extraits et réponds.
Document 1 : stockage des identifiants
async function seConnecter(matricule: string, motDePasse: string) {
const reponse = await fetch(`${URL_API}/connexion`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ matricule, motDePasse }),
});
const { jeton } = await reponse.json();
await Storage.setItem('jeton', jeton); // kv-store
await Storage.setItem('motDePasse', motDePasse); // pour la reconnexion automatique
}
Document 2 : journal local. Table connexion (id, horodatage, resultat, message) ; la fonction d'ajout construit la requête ainsi :
await db.execAsync(`INSERT INTO connexion (resultat, message) VALUES ('${resultat}', '${message}')`);
Document 3 : projet. La direction envisage de remplacer le mot de passe par la reconnaissance faciale « pour gagner du temps », et de suivre en permanence la position des chauffeurs pendant leurs heures de travail.
- Relève deux problèmes de sécurité dans le document 1 et propose une correction.
- Quelle vulnérabilité présente le document 2 ? Réécris la fonction d'ajout.
- La table
connexionpermet-elle une journalisation conforme aux recommandations de la CNIL sur un terminal partagé entre chauffeurs ? Que faut-il ajouter ? - Quelles conditions la direction doit-elle respecter pour la reconnaissance faciale ?
- Le suivi permanent de la position des chauffeurs est-il possible ? À quelles conditions ?
Voir la réponseMasquer la réponse de l'exercice 1
-
Le jeton est stocké en clair dans kv-store, un fichier lisible sur un téléphone rooté ou dans une sauvegarde : il doit aller dans SecureStore (Keystore ou trousseau). Le mot de passe est stocké, en clair de surcroît : on ne stocke jamais un mot de passe ; la reconnexion automatique repose sur un jeton de rafraîchissement révocable, rangé dans SecureStore. Accessoirement,
reponse.okn'est pas vérifié : une erreur401serait traitée comme un succès. -
Injection SQL dans la base locale : les valeurs sont collées dans la requête. Si
messagecontient une apostrophe (« l'utilisateur… »), la requête casse ; une valeur forgée peut modifier la requête. Correction avec une requête préparée :await db.runAsync('INSERT INTO connexion (resultat, message) VALUES (?, ?)', [resultat, message]);execAsyncn'accepte pas de paramètres : il est réservé au SQL fixe. -
Non. La table contient l'horodatage et la nature de l'opération (
resultat,message), mais ni l'auteur (le matricule du chauffeur), indispensable sur un terminal partagé, ni l'équipement (un identifiant du terminal). Il faut ajouter au moinsmatriculeetid_terminal, et prévoir une durée de conservation limitée (la CNIL recommande de l'ordre de six mois à un an pour ces journaux, sauf obligation particulière). -
La reconnaissance faciale traite une donnée biométrique, catégorie particulière au sens de l'article 9 du RGPD, dont le traitement est interdit par principe. « Gagner du temps » ne suffit pas à le justifier. Si elle est retenue : utiliser la biométrie du système (le modèle reste dans le téléphone, l'employeur ne le reçoit jamais), proposer une alternative (mot de passe ou code) sans pénaliser ceux qui la refusent, informer les chauffeurs, inscrire le traitement au registre et mener une analyse d'impact si le risque est élevé, en suivant le règlement type de la CNIL sur la biométrie au travail.
-
La géolocalisation des salariés est encadrée par la CNIL. Un suivi permanent est disproportionné s'il sert à contrôler le travail en continu. Il peut être admis pour une finalité légitime (optimiser les tournées, assurer la sécurité des chauffeurs, justifier une prestation facturée), à condition de ne géolocaliser que pendant les heures de travail, de permettre au chauffeur de désactiver le suivi pendant les pauses, d'informer les salariés et le CSE, de limiter la conservation des positions, et de l'inscrire au registre. Côté application, cela suppose la permission de localisation en arrière-plan, qu'il faudra aussi justifier auprès des magasins.
Tu avais trouvé ?
Exercice 2 sur 3 Évolution E6 : un filtre et un compteur Réussi À revoir
La commission te demande : « Dans l'onglet Mes fiches, afficher en haut le nombre de fiches à l'état clôturé (CL), en attente de validation. » Donne le vital, le confort, les couches touchées et le test.
Voir la réponseMasquer la réponse de l'exercice 2
- Vital : calculer le nombre dans l'écran à partir des fiches chargées,
const enAttente = (fiches ?? []).filter((f) => f.etat === 'CL').length;, et l'afficher dansListHeaderComponent: « 2 fiches en attente de validation ». Une seule couche touchée : l'écran. - Attention : la liste est paginée (20 fiches par page). Si un visiteur a plus de 20 fiches, le compteur calculé sur la page chargée serait faux. Le vital devient alors un champ calculé par l'API (
GET /v1/fiches?etat=CL&limite=1et lirepagination.total, ou une route de statistiques). - Confort : accorder le pluriel (« 1 fiche », « 2 fiches »), masquer la ligne quand le compteur vaut 0, la rendre touchable pour filtrer la liste.
- Test : un test unitaire d'une fonction
compterEnAttente(fiches)extraite de l'écran (0, 1, plusieurs fiches, aucune à l'étatCL), et la vérification à l'écran pendant la recette avec les données de démonstration.
Tu avais trouvé ?
Exercice 3 sur 3 PIN ou mot de passe Réussi À revoir
Une application propose de remplacer le mot de passe de 8 caractères (minuscules, majuscules et chiffres, soit 62 caractères possibles) par un code PIN de 6 chiffres. Calcule le nombre de combinaisons de chaque solution et conclus. Qu'est-ce qui peut rendre le PIN acceptable malgré tout ?
Voir la réponseMasquer la réponse de l'exercice 3
- PIN de 6 chiffres : 10⁶ = 1 000 000 combinaisons.
- Mot de passe de 8 caractères parmi 62 : 62⁸ = 218 340 105 584 896, soit environ 2,18 × 10¹⁴ combinaisons, plus de 200 millions de fois plus.
Face à une attaque par force brute sans limite d'essais (par exemple sur un fichier volé), le PIN ne résiste pas : un million d'essais se font en quelques secondes. Il devient acceptable si le nombre d'essais est limité par un composant que l'attaquant ne contrôle pas : le verrouillage du téléphone après quelques échecs, garanti par la puce sécurisée, ou un compteur côté serveur qui bloque le compte. C'est le principe du code de carte bancaire, à 4 chiffres seulement, mais bloqué après trois erreurs.
Tu avais trouvé ?