update flow and auth model

This commit is contained in:
Ada
2026-01-28 16:27:00 -08:00
parent 4d94888bb8
commit 146320052e
12 changed files with 1997 additions and 597 deletions

48
App.tsx
View File

@@ -1,28 +1,54 @@
/**
* App Entry Point
*
* Main application component with authentication routing.
* Shows loading screen while restoring auth state.
*/
import React from 'react'; import React from 'react';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StyleSheet, View, ActivityIndicator } from 'react-native'; import { StyleSheet, View, ActivityIndicator, Text } from 'react-native';
import TabNavigator from './src/navigation/TabNavigator'; import TabNavigator from './src/navigation/TabNavigator';
import AuthNavigator from './src/navigation/AuthNavigator'; import AuthNavigator from './src/navigation/AuthNavigator';
import { AuthProvider, useAuth } from './src/context/AuthContext'; import { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors'; import { colors } from './src/theme/colors';
/**
* Loading screen shown while restoring auth state
*/
function LoadingScreen() {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.nautical.teal} />
<Text style={styles.loadingText}>Loading...</Text>
</View>
);
}
/**
* Main app content with auth-based routing
*/
function AppContent() { function AppContent() {
const { user } = useAuth(); const { user, isInitializing } = useAuth();
// Show loading screen while restoring auth state
if (isInitializing) {
return <LoadingScreen />;
}
return ( return (
<NavigationContainer> <NavigationContainer>
<StatusBar style="auto" /> <StatusBar style="auto" />
{user ? ( {user ? <TabNavigator /> : <AuthNavigator />}
<TabNavigator />
) : (
<AuthNavigator />
)}
</NavigationContainer> </NavigationContainer>
); );
} }
/**
* Root App component
*/
export default function App() { export default function App() {
return ( return (
<GestureHandlerRootView style={styles.container}> <GestureHandlerRootView style={styles.container}>
@@ -42,7 +68,11 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.sentinel.background, backgroundColor: colors.flow.backgroundGradientStart,
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: colors.flow.textSecondary,
}, },
}); });

View File

