Spaces:
Sleeping
Sleeping
v.2
Browse files- .gitignore +1 -2
- DEPLOYMENT.md +15 -16
- client/.env +1 -0
- client/src/pages/Login.tsx +4 -2
- client/src/pages/Settings.tsx +37 -6
- server/.env +6 -0
- server/Dockerfile +24 -0
- server/scripts/test-db.ts +8 -2
- server/src/db.ts +21 -6
- server/src/index.ts +7 -11
- server/test-delete.ts +35 -0
.gitignore
CHANGED
|
@@ -12,8 +12,7 @@ yarn-error.log*
|
|
| 12 |
backup.xlsx
|
| 13 |
|
| 14 |
# Environment variables
|
| 15 |
-
.env
|
| 16 |
-
.env.local
|
| 17 |
.env.development.local
|
| 18 |
.env.test.local
|
| 19 |
.env.production.local
|
|
|
|
| 12 |
backup.xlsx
|
| 13 |
|
| 14 |
# Environment variables
|
| 15 |
+
# .env and .env.local removed from gitignore as per user request for private repo.
|
|
|
|
| 16 |
.env.development.local
|
| 17 |
.env.test.local
|
| 18 |
.env.production.local
|
DEPLOYMENT.md
CHANGED
|
@@ -18,30 +18,29 @@ Follow these steps to host your application for free on the web.
|
|
| 18 |
3. Get your **Database URL** and **Auth Token**.
|
| 19 |
4. You will use these in the next step.
|
| 20 |
|
| 21 |
-
## 3. Backend (
|
| 22 |
-
1. Go to [
|
| 23 |
-
2.
|
| 24 |
-
3.
|
| 25 |
-
4.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
- **Start Command**: `cd server && npx tsx src/index.ts`
|
| 29 |
-
5. Add **Environment Variables**:
|
| 30 |
- `DATABASE_URL`: (From Turso)
|
| 31 |
- `DATABASE_AUTH_TOKEN`: (From Turso)
|
| 32 |
-
- `JWT_SECRET`: (Any random string
|
| 33 |
-
- `EMAIL_USER`: (Your Gmail
|
| 34 |
-
- `EMAIL_PASS`: (
|
| 35 |
-
- `EMAIL_TO`: (
|
| 36 |
-
|
|
|
|
| 37 |
|
| 38 |
## 4. Frontend (Vercel)
|
| 39 |
-
1. Go to [Vercel.com](https://vercel.com) and sign up.
|
| 40 |
2. Create a new **Project**.
|
| 41 |
3. Connect your GitHub repository.
|
| 42 |
4. Set the **Root Directory** to `client`.
|
| 43 |
5. Add **Environment Variable**:
|
| 44 |
-
- `VITE_API_URL`: `https://your-
|
| 45 |
6. Deploy!
|
| 46 |
|
| 47 |
## 5. Mobile Access
|
|
|
|
| 18 |
3. Get your **Database URL** and **Auth Token**.
|
| 19 |
4. You will use these in the next step.
|
| 20 |
|
| 21 |
+
## 3. Backend (Hugging Face Spaces)
|
| 22 |
+
1. Go to [huggingface.co](https://huggingface.co) and sign up (Free, **no credit card**).
|
| 23 |
+
2. Click **New** -> **Space**.
|
| 24 |
+
3. Name it (e.g., `wallets-api`).
|
| 25 |
+
4. Select **Docker** as the SDK.
|
| 26 |
+
5. Go to **Settings** -> **Variables and secrets**.
|
| 27 |
+
6. Add the following **Secrets**:
|
|
|
|
|
|
|
| 28 |
- `DATABASE_URL`: (From Turso)
|
| 29 |
- `DATABASE_AUTH_TOKEN`: (From Turso)
|
| 30 |
+
- `JWT_SECRET`: (Any random string)
|
| 31 |
+
- `EMAIL_USER`: (Your Gmail)
|
| 32 |
+
- `EMAIL_PASS`: (Gmail App Password)
|
| 33 |
+
- `EMAIL_TO`: (Your email)
|
| 34 |
+
7. Push your code to the Space (you can connect it to your GitHub repo in the Settings tab).
|
| 35 |
+
8. Hugging Face will give you a URL like `https://name-space.hf.space`.
|
| 36 |
|
| 37 |
## 4. Frontend (Vercel)
|
| 38 |
+
1. Go to [Vercel.com](https://vercel.com) and sign up (Free, **no credit card**).
|
| 39 |
2. Create a new **Project**.
|
| 40 |
3. Connect your GitHub repository.
|
| 41 |
4. Set the **Root Directory** to `client`.
|
| 42 |
5. Add **Environment Variable**:
|
| 43 |
+
- `VITE_API_URL`: `https://your-huggingface-url.hf.space/api`
|
| 44 |
6. Deploy!
|
| 45 |
|
| 46 |
## 5. Mobile Access
|
client/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
VITE_API_URL=http://localhost:3001/api
|
client/src/pages/Login.tsx
CHANGED
|
@@ -4,6 +4,8 @@ import { useNavigate } from 'react-router-dom';
|
|
| 4 |
import axios from 'axios';
|
| 5 |
import { Wallet, Lock, User, AlertCircle, ArrowRight, Loader2 } from 'lucide-react';
|
| 6 |
|
|
|
|
|
|
|
| 7 |
const Login: React.FC = () => {
|
| 8 |
const [username, setUsername] = useState('');
|
| 9 |
const [password, setPassword] = useState('');
|
|
@@ -19,7 +21,7 @@ const Login: React.FC = () => {
|
|
| 19 |
if (deviceId && storedUsername) {
|
| 20 |
setIsLoading(true);
|
| 21 |
try {
|
| 22 |
-
const response = await axios.post(
|
| 23 |
username: storedUsername,
|
| 24 |
deviceId,
|
| 25 |
});
|
|
@@ -43,7 +45,7 @@ const Login: React.FC = () => {
|
|
| 43 |
try {
|
| 44 |
const deviceName = `${navigator.platform} - ${navigator.userAgent.split(') ')[0].split(' (')[1] || 'Web Browser'}`;
|
| 45 |
|
| 46 |
-
const response = await axios.post(
|
| 47 |
username,
|
| 48 |
password,
|
| 49 |
deviceName,
|
|
|
|
| 4 |
import axios from 'axios';
|
| 5 |
import { Wallet, Lock, User, AlertCircle, ArrowRight, Loader2 } from 'lucide-react';
|
| 6 |
|
| 7 |
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
|
| 8 |
+
|
| 9 |
const Login: React.FC = () => {
|
| 10 |
const [username, setUsername] = useState('');
|
| 11 |
const [password, setPassword] = useState('');
|
|
|
|
| 21 |
if (deviceId && storedUsername) {
|
| 22 |
setIsLoading(true);
|
| 23 |
try {
|
| 24 |
+
const response = await axios.post(`${API_URL}/login-device`, {
|
| 25 |
username: storedUsername,
|
| 26 |
deviceId,
|
| 27 |
});
|
|
|
|
| 45 |
try {
|
| 46 |
const deviceName = `${navigator.platform} - ${navigator.userAgent.split(') ')[0].split(' (')[1] || 'Web Browser'}`;
|
| 47 |
|
| 48 |
+
const response = await axios.post(`${API_URL}/login`, {
|
| 49 |
username,
|
| 50 |
password,
|
| 51 |
deviceName,
|
client/src/pages/Settings.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
import { useState, useRef, useEffect } from 'react';
|
|
|
|
| 2 |
import { Settings as SettingsIcon, Download, Upload, Trash2, AlertCircle, CheckCircle2, ShieldAlert, Smartphone, X } from 'lucide-react';
|
| 3 |
import { Button } from '../components/ui/button';
|
| 4 |
import { useAuthStore } from '../store/useAuthStore';
|
| 5 |
import axios from 'axios';
|
| 6 |
|
|
|
|
|
|
|
| 7 |
export default function Settings() {
|
|
|
|
| 8 |
const [exporting, setExporting] = useState(false);
|
| 9 |
const [importing, setImporting] = useState(false);
|
| 10 |
const [clearing, setClearing] = useState(false);
|
|
@@ -24,7 +28,7 @@ export default function Settings() {
|
|
| 24 |
if (!token) return;
|
| 25 |
setLoadingDevices(true);
|
| 26 |
try {
|
| 27 |
-
const response = await axios.get(
|
| 28 |
headers: { Authorization: `Bearer ${token}` }
|
| 29 |
});
|
| 30 |
setDevices(response.data);
|
|
@@ -39,7 +43,7 @@ export default function Settings() {
|
|
| 39 |
if (!window.confirm('Are you sure you want to remove this device? You will be logged out on that device.')) return;
|
| 40 |
|
| 41 |
try {
|
| 42 |
-
await axios.delete(`
|
| 43 |
headers: { Authorization: `Bearer ${token}` }
|
| 44 |
});
|
| 45 |
|
|
@@ -54,7 +58,11 @@ export default function Settings() {
|
|
| 54 |
setExporting(true);
|
| 55 |
setStatus('idle');
|
| 56 |
try {
|
| 57 |
-
const response = await fetch(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
if (!response.ok) throw new Error('Export failed');
|
| 59 |
|
| 60 |
const blob = await response.blob();
|
|
@@ -94,9 +102,12 @@ export default function Settings() {
|
|
| 94 |
reader.onload = async (event) => {
|
| 95 |
const base64 = (event.target?.result as string).split(',')[1];
|
| 96 |
try {
|
| 97 |
-
const response = await fetch(
|
| 98 |
method: 'POST',
|
| 99 |
-
headers: {
|
|
|
|
|
|
|
|
|
|
| 100 |
body: JSON.stringify({ file: base64, replace: replaceData })
|
| 101 |
});
|
| 102 |
|
|
@@ -105,6 +116,13 @@ export default function Settings() {
|
|
| 105 |
throw new Error(err.error || 'Import failed');
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
setStatus('success');
|
| 109 |
setMessage('Data imported successfully!');
|
| 110 |
} catch (error: any) {
|
|
@@ -133,8 +151,21 @@ export default function Settings() {
|
|
| 133 |
setClearing(true);
|
| 134 |
setStatus('idle');
|
| 135 |
try {
|
| 136 |
-
const response = await fetch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
if (!response.ok) throw new Error('Clear data failed');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
setStatus('success');
|
| 139 |
setMessage('All data cleared successfully.');
|
| 140 |
} catch (error) {
|
|
|
|
| 1 |
import { useState, useRef, useEffect } from 'react';
|
| 2 |
+
import { useQueryClient } from '@tanstack/react-query';
|
| 3 |
import { Settings as SettingsIcon, Download, Upload, Trash2, AlertCircle, CheckCircle2, ShieldAlert, Smartphone, X } from 'lucide-react';
|
| 4 |
import { Button } from '../components/ui/button';
|
| 5 |
import { useAuthStore } from '../store/useAuthStore';
|
| 6 |
import axios from 'axios';
|
| 7 |
|
| 8 |
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
|
| 9 |
+
|
| 10 |
export default function Settings() {
|
| 11 |
+
const queryClient = useQueryClient();
|
| 12 |
const [exporting, setExporting] = useState(false);
|
| 13 |
const [importing, setImporting] = useState(false);
|
| 14 |
const [clearing, setClearing] = useState(false);
|
|
|
|
| 28 |
if (!token) return;
|
| 29 |
setLoadingDevices(true);
|
| 30 |
try {
|
| 31 |
+
const response = await axios.get(`${API_URL}/devices`, {
|
| 32 |
headers: { Authorization: `Bearer ${token}` }
|
| 33 |
});
|
| 34 |
setDevices(response.data);
|
|
|
|
| 43 |
if (!window.confirm('Are you sure you want to remove this device? You will be logged out on that device.')) return;
|
| 44 |
|
| 45 |
try {
|
| 46 |
+
await axios.delete(`${API_URL}/devices/${id}`, {
|
| 47 |
headers: { Authorization: `Bearer ${token}` }
|
| 48 |
});
|
| 49 |
|
|
|
|
| 58 |
setExporting(true);
|
| 59 |
setStatus('idle');
|
| 60 |
try {
|
| 61 |
+
const response = await fetch(`${API_URL}/export`, {
|
| 62 |
+
headers: {
|
| 63 |
+
'Authorization': `Bearer ${token}`
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
if (!response.ok) throw new Error('Export failed');
|
| 67 |
|
| 68 |
const blob = await response.blob();
|
|
|
|
| 102 |
reader.onload = async (event) => {
|
| 103 |
const base64 = (event.target?.result as string).split(',')[1];
|
| 104 |
try {
|
| 105 |
+
const response = await fetch(`${API_URL}/import`, {
|
| 106 |
method: 'POST',
|
| 107 |
+
headers: {
|
| 108 |
+
'Content-Type': 'application/json',
|
| 109 |
+
'Authorization': `Bearer ${token}`
|
| 110 |
+
},
|
| 111 |
body: JSON.stringify({ file: base64, replace: replaceData })
|
| 112 |
});
|
| 113 |
|
|
|
|
| 116 |
throw new Error(err.error || 'Import failed');
|
| 117 |
}
|
| 118 |
|
| 119 |
+
// Invalidate queries to refresh UI
|
| 120 |
+
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
| 121 |
+
queryClient.invalidateQueries({ queryKey: ['exchanges'] });
|
| 122 |
+
queryClient.invalidateQueries({ queryKey: ['loans'] });
|
| 123 |
+
queryClient.invalidateQueries({ queryKey: ['analytics'] });
|
| 124 |
+
queryClient.invalidateQueries({ queryKey: ['wallets'] });
|
| 125 |
+
|
| 126 |
setStatus('success');
|
| 127 |
setMessage('Data imported successfully!');
|
| 128 |
} catch (error: any) {
|
|
|
|
| 151 |
setClearing(true);
|
| 152 |
setStatus('idle');
|
| 153 |
try {
|
| 154 |
+
const response = await fetch(`${API_URL}/data`, {
|
| 155 |
+
method: 'DELETE',
|
| 156 |
+
headers: {
|
| 157 |
+
'Authorization': `Bearer ${token}`
|
| 158 |
+
}
|
| 159 |
+
});
|
| 160 |
if (!response.ok) throw new Error('Clear data failed');
|
| 161 |
+
|
| 162 |
+
// Invalidate queries to refresh UI
|
| 163 |
+
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
| 164 |
+
queryClient.invalidateQueries({ queryKey: ['exchanges'] });
|
| 165 |
+
queryClient.invalidateQueries({ queryKey: ['loans'] });
|
| 166 |
+
queryClient.invalidateQueries({ queryKey: ['analytics'] });
|
| 167 |
+
queryClient.invalidateQueries({ queryKey: ['wallets'] });
|
| 168 |
+
|
| 169 |
setStatus('success');
|
| 170 |
setMessage('All data cleared successfully.');
|
| 171 |
} catch (error) {
|
server/.env
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
EMAIL_USER=amezamanj11@gmail.com
|
| 2 |
+
EMAIL_PASS="ttvy mdoi ncke vdkh"
|
| 3 |
+
EMAIL_TO=amezamanj7@gmail.com
|
| 4 |
+
|
| 5 |
+
DATABASE_URL=libsql://wallets-01amez.aws-ap-northeast-1.turso.io
|
| 6 |
+
DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3NzM3OTA2ODMsImlkIjoiMDE5Y2ZlMjktYjUwMS03ZWMzLTk0ODAtZTYyN2QyNWQ1NjQzIiwicmlkIjoiZTE4ZDAwNDktYjBlMS00MzE5LWFhMGYtNGM4YWM5NTk1YTZiIn0.rhWzrofoZlV4mAVmh6QABnnlxeuz7fwxnjEr0QBQBfsrH2ziRl25UZc090NH59JPP0aqmi4fmp1P9oxRRUfvCw
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy package files
|
| 6 |
+
COPY package*.json ./
|
| 7 |
+
|
| 8 |
+
# Install dependencies
|
| 9 |
+
RUN npm install
|
| 10 |
+
|
| 11 |
+
# Copy source code
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Build if necessary (not needed for tsx but good practice)
|
| 15 |
+
# RUN npm run build
|
| 16 |
+
|
| 17 |
+
# Expose port (HF Spaces uses 7860 by default)
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Set environment variable for port
|
| 21 |
+
ENV PORT=7860
|
| 22 |
+
|
| 23 |
+
# Start command
|
| 24 |
+
CMD ["npx", "tsx", "src/index.ts"]
|
server/scripts/test-db.ts
CHANGED
|
@@ -10,10 +10,16 @@ async function test() {
|
|
| 10 |
console.log(`Found ${wallets.length} wallets.`);
|
| 11 |
|
| 12 |
if (wallets.length > 0) {
|
| 13 |
-
console.log('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
-
console.log('SUCCESS: Database connection
|
| 17 |
process.exit(0);
|
| 18 |
} catch (error) {
|
| 19 |
console.error('FAILED: Database connection error:', error);
|
|
|
|
| 10 |
console.log(`Found ${wallets.length} wallets.`);
|
| 11 |
|
| 12 |
if (wallets.length > 0) {
|
| 13 |
+
console.log('Testing insert with undefined parameter...');
|
| 14 |
+
// Inserting a transaction with undefined for optional fields
|
| 15 |
+
await db.run(
|
| 16 |
+
'INSERT INTO transactions (type, amount, currency, wallet_id, date, category, note) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
| 17 |
+
['income', 100, 'USD', wallets[0].id, new Date().toISOString(), undefined, 'Test with undefined']
|
| 18 |
+
);
|
| 19 |
+
console.log('Insert with undefined parameter successful.');
|
| 20 |
}
|
| 21 |
|
| 22 |
+
console.log('SUCCESS: Database connection and parameter sanitization are working.');
|
| 23 |
process.exit(0);
|
| 24 |
} catch (error) {
|
| 25 |
console.error('FAILED: Database connection error:', error);
|
server/src/db.ts
CHANGED
|
@@ -26,18 +26,33 @@ export async function getDb(): Promise<Database> {
|
|
| 26 |
authToken,
|
| 27 |
});
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
const db: Database = {
|
| 30 |
get: async (sql: string, params: any[] = []) => {
|
| 31 |
-
const res = await client.execute({ sql, args: params });
|
| 32 |
-
return res.rows[0] ?
|
| 33 |
},
|
| 34 |
all: async (sql: string, params: any[] = []) => {
|
| 35 |
-
const res = await client.execute({ sql, args: params });
|
| 36 |
-
return res.rows.map(row => (
|
| 37 |
},
|
| 38 |
run: async (sql: string, params: any[] = []) => {
|
| 39 |
-
const res = await client.execute({ sql, args: params });
|
| 40 |
-
return { lastID: res.lastInsertRowid || 0 };
|
| 41 |
},
|
| 42 |
exec: async (sql: string) => {
|
| 43 |
await client.batch(sql.split(';').filter(s => s.trim()), 'write');
|
|
|
|
| 26 |
authToken,
|
| 27 |
});
|
| 28 |
|
| 29 |
+
const sanitizeParams = (params: any[]): any[] => {
|
| 30 |
+
return params.map(p => p === undefined ? null : p);
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const sanitizeResult = (row: any) => {
|
| 34 |
+
if (!row) return row;
|
| 35 |
+
const newRow = { ...row };
|
| 36 |
+
for (const [key, value] of Object.entries(newRow)) {
|
| 37 |
+
if (typeof value === 'bigint') {
|
| 38 |
+
newRow[key] = Number(value);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
return newRow;
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
const db: Database = {
|
| 45 |
get: async (sql: string, params: any[] = []) => {
|
| 46 |
+
const res = await client.execute({ sql, args: sanitizeParams(params) });
|
| 47 |
+
return res.rows[0] ? sanitizeResult(res.rows[0]) : undefined;
|
| 48 |
},
|
| 49 |
all: async (sql: string, params: any[] = []) => {
|
| 50 |
+
const res = await client.execute({ sql, args: sanitizeParams(params) });
|
| 51 |
+
return res.rows.map(row => sanitizeResult(row));
|
| 52 |
},
|
| 53 |
run: async (sql: string, params: any[] = []) => {
|
| 54 |
+
const res = await client.execute({ sql, args: sanitizeParams(params) });
|
| 55 |
+
return { lastID: Number(res.lastInsertRowid || 0) };
|
| 56 |
},
|
| 57 |
exec: async (sql: string) => {
|
| 58 |
await client.batch(sql.split(';').filter(s => s.trim()), 'write');
|
server/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ dotenv.config();
|
|
| 14 |
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production';
|
| 15 |
|
| 16 |
const app = express();
|
| 17 |
-
const PORT = 3001;
|
| 18 |
|
| 19 |
app.use(cors());
|
| 20 |
app.use(express.json({ limit: '50mb' }));
|
|
@@ -162,10 +162,10 @@ const loanSchema = z.object({
|
|
| 162 |
async function getWalletBalance(db: any, walletId: number): Promise<number> {
|
| 163 |
const query1 = `
|
| 164 |
SELECT
|
| 165 |
-
COALESCE(SUM(CASE WHEN type =
|
| 166 |
-
COALESCE(SUM(CASE WHEN type =
|
| 167 |
-
COALESCE(SUM(CASE WHEN type =
|
| 168 |
-
COALESCE(SUM(CASE WHEN type =
|
| 169 |
FROM transactions
|
| 170 |
WHERE wallet_id = ? OR to_wallet_id = ?
|
| 171 |
`;
|
|
@@ -340,9 +340,7 @@ app.post('/api/import', authenticateToken, async (req, res) => {
|
|
| 340 |
const db = await getDb();
|
| 341 |
|
| 342 |
if (replace) {
|
| 343 |
-
await db.
|
| 344 |
-
await db.run('DELETE FROM exchanges');
|
| 345 |
-
await db.run('DELETE FROM loans');
|
| 346 |
}
|
| 347 |
|
| 348 |
// Import Transactions
|
|
@@ -395,9 +393,7 @@ app.post('/api/import', authenticateToken, async (req, res) => {
|
|
| 395 |
app.delete('/api/data', authenticateToken, async (req, res) => {
|
| 396 |
try {
|
| 397 |
const db = await getDb();
|
| 398 |
-
await db.
|
| 399 |
-
await db.run('DELETE FROM exchanges');
|
| 400 |
-
await db.run('DELETE FROM loans');
|
| 401 |
res.json({ success: true, message: 'All data cleared successfully' });
|
| 402 |
} catch (error: any) {
|
| 403 |
console.error('Clear data error:', error);
|
|
|
|
| 14 |
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production';
|
| 15 |
|
| 16 |
const app = express();
|
| 17 |
+
const PORT = process.env.PORT || 3001;
|
| 18 |
|
| 19 |
app.use(cors());
|
| 20 |
app.use(express.json({ limit: '50mb' }));
|
|
|
|
| 162 |
async function getWalletBalance(db: any, walletId: number): Promise<number> {
|
| 163 |
const query1 = `
|
| 164 |
SELECT
|
| 165 |
+
COALESCE(SUM(CASE WHEN type = 'income' AND wallet_id = ? THEN amount ELSE 0 END), 0) -
|
| 166 |
+
COALESCE(SUM(CASE WHEN type = 'expense' AND wallet_id = ? THEN amount ELSE 0 END), 0) -
|
| 167 |
+
COALESCE(SUM(CASE WHEN type = 'transfer' AND wallet_id = ? THEN amount ELSE 0 END), 0) +
|
| 168 |
+
COALESCE(SUM(CASE WHEN type = 'transfer' AND to_wallet_id = ? THEN amount ELSE 0 END), 0) as balance
|
| 169 |
FROM transactions
|
| 170 |
WHERE wallet_id = ? OR to_wallet_id = ?
|
| 171 |
`;
|
|
|
|
| 340 |
const db = await getDb();
|
| 341 |
|
| 342 |
if (replace) {
|
| 343 |
+
await db.exec('DELETE FROM transactions; DELETE FROM exchanges; DELETE FROM loans;');
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
// Import Transactions
|
|
|
|
| 393 |
app.delete('/api/data', authenticateToken, async (req, res) => {
|
| 394 |
try {
|
| 395 |
const db = await getDb();
|
| 396 |
+
await db.exec('DELETE FROM transactions; DELETE FROM exchanges; DELETE FROM loans;');
|
|
|
|
|
|
|
| 397 |
res.json({ success: true, message: 'All data cleared successfully' });
|
| 398 |
} catch (error: any) {
|
| 399 |
console.error('Clear data error:', error);
|
server/test-delete.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getDb } from './src/db';
|
| 2 |
+
|
| 3 |
+
async function testDelete() {
|
| 4 |
+
try {
|
| 5 |
+
const db = await getDb();
|
| 6 |
+
|
| 7 |
+
console.log('Inserting test loan...');
|
| 8 |
+
await db.run(
|
| 9 |
+
'INSERT INTO loans (person, type, amount, currency, date) VALUES (?, ?, ?, ?, ?)',
|
| 10 |
+
['Test Person', 'loaned', 500, 'USD', new Date().toISOString()]
|
| 11 |
+
);
|
| 12 |
+
|
| 13 |
+
let loans = await db.all('SELECT * FROM loans');
|
| 14 |
+
console.log(`Loans before delete: ${loans.length}`);
|
| 15 |
+
|
| 16 |
+
console.log('Deleting all loans...');
|
| 17 |
+
await db.run('DELETE FROM loans');
|
| 18 |
+
|
| 19 |
+
loans = await db.all('SELECT * FROM loans');
|
| 20 |
+
console.log(`Loans after delete: ${loans.length}`);
|
| 21 |
+
|
| 22 |
+
if (loans.length === 0) {
|
| 23 |
+
console.log('SUCCESS: Delete from loans works.');
|
| 24 |
+
} else {
|
| 25 |
+
console.log('FAILURE: Delete from loans did not work.');
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
process.exit(0);
|
| 29 |
+
} catch (error) {
|
| 30 |
+
console.error('Test failed:', error);
|
| 31 |
+
process.exit(1);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
testDelete();
|