6 Commits

Author SHA1 Message Date
d64a6557a8 The vault notification mode1 2026-01-31 22:58:51 -04:00
56bb72aab8 The vault notification mode1 2026-01-30 22:42:20 -04:00
749ed2f05a Merge tab 2026-01-29 22:51:10 -04:00
Ada
da4a7de0ad improve tab structure 2026-01-28 17:24:15 -08:00
Ada
146320052e update flow and auth model 2026-01-28 16:27:00 -08:00
lusixing
4d94888bb8 added login and register 2026-01-27 19:52:36 -08:00
20 changed files with 4585 additions and 677 deletions

70
App.tsx
View File

@@ -1,17 +1,65 @@
/**
* 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 { Buffer } from 'buffer';
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 } 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 { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors';
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
globalThis.Buffer = Buffer;
}
/**
* 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() {
const { user, isInitializing } = useAuth();
// Show loading screen while restoring auth state
if (isInitializing) {
return <LoadingScreen />;
}
return (
<NavigationContainer>
<StatusBar style="auto" />
{user ? <TabNavigator /> : <AuthNavigator />}
</NavigationContainer>
);
}
/**
* Root App component
*/
export default function App() { export default function App() {
return ( return (
<GestureHandlerRootView style={styles.container}> <GestureHandlerRootView style={styles.container}>
<NavigationContainer> <AuthProvider>
<StatusBar style="auto" /> <AppContent />
<TabNavigator /> </AuthProvider>
</NavigationContainer>
</GestureHandlerRootView> </GestureHandlerRootView>
); );
} }
@@ -19,5 +67,17 @@ export default function App() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#000',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
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**: 管理员操作
## 运行项目 ## 运行项目

192
package-lock.json generated
View File