@@ -44,8 +44,35 @@ Sentinel is a mobile application that helps users securely manage their digital
- **Framework**: React Native (Expo SDK 52) - **Framework**: React Native (Expo SDK 52)
- **Language**: TypeScript - **Language**: TypeScript
- **Navigation**: React Navigation (Bottom Tabs) - **Navigation**: React Navigation (Bottom Tabs)
- **Icons**: @expo/vector-icons (Feather, Ionicons, MaterialCommunityIcons, FontAwesome5) - **Icons**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
- **Styling**: Custom nautical theme with gradients - **Styling**: Custom nautical theme with gradients
- **State Management**: React Context (AuthContext)
- **Storage**: AsyncStorage for auth persistence
## Configuration
The application uses a centralized configuration file located at `src/config/index.ts`.
### Key Configuration Options
| Option | Description | Default |
|--------|-------------|---------|
| `NO_BACKEND_MODE` | Use mock data instead of real backend | `false` |
| `DEBUG_MODE` | Enable API debug logging | `true` |
| `API_BASE_URL` | Backend API server URL | `http://localhost:8000` |
| `API_TIMEOUT` | Request timeout (ms) | `30000` |
### API Endpoints
All backend API routes are defined in `API_ENDPOINTS`:
- **AUTH**: `/login`, `/register`
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
- **AI**: `/ai/proxy`
- **ADMIN**: `/admin/declare-guale`
### Environment Setup
For development, you may need to modify `API_BASE_URL` in the config file to match your backend server address.
## Project Structure ## Project Structure
@@ -56,14 +83,27 @@ src/
│ ├── BiometricModal.tsx │ ├── BiometricModal.tsx
│ ├── Icons.tsx │ ├── Icons.tsx
│ └── VaultDoorAnimation.tsx │ └── VaultDoorAnimation.tsx
├── config/
│ └── index.ts # Centralized configuration
├── context/
│ └── AuthContext.tsx # Authentication state management
├── navigation/ ├── navigation/
── TabNavigator.tsx ── AuthNavigator.tsx # Login/Register navigation
│ └── TabNavigator.tsx # Main app navigation
├── screens/ ├── screens/
│ ├── FlowScreen.tsx │ ├── FlowScreen.tsx # AI chat interface
│ ├── VaultScreen.tsx │ ├── VaultScreen.tsx
│ ├── SentinelScreen.tsx │ ├── SentinelScreen.tsx
│ ├── HeritageScreen.tsx │ ├── HeritageScreen.tsx
── MeScreen.tsx ── MeScreen.tsx
│ ├── LoginScreen.tsx
│ └── RegisterScreen.tsx
├── services/
│ ├── index.ts # Service exports
│ ├── ai.service.ts # AI API integration
│ ├── auth.service.ts # Authentication API
│ ├── assets.service.ts # Asset management API
│ └── admin.service.ts # Admin operations API
├── theme/ ├── theme/
│ └── colors.ts │ └── colors.ts
└── types/ └── types/
@@ -80,6 +120,15 @@ assets/
└── captain-avatar.svg # Avatar placeholder └── captain-avatar.svg # Avatar placeholder
``` ```
## Services
The application uses a modular service architecture for API communication:
- **AuthService**: User authentication (login, register)
- **AIService**: AI conversation proxy with support for text and image input
- **AssetsService**: Digital asset management
- **AdminService**: Administrative operations
## Icons & Branding ## Icons & Branding
The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background. The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background.
@@ -163,8 +212,44 @@ Sentinel 是一款帮助用户安全管理数字遗产的移动应用程序。
- **框架**: React Native (Expo SDK 52) - **框架**: React Native (Expo SDK 52)
- **语言**: TypeScript - **语言**: TypeScript
- **导航**: React Navigation (底部标签) - **导航**: React Navigation (底部标签)
- **图标**: @expo/vector-icons - **图标**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
- **样式**: 自定义航海主题配渐变 - **样式**: 自定义航海主题配渐变
- **状态管理**: React Context (AuthContext)
- **存储**: AsyncStorage 用于认证持久化
## 配置说明
应用使用位于 `src/config/index.ts` 的集中配置文件。
### 主要配置项
| 选项 | 说明 | 默认值 |
|------|------|--------|
| `NO_BACKEND_MODE` | 使用模拟数据而非真实后端 | `false` |
| `DEBUG_MODE` | 启用 API 调试日志 | `true` |
| `API_BASE_URL` | 后端 API 服务器地址 | `http://localhost:8000` |
| `API_TIMEOUT` | 请求超时时间(毫秒) | `30000` |
### API 端点
所有后端 API 路由定义在 `API_ENDPOINTS` 中:
- **AUTH**: `/login`, `/register`
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
- **AI**: `/ai/proxy`
- **ADMIN**: `/admin/declare-guale`
### 环境配置
开发时,您可能需要修改配置文件中的 `API_BASE_URL` 以匹配您的后端服务器地址。
## 服务层
应用使用模块化的服务架构进行 API 通信:
- **AuthService**: 用户认证(登录、注册)
- **AIService**: AI 对话代理,支持文本和图片输入
- **AssetsService**: 数字资产管理
- **AdminService**: 管理员操作
## 运行项目 ## 运行项目

