Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 regardeCe que tu montres avec AppliFrais Mobile
L'architectureLe schéma à trois étages : application, API REST, base MySQL ; l'application ne touche jamais la base (chapitre 6)
L'appel à un service distantLe 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 testsnpm test qui passe, le cahier de recette avec les appareils testés
Les outilsGit et ses tickets, Expo et Expo Build, Jest, l'émulateur
Ce qui est propre au mobileLe 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 start puis a ;
  • 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 test passe, npx tsc --noEmit ne 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.

ÉvolutionCouches touchées
Ajouter un champ à un formulaireBase (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 écranUn 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ôleAPI 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 :

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

js
// 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
});
js
// 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 :

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 :

ts
  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 :

tsx
<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 :

ts
// 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') :

ts
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 :

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 typeCe qu'on attendChapitre
Comparer un code PIN et un mot de passeLe calcul du nombre de combinaisons (10⁴ pour 4 chiffres, environ 70⁸ pour 8 caractères parmi 70) et la résistance à la force bruteCours Authentification
Distinguer biométrie et mot de passe au regard du RGPDDonnée biométrique : catégorie particulière (article 9), alternative obligatoire, analyse d'impact9
Écrire l'insertion dans un journal SQLite localUne requête préparée avec des ? et un tableau de valeurs7
Compléter les rappels de succès et d'échec de l'authentification biométriqueJournaliser l'échec avec le code d'erreur, le succès avec « OK », puis ouvrir l'écran suivant9
Vérifier qu'un journal respecte les recommandations de la CNILAuteur identifié, horodatage, équipement, nature de l'opération ; conservation limitée9
Où stocker un jeton ou une clé sur le téléphoneLe Keystore ou le trousseau (SecureStore), jamais les préférences en clair ni le code7 et 13
Pourquoi une clé d'API ne doit pas être dans l'applicationLe code se décompile ; un client public ne garde aucun secret ; passer par le serveur6 et 13
Sécuriser les échangesHTTPS, vérification des certificats, jeton dans l'en-tête et non dans l'adresse ; épinglage pour une application sensible13

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

ts
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 :

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

  1. Relève deux problèmes de sécurité dans le document 1 et propose une correction.
  2. Quelle vulnérabilité présente le document 2 ? Réécris la fonction d'ajout.
  3. La table connexion permet-elle une journalisation conforme aux recommandations de la CNIL sur un terminal partagé entre chauffeurs ? Que faut-il ajouter ?
  4. Quelles conditions la direction doit-elle respecter pour la reconnaissance faciale ?
  5. Le suivi permanent de la position des chauffeurs est-il possible ? À quelles conditions ?
Voir la réponseMasquer la réponse de l'exercice 1
  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.ok n'est pas vérifié : une erreur 401 serait traitée comme un succès.

  2. Injection SQL dans la base locale : les valeurs sont collées dans la requête. Si message contient 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 :

    ts
    await db.runAsync('INSERT INTO connexion (resultat, message) VALUES (?, ?)', [resultat, message]);
    

    execAsync n'accepte pas de paramètres : il est réservé au SQL fixe.

  3. 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 moins matricule et id_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).

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

  5. 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 dans ListHeaderComponent : « 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=1 et lire pagination.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'état CL), 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é ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Refresh token, idempotence et notifications : l'API Node.js d'une application mobileRevoir le sommaire du cours