@@ -10,13 +10,18 @@
"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",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"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",
@@ -26,6 +31,7 @@
"react-native-reanimated": "~3.16.1", "react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",
"react-native-view-shot": "^3.8.0",
"react-native-web": "~0.19.13" "react-native-web": "~0.19.13"
}, },
"devDependencies": { "devDependencies": {
@@ -3176,6 +3182,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3245,6 +3263,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",
@@ -3684,6 +3714,23 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/@react-navigation/native-stack": {
"version": "6.11.0",
"resolved": "https://registry.npmmirror.com/@react-navigation/native-stack/-/native-stack-6.11.0.tgz",
"integrity": "sha512-U5EcUB9Q2NQspCFwYGGNJm0h6wBCOv7T30QjndmvlawLkNt7S7KWbpWyxS9XBHSIKF57RgWjfxuJNTgTstpXxw==",
"license": "MIT",
"dependencies": {
"@react-navigation/elements": "^1.3.31",
"warn-once": "^0.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
}
},
"node_modules/@react-navigation/routers": { "node_modules/@react-navigation/routers": {
"version": "6.1.9", "version": "6.1.9",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
@@ -4311,6 +4358,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4378,6 +4434,15 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/bip39": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz",
"integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==",
"license": "ISC",
"dependencies": {
"@noble/hashes": "^1.2.0"
}
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@@ -4465,9 +4530,9 @@
} }
}, },
"node_modules/buffer": { "node_modules/buffer": {
"version": "5.7.1", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4485,7 +4550,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.1.13" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-alloc": { "node_modules/buffer-alloc": {
@@ -5097,6 +5162,15 @@
"hyphenate-style-name": "^1.0.3" "hyphenate-style-name": "^1.0.3"
} }
}, },
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -5678,6 +5752,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",
@@ -6359,6 +6454,19 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -6668,6 +6776,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",
@@ -7547,6 +7664,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",
@@ -9260,6 +9389,19 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-view-shot": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.8.0.tgz",
"integrity": "sha512-4cU8SOhMn3YQIrskh+5Q8VvVRxQOu8/s1M9NAL4z5BY1Rm0HXMWkQJ4N0XsZ42+Yca+y86ISF3LC5qdLPvPuiA==",
"license": "MIT",
"dependencies": {
"html2canvas": "^1.4.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-web": { "node_modules/react-native-web": {
"version": "0.19.13", "version": "0.19.13",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
@@ -10509,6 +10651,15 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/thenify": { "node_modules/thenify": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -10796,6 +10947,15 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -10898,6 +11058,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/whatwg-url-without-unicode/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",

View File

@@ -11,19 +11,25 @@
"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",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"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",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-native": "^0.76.9", "react-native": "^0.76.9",
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",
"react-native-view-shot": "^3.8.0",
"react-native-reanimated": "~3.16.1", "react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.4.0",

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

@@ -0,0 +1,147 @@
/**
* 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',
email: 'captain@sentinel.local',
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;

175
src/context/AuthContext.tsx Normal file
View File

@@ -0,0 +1,175 @@
/**
* 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 { authService } from '../services/auth.service';
// =============================================================================
// Type Definitions
// =============================================================================
interface AuthContextType {
user: User | null;
token: string | null;
isLoading: boolean;
isInitializing: boolean;
signIn: (credentials: LoginRequest) => Promise<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);
// =============================================================================
// Provider Component
// =============================================================================
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
// Load saved auth state on app start
useEffect(() => {
loadStoredAuth();
}, []);
/**
* Load stored authentication from AsyncStorage
*/
const loadStoredAuth = async () => {
try {
const [storedToken, storedUser] = await Promise.all([
AsyncStorage.getItem(STORAGE_KEYS.TOKEN),
AsyncStorage.getItem(STORAGE_KEYS.USER),
]);
if (storedToken && storedUser) {
setToken(storedToken);
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);
}
};
/**
* Save authentication to AsyncStorage
*/
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() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import LoginScreen from '../screens/LoginScreen';
import RegisterScreen from '../screens/RegisterScreen';
const Stack = createNativeStackNavigator();
export default function AuthNavigator() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Navigator>
);
}

View File

@@ -7,7 +7,7 @@ import { colors, borderRadius, typography } from '../theme/colors';
// Screens // Screens
import FlowScreen from '../screens/FlowScreen'; import FlowScreen from '../screens/FlowScreen';
import SentinelScreen from '../screens/SentinelScreen'; import SentinelScreen from '../screens/SentinelScreen';
import HeritageScreen from '../screens/HeritageScreen'; // import HeritageScreen from '../screens/HeritageScreen'; // Heritage functionality moved to Me and Sentinel
import MeScreen from '../screens/MeScreen'; import MeScreen from '../screens/MeScreen';
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
@@ -104,6 +104,7 @@ export default function TabNavigator() {
tabBarStyle: styles.tabBarDark, tabBarStyle: styles.tabBarDark,
}} }}
/> />
{/* Heritage tab commented out - functionality moved to Me (Fleet Legacy) and Sentinel (Shadow Vault)
<Tab.Screen <Tab.Screen
name="Heritage" name="Heritage"
component={HeritageScreen} component={HeritageScreen}
@@ -118,6 +119,7 @@ export default function TabNavigator() {
), ),
}} }}
/> />
*/}
<Tab.Screen <Tab.Screen
name="Me" name="Me"
component={MeScreen} component={MeScreen}

File diff suppressed because it is too large Load Diff

256
src/screens/LoginScreen.tsx Normal file
View File