56
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@expo/metro-runtime": "~4.0.1", "@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4", "@expo/vector-icons": "~14.0.4",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
@@ -18,6 +19,7 @@
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",
"expo-font": "~13.0.4", "expo-font": "~13.0.4",
"expo-haptics": "~14.0.0", "expo-haptics": "~14.0.0",
"expo-image-picker": "^17.0.10",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~14.0.2",
"expo-status-bar": "~2.0.0", "expo-status-bar": "~2.0.0",
"react": "18.3.1", "react": "18.3.1",
@@ -3275,6 +3277,18 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.76.9", "version": "0.76.9",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
@@ -5722,6 +5736,27 @@
"expo": "*" "expo": "*"
} }
}, },
"node_modules/expo-image-loader": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
"integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-image-picker": {
"version": "17.0.10",
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
"integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
"license": "MIT",
"dependencies": {
"expo-image-loader": "~6.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-linear-gradient": { "node_modules/expo-linear-gradient": {
"version": "14.0.2", "version": "14.0.2",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz", "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
@@ -6712,6 +6747,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-plain-object": { "node_modules/is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -7591,6 +7635,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@expo/metro-runtime": "~4.0.1", "@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4", "@expo/vector-icons": "~14.0.4",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
@@ -19,6 +20,7 @@
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",
"expo-font": "~13.0.4", "expo-font": "~13.0.4",
"expo-haptics": "~14.0.0", "expo-haptics": "~14.0.0",
"expo-image-picker": "^17.0.10",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~14.0.2",
"expo-status-bar": "~2.0.0", "expo-status-bar": "~2.0.0",
"react": "18.3.1", "react": "18.3.1",

146
src/config/index.ts Normal file
View File

@@ -0,0 +1,146 @@
/**
* Application Configuration
*
* This file contains all configuration settings for the frontend application.
* Centralized configuration makes it easier to manage environment-specific settings.
*/
// =============================================================================
// Environment Configuration
// =============================================================================
/**
* Set to true to use mock data instead of real backend calls.
* Useful for development and testing without a running backend.
*/
export const NO_BACKEND_MODE = false;
/**
* Enable debug logging for API calls
*/
export const DEBUG_MODE = true;
// =============================================================================
// API Configuration
// =============================================================================
/**
* Base URL for the backend API server
*/
export const API_BASE_URL = 'http://localhost:8000';
/**
* API request timeout in milliseconds
*/
export const API_TIMEOUT = 30000;
/**
* API Endpoints
* All backend API routes are defined here for easy reference and maintenance.
*/
export const API_ENDPOINTS = {
// Authentication
AUTH: {
LOGIN: '/login',
REGISTER: '/register',
},
// Assets Management
ASSETS: {
GET: '/assets/get',
CREATE: '/assets/create',
CLAIM: '/assets/claim',
ASSIGN: '/assets/assign',
},
// AI Services
AI: {
PROXY: '/ai/proxy',
},
// Admin Operations
ADMIN: {
DECLARE_GUALE: '/admin/declare-guale',
},
} as const;
// =============================================================================
// Helper Functions
// =============================================================================
/**
* Build full API URL from endpoint
* @param endpoint - API endpoint path (e.g., '/login')
* @returns Full URL (e.g., 'http://192.168.56.103:8000/login')
*/
export function buildApiUrl(endpoint: string): string {
return `${API_BASE_URL}${endpoint}`;
}
/**
* Get default headers for API requests
* @param token - Optional JWT token for authenticated requests
* @returns Headers object
*/
export function getApiHeaders(token?: string): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
/**
* Log API debug information
* Only logs when DEBUG_MODE is enabled
*/
export function logApiDebug(label: string, data: unknown): void {
if (DEBUG_MODE) {
console.log(`[API Debug] ${label}:`, data);
}
}
// =============================================================================
// Mock User Configuration (for NO_BACKEND_MODE)
// =============================================================================
export const MOCK_CONFIG = {
USER: {
id: 999,
username: 'MockCaptain',
public_key: 'mock_public_key',
is_admin: true,
guale: false,
tier: 'premium',
tier_expires_at: '2026-12-31T23:59:59Z',
last_active_at: new Date().toISOString(),
},
ACCESS_TOKEN: 'mock_access_token',
RESPONSE_DELAY: 200, // milliseconds
} as const;
// =============================================================================
// AI Service Configuration
// =============================================================================
export const AI_CONFIG = {
/**
* Default system prompt for AI conversations
*/
DEFAULT_SYSTEM_PROMPT: 'You are a helpful journal assistant. Help the user reflect on their thoughts and feelings.',
/**
* Mock response delay in milliseconds (for NO_BACKEND_MODE)
*/
MOCK_RESPONSE_DELAY: 500,
} as const;
// =============================================================================
// Export Type Definitions
// =============================================================================
export type ApiEndpoint = typeof API_ENDPOINTS;

View File

@@ -1,66 +1,175 @@
import React, { createContext, useContext, useState, ReactNode } from 'react'; /**
* AuthContext - Authentication State Management
*
* Provides authentication state and methods throughout the app.
* Uses AsyncStorage for persistent login state.
*/
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { User, LoginRequest, RegisterRequest } from '../types'; import { User, LoginRequest, RegisterRequest } from '../types';
import { authService } from '../services/auth.service'; import { authService } from '../services/auth.service';
import { Alert } from 'react-native';
// =============================================================================
// Type Definitions
// =============================================================================
interface AuthContextType { interface AuthContextType {
user: User | null; user: User | null;
token: string | null; token: string | null;
isLoading: boolean; isLoading: boolean;
signIn: (credentials: LoginRequest) => Promise<void>; isInitializing: boolean;
signUp: (data: RegisterRequest) => Promise<void>; signIn: (credentials: LoginRequest) => Promise<void>;
signOut: () => void; signUp: (data: RegisterRequest) => Promise<void>;
signOut: () => void;
} }
// Storage keys
const STORAGE_KEYS = {
TOKEN: '@auth_token',
USER: '@auth_user',
};
// =============================================================================
// Context
// =============================================================================
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
// =============================================================================
// Provider Component
// =============================================================================
export function AuthProvider({ children }: { children: ReactNode }) { export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null); const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
const signIn = async (credentials: LoginRequest) => { // Load saved auth state on app start
setIsLoading(true); useEffect(() => {
try { loadStoredAuth();
const response = await authService.login(credentials); }, []);
setToken(response.access_token);
setUser(response.user);
} catch (error) {
throw error;
} finally {
setIsLoading(false);
}
};
const signUp = async (data: RegisterRequest) => { /**
setIsLoading(true); * Load stored authentication from AsyncStorage
try { */
await authService.register(data); const loadStoredAuth = async () => {
// After successful registration, sign in automatically try {
await signIn({ username: data.username, password: data.password }); const [storedToken, storedUser] = await Promise.all([
} catch (error) { AsyncStorage.getItem(STORAGE_KEYS.TOKEN),
throw error; AsyncStorage.getItem(STORAGE_KEYS.USER),
} finally { ]);
setIsLoading(false);
}
};
const signOut = () => { if (storedToken && storedUser) {
setUser(null); setToken(storedToken);
setToken(null); setUser(JSON.parse(storedUser));
}; console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
}
} catch (error) {
console.error('[Auth] Failed to load stored auth:', error);
} finally {
setIsInitializing(false);
}
};
return ( /**
<AuthContext.Provider value={{ user, token, isLoading, signIn, signUp, signOut }}> * Save authentication to AsyncStorage
{children} */
</AuthContext.Provider> const saveAuth = async (authToken: string, authUser: User) => {
); try {
await Promise.all([
AsyncStorage.setItem(STORAGE_KEYS.TOKEN, authToken),
AsyncStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(authUser)),
]);
console.log('[Auth] Session saved for user:', authUser.username);
} catch (error) {
console.error('[Auth] Failed to save auth:', error);
}
};
/**
* Clear authentication from AsyncStorage
*/
const clearAuth = async () => {
try {
await Promise.all([
AsyncStorage.removeItem(STORAGE_KEYS.TOKEN),
AsyncStorage.removeItem(STORAGE_KEYS.USER),
]);
console.log('[Auth] Session cleared');
} catch (error) {
console.error('[Auth] Failed to clear auth:', error);
}
};
/**
* Sign in with username and password
*/
const signIn = async (credentials: LoginRequest) => {
setIsLoading(true);
try {
const response = await authService.login(credentials);
setToken(response.access_token);
setUser(response.user);
await saveAuth(response.access_token, response.user);
} catch (error) {
throw error;
} finally {
setIsLoading(false);
}
};
/**
* Sign up and automatically sign in
*/
const signUp = async (data: RegisterRequest) => {
setIsLoading(true);
try {
await authService.register(data);
// After successful registration, sign in automatically
await signIn({ username: data.username, password: data.password });
} catch (error) {
throw error;
} finally {
setIsLoading(false);
}
};
/**
* Sign out and clear stored auth
*/
const signOut = () => {
setUser(null);
setToken(null);
clearAuth();
};
return (
<AuthContext.Provider
value={{
user,
token,
isLoading,
isInitializing,
signIn,
signUp,
signOut
}}
>
{children}
</AuthContext.Provider>
);
} }
// =============================================================================
// Hook
// =============================================================================
export function useAuth() { export function useAuth() {
const context = useContext(AuthContext); const context = useContext(AuthContext);
if (context === undefined) { if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider'); throw new Error('useAuth must be used within an AuthProvider');
} }
return context; return context;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
/**
* Admin Service
*
* Handles admin-only API operations.
*/
import {
NO_BACKEND_MODE,
API_ENDPOINTS,
MOCK_CONFIG,
buildApiUrl,
getApiHeaders,
logApiDebug,
} from '../config';
// =============================================================================
// Type Definitions
// =============================================================================
export interface DeclareGualeRequest {
username: string;
}
export interface DeclareGualeResponse {
message: string;
username: string;
guale: boolean;
}
// =============================================================================
// Admin Service
// =============================================================================
export const adminService = {
/**
* Declare a user as deceased (guale)
* Admin only operation
* @param request - Username to declare as deceased
* @param token - JWT token for authentication (must be admin)
* @returns Success response
*/
async declareGuale(request: DeclareGualeRequest, token: string): Promise<DeclareGualeResponse> {
if (NO_BACKEND_MODE) {
logApiDebug('Declare Guale', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({
message: `User ${request.username} has been declared as deceased`,
username: request.username,
guale: true,
});
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.ADMIN.DECLARE_GUALE);
logApiDebug('Declare Guale URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify(request),
});
logApiDebug('Declare Guale Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to declare user as deceased');
}
return await response.json();
} catch (error) {
console.error('Declare guale error:', error);
throw error;
}
},
};

