feat(api): /devices register + paseto v4 (middleware Bearer, bootstrap mobile du token)

This commit is contained in:
m
2026-09-10 07:25:11 +02:00
parent bdf1fadce9
commit caa6e6483c
18 changed files with 407 additions and 27 deletions
+24
View File
@@ -1,6 +1,7 @@
import type {
ApiData,
ApiErrorBody,
DeviceRegistration,
FileDto,
FolderDto,
ListFilesParams,
@@ -11,6 +12,12 @@ const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL ?? 'http://localhost:8
export const DEFAULT_TIMEOUT_MS = 15_000;
let authToken: string | null = null;
export function setAuthToken(token: string | null): void {
authToken = token;
}
type QueryParams = Record<string, string | number | boolean | undefined | null>;
function toQuery(params?: QueryParams): string {
@@ -24,6 +31,15 @@ function toQuery(params?: QueryParams): string {
return query ? `?${query}` : '';
}
function mergeHeaders(init?: HeadersInit): HeadersInit | undefined {
if ( !authToken ) return init;
const merged = new Headers(init);
if ( !merged.has('Authorization') ) {
merged.set('Authorization', `Bearer ${authToken}`);
}
return merged;
}
export class ApiError extends Error {
readonly code: string;
@@ -46,6 +62,7 @@ async function request<T>(
try {
response = await fetch(`${API_BASE_URL}${path}`, {
...init,
headers: mergeHeaders(init.headers),
signal: controller.signal,
});
} catch {
@@ -76,6 +93,13 @@ export const api = {
health: () => request<{ status: string }>('/health'),
registerDevice: (deviceId: string) =>
request<DeviceRegistration>('/devices', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ deviceId }),
}),
listFiles: (params?: ListFilesParams) =>
request<FileDto[]>(`/files${toQuery(params)}`),
+5
View File
@@ -42,6 +42,11 @@ export type OcrJob = {
error?: string | null;
};
export type DeviceRegistration = {
deviceId: string;
token: string;
};
export type ListParams = {
page?: number;
pageSize?: number;
+24 -9
View File
@@ -1,7 +1,12 @@
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { useSyncDevice } from '../features/syncDevice';
import { getDeviceUserId } from '../services/localStorage';
import { api, setAuthToken } from '../api/client';
import {
getDeviceAuthToken,
getDeviceUserId,
saveDeviceAuthToken,
} from '../services/localStorage';
import type { AuthContextValue, User } from './AuthContext.types';
const AuthContext = createContext<AuthContextValue | null>(null);
@@ -13,15 +18,25 @@ export function AuthProvider({ children }: { children: ReactNode }) {
useEffect(() => {
let active = true;
(async () => {
try {
const id = await getDeviceUserId();
if (active) setDeviceUserId(id);
} catch (error) {
console.warn('device identity unavailable', error);
}
})();
const bootstrap = async () => {
const id = await getDeviceUserId();
if (active) setDeviceUserId(id);
try {
let token = await getDeviceAuthToken();
if ( !token ) {
const { data } = await api.registerDevice(id);
token = data.token;
await saveDeviceAuthToken(token);
}
setAuthToken(token);
} catch (error) {
console.warn('device registration failed (offline?)', error);
}
};
bootstrap();
useSyncDevice();
return () => {
+12
View File
@@ -8,6 +8,7 @@
"name": "webui",
"version": "2.0.0",
"dependencies": {
"@expo/vector-icons": "^15.1.1",
"@tanstack/react-query": "^5.102.8",
"expo": "~57.0.8",
"expo-constants": "~57.0.17",
@@ -1991,6 +1992,17 @@
"integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==",
"license": "MIT"
},
"node_modules/@expo/vector-icons": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz",
"integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==",
"license": "MIT",
"peerDependencies": {
"expo-font": ">=14.0.4",
"react": "*",
"react-native": "*"
}
},
"node_modules/@expo/xcpretty": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.5.tgz",
+1
View File
@@ -3,6 +3,7 @@
"version": "2.0.0",
"main": "expo-router/entry",
"dependencies": {
"@expo/vector-icons": "^15.1.1",
"@tanstack/react-query": "^5.102.8",
"expo": "~57.0.8",
"expo-constants": "~57.0.17",
+1
View File
@@ -39,6 +39,7 @@ export {
DATABASE_NAME,
DATABASE_VERSION,
DEVICE_USER_ID_KEY,
AUTH_TOKEN_KEY,
PREFERENCES_KEY,
PERMISSION_TTL_MS,
} from './schema';
+2
View File
@@ -11,6 +11,8 @@ export {
saveUserPreferences,
getUserPreferences,
getDeviceUserId,
getDeviceAuthToken,
saveDeviceAuthToken,
} from './preferences';
export {
getResourcePermission,
+21 -1
View File
@@ -1,5 +1,5 @@
import { getSession } from '../session';
import { DEVICE_USER_ID_KEY, PREFERENCES_KEY } from '../schema';
import { AUTH_TOKEN_KEY, DEVICE_USER_ID_KEY, PREFERENCES_KEY } from '../schema';
import type { UserPreferences } from '../types';
const DEFAULT_PREFERENCES: UserPreferences = {
@@ -38,6 +38,26 @@ export async function saveUserPreferences(preferences: UserPreferences): Promise
);
}
export async function getDeviceAuthToken(): Promise<string | null> {
const db = await getSession();
const row = await db.getFirstAsync<{ value: string }>(
'SELECT "value" FROM user_preferences WHERE "key" = ?',
AUTH_TOKEN_KEY,
);
return row?.value ?? null;
}
export async function saveDeviceAuthToken(token: string): Promise<void> {
const db = await getSession();
await db.runAsync(
`INSERT INTO user_preferences ("key", "value", updated_at) VALUES (?, ?, ?)
ON CONFLICT("key") DO UPDATE SET "value" = excluded."value", updated_at = excluded.updated_at`,
AUTH_TOKEN_KEY,
token,
Date.now(),
);
}
export async function getUserPreferences(): Promise<UserPreferences> {
const db = await getSession();
const row = await db.getFirstAsync<{ value: string }>(
+2
View File
@@ -6,6 +6,8 @@ export const PREFERENCES_KEY = 'user_preferences';
export const DEVICE_USER_ID_KEY = 'device_user_id';
export const AUTH_TOKEN_KEY = 'auth_token';
export const FOLDER_COLUMNS = [
'resource_id',
'uri',
+2
View File
@@ -1,6 +1,8 @@
export {
getUserPreferences,
getDeviceUserId,
getDeviceAuthToken,
saveDeviceAuthToken,
getFolders,
getFolderFolders,
getFolder,