feat(api): /devices register + paseto v4 (middleware Bearer, bootstrap mobile du token)
This commit is contained in:
@@ -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)}`),
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ export type OcrJob = {
|
||||
error?: string | null;
|
||||
};
|
||||
|
||||
export type DeviceRegistration = {
|
||||
deviceId: string;
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type ListParams = {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Generated
+12
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -39,6 +39,7 @@ export {
|
||||
DATABASE_NAME,
|
||||
DATABASE_VERSION,
|
||||
DEVICE_USER_ID_KEY,
|
||||
AUTH_TOKEN_KEY,
|
||||
PREFERENCES_KEY,
|
||||
PERMISSION_TTL_MS,
|
||||
} from './schema';
|
||||
@@ -11,6 +11,8 @@ export {
|
||||
saveUserPreferences,
|
||||
getUserPreferences,
|
||||
getDeviceUserId,
|
||||
getDeviceAuthToken,
|
||||
saveDeviceAuthToken,
|
||||
} from './preferences';
|
||||
export {
|
||||
getResourcePermission,
|
||||
|
||||
@@ -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 }>(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export {
|
||||
getUserPreferences,
|
||||
getDeviceUserId,
|
||||
getDeviceAuthToken,
|
||||
saveDeviceAuthToken,
|
||||
getFolders,
|
||||
getFolderFolders,
|
||||
getFolder,
|
||||
|
||||
Reference in New Issue
Block a user