243
src/services/ai.service.ts Normal file
View File

@@ -0,0 +1,243 @@
/**
* AI Service
*
* Handles communication with the AI proxy endpoint for chat completions.
*/
import {
NO_BACKEND_MODE,
API_ENDPOINTS,
AI_CONFIG,
buildApiUrl,
getApiHeaders,
logApiDebug,
} from '../config';
// =============================================================================
// Type Definitions
// =============================================================================
export interface AIMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface AIRequest {
messages: AIMessage[];
model?: string;
}
export interface AIResponse {
id: string;
object: string;
created: number;
model: string;
choices: Array<{
index: number;
message: AIMessage;
finish_reason: string;
}>;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
// =============================================================================
// Mock Response Generator
// =============================================================================
const createMockResponse = (userMessage: string): AIResponse => {
return {
id: `mock-${Date.now()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: 'mock-model',
choices: [
{
index: 0,
message: {
role: 'assistant',
content: `I received your message: "${userMessage}". This is a mock response since the backend is not connected.`,
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
},
};
};
// =============================================================================
// AI Service
// =============================================================================
export const aiService = {
/**
* Send chat messages to the AI proxy
* @param messages - Array of chat messages
* @param token - JWT token for authentication
* @returns AI response
*/
async chat(messages: AIMessage[], token?: string): Promise<AIResponse> {
if (NO_BACKEND_MODE) {
logApiDebug('AI Chat', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
const lastUserMessage = messages.filter((m) => m.role === 'user').pop();
resolve(createMockResponse(lastUserMessage?.content || 'Hello'));
}, AI_CONFIG.MOCK_RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
logApiDebug('AI Request', {
url,
hasToken: !!token,
messageCount: messages.length,
});
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify({ messages } as AIRequest),
});
logApiDebug('AI Response Status', response.status);
if (!response.ok) {
const errorText = await response.text();
logApiDebug('AI Error Response', errorText);
let errorDetail = 'AI request failed';
try {
const errorData = JSON.parse(errorText);
errorDetail = errorData.detail || errorDetail;
} catch {
errorDetail = errorText || errorDetail;
}
throw new Error(`${response.status}: ${errorDetail}`);
}
const data = await response.json();
logApiDebug('AI Success', {
id: data.id,
model: data.model,
choicesCount: data.choices?.length,
});
return data;
} catch (error) {
console.error('AI proxy error:', error);
throw error;
}
},
/**
* Simple helper for single message chat
* @param content - User message content
* @param token - JWT token for authentication
* @returns AI response text
*/
async sendMessage(content: string, token?: string): Promise<string> {
const messages: AIMessage[] = [
{
role: 'system',
content: AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
},
{
role: 'user',
content,
},
];
const response = await this.chat(messages, token);
return response.choices[0]?.message?.content || 'No response';
},
/**
* Send a message with an image to AI for analysis
* @param content - User message content
* @param imageBase64 - Base64 encoded image data
* @param token - JWT token for authentication
* @returns AI response text
*/
async sendMessageWithImage(content: string, imageBase64: string, token?: string): Promise<string> {
if (NO_BACKEND_MODE) {
logApiDebug('AI Image Analysis', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve('This is a mock image analysis response. The image appears to show an interesting scene. In production, this would be analyzed by Gemini AI.');
}, AI_CONFIG.MOCK_RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
logApiDebug('AI Image Request', {
url,
hasToken: !!token,
hasImage: !!imageBase64,
});
// Gemini vision format - using multimodal content
const messages = [
{
role: 'user',
content: [
{
type: 'text',
text: content,
},
{
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${imageBase64}`,
},
},
],
},
];
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify({ messages }),
});
logApiDebug('AI Image Response Status', response.status);
if (!response.ok) {
const errorText = await response.text();
logApiDebug('AI Image Error Response', errorText);
let errorDetail = 'AI image request failed';
try {
const errorData = JSON.parse(errorText);
errorDetail = errorData.detail || errorDetail;
} catch {
errorDetail = errorText || errorDetail;
}
throw new Error(`${response.status}: ${errorDetail}`);
}
const data = await response.json();
logApiDebug('AI Image Success', {
id: data.id,
model: data.model,
});
return data.choices[0]?.message?.content || 'No response';
} catch (error) {
console.error('AI image proxy error:', error);
throw error;
}
},
};

