feat(mobile): identité user-first — login/password, SecureStore, scoping par compte

- api: DeviceRegistration device-only, login/changePassword/resolveUser, interception 401 (purge) exonérée sur le login ; type User aligné sur le fil (is_admin)
- SecureStore (expo-secure-store v57) : token + profil, jamais en SQLite ; miroir active_user_id
- AuthContext : statut loading/signedOut/signedIn, bootstrap register→restore, signIn/signOut, garde-fou switch de compte
- app/login.tsx + gate de routes dans _layout (Redirect signedOut) ; i18n fr/en
- DB v5 : user_id sur pending_operations + resource_permissions (UNIQUE par user), repos scopés (user_id IS ? OR IS NULL)
- syncOutbox : delta permissions par compte (Map), outbox poussée du compte actif uniquement
- tests: register/login/401/resolve/changePassword, scoping outbox+permissions, migrations v5 ; e2e live revert register→login admin
This commit is contained in:
m
2026-09-10 21:34:34 +02:00
parent c2c7dddba6
commit 0536fca6c3
25 changed files with 663 additions and 49 deletions
+17 -5
View File
@@ -13,10 +13,17 @@ import { MIGRATIONS, type MigrationDb } from '../services/db/migrations';
import type { DbSession } from '../services/db/session';
import { __setDbForTests } from '../services/db/session';
import { api, setAuthToken } from '../api/client';
import { enqueuePendingOperation, getPendingOperations } from '../services/db';
import {
clearActiveUserId,
enqueuePendingOperation,
getPendingOperations,
setActiveUserId,
} from '../services/db';
import { pushPendingOps, refreshPermissions, resetPermissionCachedAtForTests } from '../features/syncOutbox';
const BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL ?? 'http://localhost:8080/api/v1';
const ADMIN_USERNAME = process.env.E2E_ADMIN_USERNAME ?? 'admin';
const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? 'admin';
type Harness = MigrationDb & DbSession;
@@ -75,9 +82,10 @@ beforeEach(async () => {
afterEach(() => {
__setDbForTests(null);
setAuthToken(null);
clearActiveUserId();
});
test('bout en bout : register → push outbox → snapshot → relecture serveur', async (t) => {
test('bout en bout : register + login admin → push outbox → snapshot → relecture serveur', async (t) => {
try {
await api.health();
} catch (error) {
@@ -88,11 +96,15 @@ test('bout en bout : register → push outbox → snapshot → relecture serveur
throw error;
}
// Enregistrement du device (idempotent) puis login = SEULE porte de token.
deviceId = newDeviceId();
const reg = await api.registerDevice(deviceId);
setAuthToken(reg.data.token);
await api.registerDevice(deviceId);
const login = await api.login(ADMIN_USERNAME, ADMIN_PASSWORD, deviceId);
setAuthToken(login.data.token);
await setActiveUserId(login.data.user.id);
assert.equal(login.data.user.is_admin, true, 'ladmin E2E doit exister (ADMIN_*)');
// 1. Enqueue un CREATE_RESOURCE puis push
// 1. Enqueue un CREATE_RESOURCE puis push (scopé au compte connecté)
const name = `e2e-${deviceId.slice(0, 8)}`;
const resourceId = newDeviceId();
const opId = await enqueuePendingOperation({