@@ -0,0 +1,256 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
SafeAreaView,
Alert,
ActivityIndicator,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
import { useAuth } from '../context/AuthContext';
export default function LoginScreen({ navigation }: any) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { signIn, isLoading } = useAuth();
const handleLogin = async () => {
setError(null);
if (!email || !password) {
setError('Please enter both username and password.');
return;
}
try {
await signIn({ username: email, password });
} catch (err: any) {
setError('Login failed. Please check your credentials.');
}
};
return (
<View style={styles.container}>
<LinearGradient
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.content}
>
<View style={styles.header}>
<View style={styles.logoContainer}>
<MaterialCommunityIcons name="anchor" size={48} color={colors.sentinel.primary} />
</View>
<Text style={styles.title}>Welcome Aboard!</Text>
<Text style={styles.subtitle}>Sign in to access your sanctuaryy</Text>
{error && (
<View style={styles.errorContainer}>
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
<Text style={styles.errorText}>{error}</Text>
</View>
)}
</View>
<View style={styles.form}>
<View style={styles.inputContainer}>
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Username"
placeholderTextColor={colors.sentinel.textSecondary}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
/>
</View>
<View style={styles.inputContainer}>
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor={colors.sentinel.textSecondary}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
</View>
<TouchableOpacity style={styles.forgotButton}>
<Text style={styles.forgotText}>Forgot Password?</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.loginButton}
activeOpacity={0.8}
onPress={handleLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.loginButtonText}>Login</Text>
)}
</TouchableOpacity>
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>OR</Text>
<View style={styles.dividerLine} />
</View>
<TouchableOpacity
style={styles.registerLink}
onPress={() => navigation.navigate('Register')}
>
<Text style={styles.registerText}>
Don't have an account? <Text style={styles.registerHighlight}>Join the Fleet</Text>
</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradient: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
paddingHorizontal: spacing.lg,
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: spacing.xxl,
},
logoContainer: {
width: 80,
height: 80,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.2)',
},
title: {
fontSize: typography.fontSize.xxl,
fontWeight: 'bold',
color: colors.sentinel.text,
marginBottom: spacing.xs,
letterSpacing: 0.5,
},
subtitle: {
fontSize: typography.fontSize.base,
color: colors.sentinel.textSecondary,
marginBottom: spacing.md,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 68, 68, 0.1)',
padding: spacing.sm,
borderRadius: borderRadius.md,
marginTop: spacing.sm,
borderWidth: 1,
borderColor: 'rgba(255, 68, 68, 0.2)',
},
errorText: {
color: colors.sentinel.statusCritical,
fontSize: typography.fontSize.sm,
marginLeft: spacing.xs,
fontWeight: '500',
},
form: {
width: '100%',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.1)',
marginBottom: spacing.base,
height: 56,
paddingHorizontal: spacing.base,
},
inputIcon: {
marginRight: spacing.md,
},
input: {
flex: 1,
color: colors.sentinel.text,
fontSize: typography.fontSize.base,
},
forgotButton: {
alignSelf: 'flex-end',
marginBottom: spacing.lg,
},
forgotText: {
color: colors.sentinel.primary,
fontSize: typography.fontSize.sm,
},
loginButton: {
backgroundColor: colors.nautical.teal,
height: 56,
borderRadius: borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
...shadows.glow,
},
loginButtonText: {
color: colors.white,
fontSize: typography.fontSize.md,
fontWeight: 'bold',
letterSpacing: 0.5,
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: spacing.xl,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: 'rgba(184, 224, 229, 0.1)',
},
dividerText: {
color: colors.sentinel.textSecondary,
paddingHorizontal: spacing.md,
fontSize: typography.fontSize.sm,
},
registerLink: {
alignItems: 'center',
},
registerText: {
color: colors.sentinel.textSecondary,
fontSize: typography.fontSize.base,
},
registerHighlight: {
color: colors.sentinel.primary,
fontWeight: 'bold',
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,290 @@
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
Platform,
SafeAreaView,
ScrollView,
Alert,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
import { useAuth } from '../context/AuthContext';
import { ActivityIndicator } from 'react-native';
export default function RegisterScreen({ navigation }: any) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { signUp, isLoading } = useAuth();
const handleRegister = async () => {
setError(null);
if (!name || !email || !password || !confirmPassword) {
setError('Please fill in all fields.');
return;
}
if (password !== confirmPassword) {
setError('Passwords do not match.');
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError('Please enter a valid email address.');
return;
}
try {
await signUp({ username: name, email, password });
} catch (err: any) {
setError(err.message || 'Registration failed. Please try again.');
}
};
return (
<View style={styles.container}>
<LinearGradient
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.content}
>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Feather name="arrow-left" size={24} color={colors.sentinel.text} />
</TouchableOpacity>
<View style={styles.header}>
<View style={styles.logoContainer}>
<MaterialCommunityIcons name="compass-outline" size={48} color={colors.sentinel.primary} />
</View>
<Text style={styles.title}>Join the Fleet</Text>
<Text style={styles.subtitle}>Begin your journey with Sentinel</Text>
{error && (
<View style={styles.errorContainer}>
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
<Text style={styles.errorText}>{error}</Text>
</View>
)}
</View>
<View style={styles.form}>
<View style={styles.inputContainer}>
<Feather name="user" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Captain's Name"
placeholderTextColor={colors.sentinel.textSecondary}
value={name}
onChangeText={setName}
/>
</View>
<View style={styles.inputContainer}>
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Email"
placeholderTextColor={colors.sentinel.textSecondary}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
</View>
<View style={styles.inputContainer}>
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Password"
placeholderTextColor={colors.sentinel.textSecondary}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
</View>
<View style={styles.inputContainer}>
<Feather name="check-circle" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Confirm Password"
placeholderTextColor={colors.sentinel.textSecondary}
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
/>
</View>
<TouchableOpacity
style={[styles.registerButton, isLoading && styles.disabledButton]}
activeOpacity={0.8}
onPress={handleRegister}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.registerButtonText}>Create Account</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.loginLink}
onPress={() => navigation.navigate('Login')}
>
<Text style={styles.loginLinkText}>
Already have an account? <Text style={styles.highlight}>Login</Text>
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradient: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
paddingHorizontal: spacing.lg,
paddingBottom: spacing.xl,
},
backButton: {
marginTop: spacing.md,
marginBottom: spacing.lg,
width: 40,
height: 40,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
alignItems: 'center',
justifyContent: 'center',
},
header: {
alignItems: 'center',
marginBottom: spacing.xl,
},
logoContainer: {
width: 80,
height: 80,
borderRadius: borderRadius.full,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.2)',
},
title: {
fontSize: typography.fontSize.xxl,
fontWeight: 'bold',
color: colors.sentinel.text,
marginBottom: spacing.xs,
letterSpacing: 0.5,
},
subtitle: {
fontSize: typography.fontSize.base,
color: colors.sentinel.textSecondary,
},
form: {
width: '100%',
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: 'rgba(184, 224, 229, 0.1)',
marginBottom: spacing.base,
height: 56,
paddingHorizontal: spacing.base,
},
inputIcon: {
marginRight: spacing.md,
},
input: {
flex: 1,
color: colors.sentinel.text,
fontSize: typography.fontSize.base,
},
registerButton: {
backgroundColor: colors.nautical.seafoam,
height: 56,
borderRadius: borderRadius.lg,
alignItems: 'center',
justifyContent: 'center',
marginTop: spacing.md,
...shadows.glow,
},
registerButtonText: {
color: colors.white,
fontSize: typography.fontSize.md,
fontWeight: 'bold',
letterSpacing: 0.5,
},
loginLink: {
alignItems: 'center',
marginTop: spacing.xl,
},
loginLinkText: {
color: colors.sentinel.textSecondary,
fontSize: typography.fontSize.base,
},
highlight: {
color: colors.nautical.seafoam,
fontWeight: 'bold',
},
disabledButton: {
opacity: 0.7,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 68, 68, 0.1)',
padding: spacing.sm,
borderRadius: borderRadius.md,
marginTop: spacing.md,
borderWidth: 1,
borderColor: 'rgba(255, 68, 68, 0.2)',
},
errorText: {
color: colors.sentinel.statusCritical,
fontSize: typography.fontSize.sm,
marginLeft: spacing.xs,
fontWeight: '500',
},
});