View File

@@ -0,0 +1,243 @@
/**
* Assets Service
*
* Handles all asset-related API operations including CRUD and inheritance.
*/
import {
NO_BACKEND_MODE,
API_ENDPOINTS,
MOCK_CONFIG,
buildApiUrl,
getApiHeaders,
logApiDebug,
} from '../config';
// =============================================================================
// Type Definitions
// =============================================================================
export interface Asset {
id: number;
title: string;
author_id: number;
private_key_shard: string;
content_outer_encrypted: string;
}
export interface AssetCreate {
title: string;
private_key_shard: string;
content_inner_encrypted: string;
}
export interface AssetClaim {
asset_id: number;
private_key_shard: string;
}
export interface AssetClaimResponse {
asset_id: number;
title: string;
decrypted_content: string;
server_shard_key: string;
}
export interface AssetAssign {
asset_id: number;
heir_name: string;
}
// =============================================================================
// Mock Data
// =============================================================================
const MOCK_ASSETS: Asset[] = [
{
id: 1,
title: 'Mock Asset 1',
author_id: MOCK_CONFIG.USER.id,
private_key_shard: 'mock_shard_1',
content_outer_encrypted: 'mock_encrypted_content_1',
},
{
id: 2,
title: 'Mock Asset 2',
author_id: MOCK_CONFIG.USER.id,
private_key_shard: 'mock_shard_2',
content_outer_encrypted: 'mock_encrypted_content_2',
},
];
// =============================================================================
// Assets Service
// =============================================================================
export const assetsService = {
/**
* Get all assets for the current user
* @param token - JWT token for authentication
* @returns Array of user's assets
*/
async getMyAssets(token: string): Promise<Asset[]> {
if (NO_BACKEND_MODE) {
logApiDebug('Get Assets', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => resolve(MOCK_ASSETS), MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.ASSETS.GET);
logApiDebug('Get Assets URL', url);
try {
const response = await fetch(url, {
method: 'GET',
headers: getApiHeaders(token),
});
logApiDebug('Get Assets Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to fetch assets');
}
return await response.json();
} catch (error) {
console.error('Get assets error:', error);
throw error;
}
},
/**
* Create a new asset
* @param asset - Asset creation data
* @param token - JWT token for authentication
* @returns Created asset
*/
async createAsset(asset: AssetCreate, token: string): Promise<Asset> {
if (NO_BACKEND_MODE) {
logApiDebug('Create Asset', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: Date.now(),
title: asset.title,
author_id: MOCK_CONFIG.USER.id,
private_key_shard: asset.private_key_shard,
content_outer_encrypted: asset.content_inner_encrypted,
});
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.ASSETS.CREATE);
logApiDebug('Create Asset URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify(asset),
});
logApiDebug('Create Asset Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to create asset');
}
return await response.json();
} catch (error) {
console.error('Create asset error:', error);
throw error;
}
},
/**
* Claim an inherited asset
* @param claim - Asset claim data
* @param token - JWT token for authentication
* @returns Claimed asset with decrypted content
*/
async claimAsset(claim: AssetClaim, token: string): Promise<AssetClaimResponse> {
if (NO_BACKEND_MODE) {
logApiDebug('Claim Asset', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({
asset_id: claim.asset_id,
title: 'Mock Claimed Asset',
decrypted_content: 'This is the decrypted content of the claimed asset.',
server_shard_key: 'mock_server_shard',
});
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.ASSETS.CLAIM);
logApiDebug('Claim Asset URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify(claim),
});
logApiDebug('Claim Asset Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to claim asset');
}
return await response.json();
} catch (error) {
console.error('Claim asset error:', error);
throw error;
}
},
/**
* Assign an asset to an heir
* @param assignment - Asset assignment data
* @param token - JWT token for authentication
* @returns Success message
*/
async assignAsset(assignment: AssetAssign, token: string): Promise<{ message: string }> {
if (NO_BACKEND_MODE) {
logApiDebug('Assign Asset', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({ message: `Asset assigned to ${assignment.heir_name}` });
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.ASSETS.ASSIGN);
logApiDebug('Assign Asset URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(token),
body: JSON.stringify(assignment),
});
logApiDebug('Assign Asset Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to assign asset');
}
return await response.json();
} catch (error) {
console.error('Assign asset error:', error);
throw error;
}
},
};

