qd
Browse files- keystone-app/app.json +6 -2
- keystone-app/app/(tabs)/index.tsx +12 -9
- keystone-app/package-lock.json +5 -27
- keystone-app/package.json +2 -2
- keystone-backend/main.py +10 -6
keystone-app/app.json
CHANGED
|
@@ -16,10 +16,14 @@
|
|
| 16 |
"photosPermission": "Allow KeyStone to access your photos and videos to back them up to your private cloud.",
|
| 17 |
"savePhotosPermission": "Allow KeyStone to save photos.",
|
| 18 |
"isAccessMediaLocationEnabled": true,
|
| 19 |
-
"granularPermissions": [
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
],
|
| 22 |
-
"expo-router"
|
|
|
|
| 23 |
],
|
| 24 |
"extra": {
|
| 25 |
"eas": {
|
|
|
|
| 16 |
"photosPermission": "Allow KeyStone to access your photos and videos to back them up to your private cloud.",
|
| 17 |
"savePhotosPermission": "Allow KeyStone to save photos.",
|
| 18 |
"isAccessMediaLocationEnabled": true,
|
| 19 |
+
"granularPermissions": [
|
| 20 |
+
"photo",
|
| 21 |
+
"video"
|
| 22 |
+
]
|
| 23 |
}
|
| 24 |
],
|
| 25 |
+
"expo-router",
|
| 26 |
+
"expo-font"
|
| 27 |
],
|
| 28 |
"extra": {
|
| 29 |
"eas": {
|
keystone-app/app/(tabs)/index.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import { Image } from 'expo-image';
|
|
| 19 |
import * as FileSystem from 'expo-file-system/legacy';
|
| 20 |
import { Video, ResizeMode } from 'expo-av';
|
| 21 |
import * as Sharing from 'expo-sharing';
|
| 22 |
-
import
|
| 23 |
import { StatusBar } from 'expo-status-bar';
|
| 24 |
import { MaterialIcons } from '@expo/vector-icons';
|
| 25 |
import { format, isToday, isYesterday } from 'date-fns';
|
|
@@ -70,7 +70,7 @@ export default function PhotosScreen() {
|
|
| 70 |
const [syncProgress, setSyncProgress] = useState(0);
|
| 71 |
const [syncTotal, setSyncTotal] = useState(0);
|
| 72 |
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'done'>('idle');
|
| 73 |
-
const [netInfo, setNetInfo] = useState<
|
| 74 |
const [allowCellular, setAllowCellular] = useState(false);
|
| 75 |
const [itemsToBackup, setItemsToBackup] = useState(0);
|
| 76 |
|
|
@@ -80,10 +80,13 @@ export default function PhotosScreen() {
|
|
| 80 |
// ── Network & Settings Listeners ──────────────────────────────
|
| 81 |
useEffect(() => {
|
| 82 |
// Listen to network changes
|
| 83 |
-
const
|
| 84 |
setNetInfo(state);
|
| 85 |
});
|
| 86 |
|
|
|
|
|
|
|
|
|
|
| 87 |
// Load cellular setting
|
| 88 |
const loadSetting = async () => {
|
| 89 |
try {
|
|
@@ -101,7 +104,7 @@ export default function PhotosScreen() {
|
|
| 101 |
});
|
| 102 |
|
| 103 |
return () => {
|
| 104 |
-
|
| 105 |
subscription.remove();
|
| 106 |
};
|
| 107 |
}, []);
|
|
@@ -193,7 +196,7 @@ export default function PhotosScreen() {
|
|
| 193 |
return;
|
| 194 |
}
|
| 195 |
|
| 196 |
-
if (netInfo?.type ===
|
| 197 |
Alert.alert('Waiting for Wi-Fi', 'Syncing on mobile data is disabled in Settings.');
|
| 198 |
return;
|
| 199 |
}
|
|
@@ -221,12 +224,12 @@ export default function PhotosScreen() {
|
|
| 221 |
try {
|
| 222 |
const info = await MediaLibrary.getAssetInfoAsync(asset);
|
| 223 |
const localUri = info.localUri || asset.uri;
|
| 224 |
-
const resp = await FileSystem.uploadAsync(`${BACKEND_URL}/upload`, localUri, {
|
| 225 |
fieldName: 'file',
|
| 226 |
httpMethod: 'POST',
|
| 227 |
uploadType: FileSystem.FileSystemUploadType.MULTIPART,
|
| 228 |
parameters: { creation_time: asset.creationTime.toString() },
|
| 229 |
-
headers: {
|
| 230 |
});
|
| 231 |
if (resp.status === 200) {
|
| 232 |
synced = await addSyncedId(asset.id, synced);
|
|
@@ -318,7 +321,7 @@ export default function PhotosScreen() {
|
|
| 318 |
return { text: 'Offline', icon: 'cloud-off', color: '#d93025', spinning: false };
|
| 319 |
}
|
| 320 |
|
| 321 |
-
if (netInfo.type ===
|
| 322 |
return { text: 'Waiting for Wi-Fi', icon: 'cloud-sync', color: '#f29900', spinning: false };
|
| 323 |
}
|
| 324 |
|
|
@@ -447,7 +450,7 @@ export default function PhotosScreen() {
|
|
| 447 |
<TouchableOpacity
|
| 448 |
style={styles.syncIconContainer}
|
| 449 |
onPress={syncAssets}
|
| 450 |
-
disabled={isSyncing || (!netInfo?.isConnected) || (netInfo?.type ===
|
| 451 |
>
|
| 452 |
{statusUI.spinning ? (
|
| 453 |
<ActivityIndicator size="small" color={statusUI.color} />
|
|
|
|
| 19 |
import * as FileSystem from 'expo-file-system/legacy';
|
| 20 |
import { Video, ResizeMode } from 'expo-av';
|
| 21 |
import * as Sharing from 'expo-sharing';
|
| 22 |
+
import * as Network from 'expo-network';
|
| 23 |
import { StatusBar } from 'expo-status-bar';
|
| 24 |
import { MaterialIcons } from '@expo/vector-icons';
|
| 25 |
import { format, isToday, isYesterday } from 'date-fns';
|
|
|
|
| 70 |
const [syncProgress, setSyncProgress] = useState(0);
|
| 71 |
const [syncTotal, setSyncTotal] = useState(0);
|
| 72 |
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'done'>('idle');
|
| 73 |
+
const [netInfo, setNetInfo] = useState<Network.NetworkState | null>(null);
|
| 74 |
const [allowCellular, setAllowCellular] = useState(false);
|
| 75 |
const [itemsToBackup, setItemsToBackup] = useState(0);
|
| 76 |
|
|
|
|
| 80 |
// ── Network & Settings Listeners ──────────────────────────────
|
| 81 |
useEffect(() => {
|
| 82 |
// Listen to network changes
|
| 83 |
+
const subscriptionNet = Network.addNetworkStateListener(state => {
|
| 84 |
setNetInfo(state);
|
| 85 |
});
|
| 86 |
|
| 87 |
+
// Also get initial state
|
| 88 |
+
Network.getNetworkStateAsync().then(setNetInfo).catch(() => {});
|
| 89 |
+
|
| 90 |
// Load cellular setting
|
| 91 |
const loadSetting = async () => {
|
| 92 |
try {
|
|
|
|
| 104 |
});
|
| 105 |
|
| 106 |
return () => {
|
| 107 |
+
subscriptionNet.remove();
|
| 108 |
subscription.remove();
|
| 109 |
};
|
| 110 |
}, []);
|
|
|
|
| 196 |
return;
|
| 197 |
}
|
| 198 |
|
| 199 |
+
if (netInfo?.type === Network.NetworkStateType.CELLULAR && !allowCellular) {
|
| 200 |
Alert.alert('Waiting for Wi-Fi', 'Syncing on mobile data is disabled in Settings.');
|
| 201 |
return;
|
| 202 |
}
|
|
|
|
| 224 |
try {
|
| 225 |
const info = await MediaLibrary.getAssetInfoAsync(asset);
|
| 226 |
const localUri = info.localUri || asset.uri;
|
| 227 |
+
const resp = await FileSystem.uploadAsync(`${BACKEND_URL}/upload?token=${MOCK_TOKEN}`, localUri, {
|
| 228 |
fieldName: 'file',
|
| 229 |
httpMethod: 'POST',
|
| 230 |
uploadType: FileSystem.FileSystemUploadType.MULTIPART,
|
| 231 |
parameters: { creation_time: asset.creationTime.toString() },
|
| 232 |
+
headers: { authorization: `Bearer ${MOCK_TOKEN}` },
|
| 233 |
});
|
| 234 |
if (resp.status === 200) {
|
| 235 |
synced = await addSyncedId(asset.id, synced);
|
|
|
|
| 321 |
return { text: 'Offline', icon: 'cloud-off', color: '#d93025', spinning: false };
|
| 322 |
}
|
| 323 |
|
| 324 |
+
if (netInfo.type === Network.NetworkStateType.CELLULAR && !allowCellular && itemsToBackup > 0) {
|
| 325 |
return { text: 'Waiting for Wi-Fi', icon: 'cloud-sync', color: '#f29900', spinning: false };
|
| 326 |
}
|
| 327 |
|
|
|
|
| 450 |
<TouchableOpacity
|
| 451 |
style={styles.syncIconContainer}
|
| 452 |
onPress={syncAssets}
|
| 453 |
+
disabled={isSyncing || (!netInfo?.isConnected) || (netInfo?.type === Network.NetworkStateType.CELLULAR && !allowCellular) || itemsToBackup === 0}
|
| 454 |
>
|
| 455 |
{statusUI.spinning ? (
|
| 456 |
<ActivityIndicator size="small" color={statusUI.color} />
|
keystone-app/package-lock.json
CHANGED
|
@@ -9,14 +9,14 @@
|
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@expo/metro-runtime": "~6.1.2",
|
| 12 |
-
"@expo/vector-icons": "^15.
|
| 13 |
"@react-native-async-storage/async-storage": "2.2.0",
|
| 14 |
-
"@react-native-community/netinfo": "11.4.1",
|
| 15 |
"date-fns": "^4.4.0",
|
| 16 |
"expo": "^54.0.36",
|
| 17 |
"expo-av": "~16.0.8",
|
| 18 |
"expo-dev-client": "~6.0.21",
|
| 19 |
"expo-file-system": "~19.0.23",
|
|
|
|
| 20 |
"expo-image": "~3.0.11",
|
| 21 |
"expo-linear-gradient": "~15.0.8",
|
| 22 |
"expo-media-library": "~18.2.1",
|
|
@@ -2762,15 +2762,6 @@
|
|
| 2762 |
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
| 2763 |
}
|
| 2764 |
},
|
| 2765 |
-
"node_modules/@react-native-community/netinfo": {
|
| 2766 |
-
"version": "11.4.1",
|
| 2767 |
-
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
|
| 2768 |
-
"integrity": "sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==",
|
| 2769 |
-
"license": "MIT",
|
| 2770 |
-
"peerDependencies": {
|
| 2771 |
-
"react-native": ">=0.59"
|
| 2772 |
-
}
|
| 2773 |
-
},
|
| 2774 |
"node_modules/@react-native/assets-registry": {
|
| 2775 |
"version": "0.81.5",
|
| 2776 |
"license": "MIT",
|
|
@@ -4587,11 +4578,10 @@
|
|
| 4587 |
}
|
| 4588 |
},
|
| 4589 |
"node_modules/expo-font": {
|
| 4590 |
-
"version": "
|
| 4591 |
-
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-
|
| 4592 |
-
"integrity": "sha512-
|
| 4593 |
"license": "MIT",
|
| 4594 |
-
"peer": true,
|
| 4595 |
"dependencies": {
|
| 4596 |
"fontfaceobserver": "^2.1.0"
|
| 4597 |
},
|
|
@@ -5358,18 +5348,6 @@
|
|
| 5358 |
"react-native": "*"
|
| 5359 |
}
|
| 5360 |
},
|
| 5361 |
-
"node_modules/expo/node_modules/expo-font": {
|
| 5362 |
-
"version": "14.0.12",
|
| 5363 |
-
"license": "MIT",
|
| 5364 |
-
"dependencies": {
|
| 5365 |
-
"fontfaceobserver": "^2.1.0"
|
| 5366 |
-
},
|
| 5367 |
-
"peerDependencies": {
|
| 5368 |
-
"expo": "*",
|
| 5369 |
-
"react": "*",
|
| 5370 |
-
"react-native": "*"
|
| 5371 |
-
}
|
| 5372 |
-
},
|
| 5373 |
"node_modules/expo/node_modules/expo-keep-awake": {
|
| 5374 |
"version": "15.0.8",
|
| 5375 |
"license": "MIT",
|
|
|
|
| 9 |
"version": "1.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
"@expo/metro-runtime": "~6.1.2",
|
| 12 |
+
"@expo/vector-icons": "^15.0.3",
|
| 13 |
"@react-native-async-storage/async-storage": "2.2.0",
|
|
|
|
| 14 |
"date-fns": "^4.4.0",
|
| 15 |
"expo": "^54.0.36",
|
| 16 |
"expo-av": "~16.0.8",
|
| 17 |
"expo-dev-client": "~6.0.21",
|
| 18 |
"expo-file-system": "~19.0.23",
|
| 19 |
+
"expo-font": "~14.0.12",
|
| 20 |
"expo-image": "~3.0.11",
|
| 21 |
"expo-linear-gradient": "~15.0.8",
|
| 22 |
"expo-media-library": "~18.2.1",
|
|
|
|
| 2762 |
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
| 2763 |
}
|
| 2764 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2765 |
"node_modules/@react-native/assets-registry": {
|
| 2766 |
"version": "0.81.5",
|
| 2767 |
"license": "MIT",
|
|
|
|
| 4578 |
}
|
| 4579 |
},
|
| 4580 |
"node_modules/expo-font": {
|
| 4581 |
+
"version": "14.0.12",
|
| 4582 |
+
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.12.tgz",
|
| 4583 |
+
"integrity": "sha512-QQzunE2Mxk45AsCWm3tK7OpVljbtVnKD58q4/qliev+cbye1IOduUnRIdD+P7DyButw17G9MTX795kgaQiz5hQ==",
|
| 4584 |
"license": "MIT",
|
|
|
|
| 4585 |
"dependencies": {
|
| 4586 |
"fontfaceobserver": "^2.1.0"
|
| 4587 |
},
|
|
|
|
| 5348 |
"react-native": "*"
|
| 5349 |
}
|
| 5350 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5351 |
"node_modules/expo/node_modules/expo-keep-awake": {
|
| 5352 |
"version": "15.0.8",
|
| 5353 |
"license": "MIT",
|
keystone-app/package.json
CHANGED
|
@@ -4,14 +4,14 @@
|
|
| 4 |
"main": "expo-router/entry",
|
| 5 |
"dependencies": {
|
| 6 |
"@expo/metro-runtime": "~6.1.2",
|
| 7 |
-
"@expo/vector-icons": "^15.
|
| 8 |
"@react-native-async-storage/async-storage": "2.2.0",
|
| 9 |
-
"@react-native-community/netinfo": "11.4.1",
|
| 10 |
"date-fns": "^4.4.0",
|
| 11 |
"expo": "^54.0.36",
|
| 12 |
"expo-av": "~16.0.8",
|
| 13 |
"expo-dev-client": "~6.0.21",
|
| 14 |
"expo-file-system": "~19.0.23",
|
|
|
|
| 15 |
"expo-image": "~3.0.11",
|
| 16 |
"expo-linear-gradient": "~15.0.8",
|
| 17 |
"expo-media-library": "~18.2.1",
|
|
|
|
| 4 |
"main": "expo-router/entry",
|
| 5 |
"dependencies": {
|
| 6 |
"@expo/metro-runtime": "~6.1.2",
|
| 7 |
+
"@expo/vector-icons": "^15.0.3",
|
| 8 |
"@react-native-async-storage/async-storage": "2.2.0",
|
|
|
|
| 9 |
"date-fns": "^4.4.0",
|
| 10 |
"expo": "^54.0.36",
|
| 11 |
"expo-av": "~16.0.8",
|
| 12 |
"expo-dev-client": "~6.0.21",
|
| 13 |
"expo-file-system": "~19.0.23",
|
| 14 |
+
"expo-font": "~14.0.12",
|
| 15 |
"expo-image": "~3.0.11",
|
| 16 |
"expo-linear-gradient": "~15.0.8",
|
| 17 |
"expo-media-library": "~18.2.1",
|
keystone-backend/main.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import os
|
| 2 |
from datetime import datetime
|
| 3 |
-
from fastapi import FastAPI, HTTPException, Header, Depends, UploadFile, File, Form
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from pydantic import BaseModel
|
| 6 |
from dotenv import load_dotenv
|
|
@@ -59,11 +59,15 @@ class LoginPayload(BaseModel):
|
|
| 59 |
username: str
|
| 60 |
password: str
|
| 61 |
|
| 62 |
-
def verify_mock_token(authorization: str = Header(None)):
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
if token != MOCK_TOKEN:
|
| 68 |
raise HTTPException(status_code=401, detail="Unauthorized: Invalid Token")
|
| 69 |
return token
|
|
|
|
| 1 |
import os
|
| 2 |
from datetime import datetime
|
| 3 |
+
from fastapi import FastAPI, HTTPException, Header, Depends, UploadFile, File, Form, Request
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
from pydantic import BaseModel
|
| 6 |
from dotenv import load_dotenv
|
|
|
|
| 59 |
username: str
|
| 60 |
password: str
|
| 61 |
|
| 62 |
+
def verify_mock_token(request: Request, authorization: str = Header(None)):
|
| 63 |
+
token = None
|
| 64 |
+
if authorization and authorization.startswith("Bearer "):
|
| 65 |
+
token = authorization.split(" ")[1]
|
| 66 |
+
|
| 67 |
+
# Fallback to query parameter (useful if proxy strips Authorization header)
|
| 68 |
+
if not token:
|
| 69 |
+
token = request.query_params.get("token")
|
| 70 |
+
|
| 71 |
if token != MOCK_TOKEN:
|
| 72 |
raise HTTPException(status_code=401, detail="Unauthorized: Invalid Token")
|
| 73 |
return token
|