z1amez commited on
Commit
ac25f89
·
1 Parent(s): 2dddd1f
.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 (Render)
22
- 1. Go to [Render.com](https://render.com) and sign up.
23
- 2. Create a new **Web Service**.
24
- 3. Connect your GitHub repository.
25
- 4. Set the following:
26
- - **Language**: `Node`
27
- - **Build Command**: `cd server && npm install`
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 like `my-secret-key-123`)
33
- - `EMAIL_USER`: (Your Gmail if using backup)
34
- - `EMAIL_PASS`: (Your Gmail App Password)
35
- - `EMAIL_TO`: (Where to send backups)
36
- 6. Render will give you a URL like `https://wallets-api.onrender.com`.
 
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-render-api-url.onrender.com/api`
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('http://localhost:3001/api/login-device', {
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('http://localhost:3001/api/login', {
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('http://localhost:3001/api/devices', {
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(`http://localhost:3001/api/devices/${id}`, {
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('http://localhost:3001/api/export');
 
 
 
 
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('http://localhost:3001/api/import', {
98
  method: 'POST',
99
- headers: { 'Content-Type': 'application/json' },
 
 
 
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('http://localhost:3001/api/data', { method: 'DELETE' });
 
 
 
 
 
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('First wallet:', wallets[0].name);
 
 
 
 
 
 
14
  }
15
 
16
- console.log('SUCCESS: Database connection is working with LibSQL wrapper.');
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] ? { ...res.rows[0] } : undefined;
33
  },
34
  all: async (sql: string, params: any[] = []) => {
35
- const res = await client.execute({ sql, args: params });
36
- return res.rows.map(row => ({ ...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 = "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,9 +340,7 @@ app.post('/api/import', authenticateToken, async (req, res) => {
340
  const db = await getDb();
341
 
342
  if (replace) {
343
- await db.run('DELETE FROM transactions');
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.run('DELETE FROM transactions');
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();