View File

@@ -1,80 +1,87 @@
import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types'; import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types';
import {
const PLATFORM_URL = 'http://192.168.56.103:8000'; NO_BACKEND_MODE,
const no_backend_mode = true; API_ENDPOINTS,
MOCK_CONFIG,
const MOCK_USER: User = { buildApiUrl,
id: 999, getApiHeaders,
username: 'MockCaptain', logApiDebug,
public_key: 'mock_public_key', } from '../config';
is_admin: true,
guale: false,
tier: 'premium',
tier_expires_at: '2026-12-31T23:59:59Z',
last_active_at: new Date().toISOString(),
};
export const authService = { export const authService = {
async login(credentials: LoginRequest): Promise<LoginResponse> { async login(credentials: LoginRequest): Promise<LoginResponse> {
if (no_backend_mode) { if (NO_BACKEND_MODE) {
console.log('No-Backend Mode: Simulating login...'); logApiDebug('Login', 'Using mock mode');
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve({ resolve({
access_token: 'mock_access_token', access_token: MOCK_CONFIG.ACCESS_TOKEN,
token_type: 'bearer', token_type: 'bearer',
user: { ...MOCK_USER, username: credentials.username }, user: { ...MOCK_CONFIG.USER, username: credentials.username } as User,
}); });
}, 200); }, MOCK_CONFIG.RESPONSE_DELAY);
}); });
} }
try {
const response = await fetch(`${PLATFORM_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
if (!response.ok) { const url = buildApiUrl(API_ENDPOINTS.AUTH.LOGIN);
throw new Error('Login failed'); logApiDebug('Login URL', url);
}
return await response.json(); try {
} catch (error) { const response = await fetch(url, {
console.error('Login error:', error); method: 'POST',
throw error; headers: getApiHeaders(),
} body: JSON.stringify(credentials),
}, });
async register(data: RegisterRequest): Promise<User> { logApiDebug('Login Response Status', response.status);
if (no_backend_mode) {
console.log('No-Backend Mode: Simulating registration...');
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...MOCK_USER, username: data.username });
}, 200);
});
}
try {
const response = await fetch(`${PLATFORM_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail?.[0]?.msg || 'Registration failed'); throw new Error(errorData.detail || 'Login failed');
} }
return await response.json(); const data = await response.json();
} catch (error) { logApiDebug('Login Success', { username: data.user?.username });
console.error('Registration error:', error); return data;
throw error; } catch (error) {
} console.error('Login error:', error);
}, throw error;
}
},
async register(data: RegisterRequest): Promise<User> {
if (NO_BACKEND_MODE) {
logApiDebug('Register', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({ ...MOCK_CONFIG.USER, username: data.username } as User);
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.AUTH.REGISTER);
logApiDebug('Register URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(),
body: JSON.stringify(data),
});
logApiDebug('Register Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail?.[0]?.msg || errorData.detail || 'Registration failed');
}
const result = await response.json();
logApiDebug('Register Success', { username: result.username });
return result;
} catch (error) {
console.error('Registration error:', error);
throw error;
}
},
}; };

25
src/services/index.ts Normal file
View File

@@ -0,0 +1,25 @@
/**
* Services Index
*
* Central export for all API services.
* Import services from here for cleaner imports.
*
* Usage:
* import { authService, aiService, assetsService, adminService } from '../services';
*/
export { authService } from './auth.service';
export { aiService, type AIMessage, type AIRequest, type AIResponse } from './ai.service';
export {
assetsService,
type Asset,
type AssetCreate,
type AssetClaim,
type AssetClaimResponse,
type AssetAssign
} from './assets.service';
export {
adminService,
type DeclareGualeRequest,
type DeclareGualeResponse
} from './admin.service';