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:
+36
-1
@@ -5,7 +5,9 @@ import type {
|
||||
FileDto,
|
||||
FolderDto,
|
||||
ListFilesParams,
|
||||
LoginResponse,
|
||||
OcrJob,
|
||||
ResolvedUser,
|
||||
ResourcePermission,
|
||||
SyncOperation,
|
||||
SyncResult,
|
||||
@@ -25,6 +27,14 @@ export function hasAuthToken(): boolean {
|
||||
return authToken !== null;
|
||||
}
|
||||
|
||||
// Notification de 401 reçus sur un endpoint protégé (hors login lui-même) —
|
||||
// AuthContext s'en sert pour purger la session (token révoqué/expiré).
|
||||
let onUnauthorized: (() => void) | null = null;
|
||||
|
||||
export function setUnauthorizedHandler(handler: (() => void) | null): void {
|
||||
onUnauthorized = handler;
|
||||
}
|
||||
|
||||
type QueryParams = Record<string, string | number | boolean | undefined | null>;
|
||||
|
||||
function toQuery(params?: QueryParams): string {
|
||||
@@ -60,7 +70,8 @@ export class ApiError extends Error {
|
||||
async function request<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
timeoutMs: number = DEFAULT_TIMEOUT_MS
|
||||
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
||||
opts: { skipUnauthorizedHandling?: boolean } = {},
|
||||
): Promise<ApiData<T>> {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
@@ -76,6 +87,10 @@ async function request<T>(
|
||||
throw new ApiError('NETWORK_ERROR', 'Serveur injoignable');
|
||||
}
|
||||
|
||||
if (response.status === 401 && !opts.skipUnauthorizedHandling) {
|
||||
onUnauthorized?.();
|
||||
}
|
||||
|
||||
const body = (await response.json().catch(() => null)) as
|
||||
| ApiData<T>
|
||||
| ApiErrorBody
|
||||
@@ -136,6 +151,26 @@ export const api = {
|
||||
body: JSON.stringify({ deviceId }),
|
||||
}),
|
||||
|
||||
// Seule porte d'émission de token (V1). Le 401 = mauvaises identifiants
|
||||
// (normal sur l'écran de login) : on ne déclenche pas la purge de session.
|
||||
login: (username: string, password: string, deviceId: string) =>
|
||||
request<LoginResponse>('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, device_id: deviceId }),
|
||||
}, DEFAULT_TIMEOUT_MS, { skipUnauthorizedHandling: true }),
|
||||
|
||||
changePassword: (currentPassword: string, newPassword: string) =>
|
||||
request<{ id: string }>('/users/me/password', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
|
||||
}),
|
||||
|
||||
// Résolution exacte d'un destinataire par username — jamais de listing.
|
||||
resolveUser: (username: string) =>
|
||||
request<ResolvedUser>(`/users/resolve${toQuery({ username })}`),
|
||||
|
||||
listFiles: (params?: ListFilesParams) =>
|
||||
request<FileDto[]>(`/files${toQuery(params)}`),
|
||||
|
||||
|
||||
@@ -44,7 +44,29 @@ export type OcrJob = {
|
||||
|
||||
export type DeviceRegistration = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
username: string;
|
||||
is_admin: boolean;
|
||||
};
|
||||
|
||||
export type LoginRequest = {
|
||||
username: string;
|
||||
password: string;
|
||||
device_id: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
token: string;
|
||||
expires_at: number;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type ResolvedUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type ListParams = {
|
||||
|
||||
Reference in New Issue
Block a user