View File

@@ -7,11 +7,13 @@ import {
TouchableOpacity, TouchableOpacity,
SafeAreaView, SafeAreaView,
Animated, Animated,
Modal,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { SystemStatus, KillSwitchLog } from '../types'; import { SystemStatus, KillSwitchLog } from '../types';
import VaultScreen from './VaultScreen';
// Status configuration with nautical theme // Status configuration with nautical theme
const statusConfig: Record<SystemStatus, { const statusConfig: Record<SystemStatus, {
@@ -76,6 +78,7 @@ export default function SentinelScreen() {
const [pulseAnim] = useState(new Animated.Value(1)); const [pulseAnim] = useState(new Animated.Value(1));
const [glowAnim] = useState(new Animated.Value(0.5)); const [glowAnim] = useState(new Animated.Value(0.5));
const [rotateAnim] = useState(new Animated.Value(0)); const [rotateAnim] = useState(new Animated.Value(0));
const [showVault, setShowVault] = useState(false);
useEffect(() => { useEffect(() => {
// Pulse animation // Pulse animation
@@ -120,6 +123,10 @@ export default function SentinelScreen() {
).start(); ).start();
}, []); }, []);
const openVault = () => {
setShowVault(true);
};
const handleHeartbeat = () => { const handleHeartbeat = () => {
// Animate pulse // Animate pulse
Animated.sequence([ Animated.sequence([
@@ -270,6 +277,26 @@ export default function SentinelScreen() {
</View> </View>
</View> </View>
{/* Shadow Vault Access */}
<View style={styles.vaultAccessCard}>
<View style={styles.vaultAccessIcon}>
<MaterialCommunityIcons name="treasure-chest" size={22} color={colors.nautical.teal} />
</View>
<View style={styles.vaultAccessContent}>
<Text style={styles.vaultAccessTitle}>Shadow Vault</Text>
<Text style={styles.vaultAccessText}>
Access sealed assets from the Lighthouse.
</Text>
</View>
<TouchableOpacity
style={styles.vaultAccessButton}
onPress={openVault}
activeOpacity={0.8}
>
<Text style={styles.vaultAccessButtonText}>Open</Text>
</TouchableOpacity>
</View>
{/* Heartbeat Button */} {/* Heartbeat Button */}
<TouchableOpacity <TouchableOpacity
style={styles.heartbeatButton} style={styles.heartbeatButton}
@@ -313,6 +340,25 @@ export default function SentinelScreen() {
</ScrollView> </ScrollView>
</SafeAreaView> </SafeAreaView>
</LinearGradient> </LinearGradient>
{/* Vault Modal */}
<Modal
visible={showVault}
animationType="slide"
onRequestClose={() => setShowVault(false)}
>
<View style={styles.vaultModalContainer}>
{showVault ? <VaultScreen /> : null}
<TouchableOpacity
style={styles.vaultCloseButton}
onPress={() => setShowVault(false)}
activeOpacity={0.85}
>
<Ionicons name="close" size={20} color={colors.nautical.cream} />
</TouchableOpacity>
</View>
</Modal>
</View> </View>
); );
} }
@@ -513,4 +559,64 @@ const styles = StyleSheet.create({
color: colors.sentinel.textSecondary, color: colors.sentinel.textSecondary,
fontFamily: typography.fontFamily.mono, fontFamily: typography.fontFamily.mono,
}, },
// Shadow Vault Access Card
vaultAccessCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.xl,
padding: spacing.base,
marginBottom: spacing.lg,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
vaultAccessIcon: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: `${colors.nautical.teal}20`,
alignItems: 'center',
justifyContent: 'center',
marginRight: spacing.md,
},
vaultAccessContent: {
flex: 1,
},
vaultAccessTitle: {
fontSize: typography.fontSize.base,
fontWeight: '600',
color: colors.sentinel.text,
marginBottom: 2,
},
vaultAccessText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.textSecondary,
},
vaultAccessButton: {
backgroundColor: colors.nautical.teal,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
},
vaultAccessButtonText: {
color: colors.nautical.cream,
fontWeight: '700',
fontSize: typography.fontSize.sm,
},
// Vault Modal
vaultModalContainer: {
flex: 1,
backgroundColor: colors.vault.background,
},
vaultCloseButton: {
position: 'absolute',
top: spacing.xl + spacing.lg,
right: spacing.lg,
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(26, 58, 74, 0.65)',
alignItems: 'center',
justifyContent: 'center',
},
}); });

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

@@ -0,0 +1,87 @@
import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types';
import {
NO_BACKEND_MODE,
API_ENDPOINTS,
MOCK_CONFIG,
buildApiUrl,
getApiHeaders,
logApiDebug,
} from '../config';
export const authService = {
async login(credentials: LoginRequest): Promise<LoginResponse> {
if (NO_BACKEND_MODE) {
logApiDebug('Login', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({
access_token: MOCK_CONFIG.ACCESS_TOKEN,
token_type: 'bearer',
user: { ...MOCK_CONFIG.USER, username: credentials.username } as User,
});
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}
const url = buildApiUrl(API_ENDPOINTS.AUTH.LOGIN);
logApiDebug('Login URL', url);
try {
const response = await fetch(url, {
method: 'POST',
headers: getApiHeaders(),
body: JSON.stringify(credentials),
});
logApiDebug('Login Response Status', response.status);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Login failed');
}
const data = await response.json();
logApiDebug('Login Success', { username: data.user?.username });
return data;
} 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';

View File

@@ -13,7 +13,7 @@ export interface FlowRecord {
} }
// Vault Types // Vault Types
export type VaultAssetType = export type VaultAssetType =
| 'game_account' | 'game_account'
| 'private_key' | 'private_key'
| 'document' | 'document'
@@ -72,3 +72,33 @@ export interface ProtocolInfo {
version: string; version: string;
lastUpdated: Date; lastUpdated: Date;
} }
// Auth Types
export interface User {
id: number;
username: string;
email?: string;
public_key: string;
is_admin: boolean;
guale: boolean;
tier: string;
tier_expires_at: string;
last_active_at: string;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface RegisterRequest {
username: string;
password: string;
email: string;
}
export interface LoginResponse {
access_token: string;
token_type: string;
user: User;
}