Skip to content

Commit 2bbda65

Browse files
committed
feat: release v3.6.1 - security hardening
Security improvements: - Enhanced CSP headers with frame-ancestors, form-action, base-uri, object-src - Added rate limiting to all bills, payments, shares, and user search endpoints - iOS Keychain backup exclusion for JWT tokens (WHEN_UNLOCKED_THIS_DEVICE_ONLY) - Timing-safe token comparison in models.py - SQL wildcard escaping for user search to prevent injection - Mobile app: wrapped sensitive logging in __DEV__ guards Other changes: - Structured logging with environment-based configuration - Fixed N+1 query issues with database indexes - Mobile app UI improvements for payment history and stats screens
1 parent 0fbc13a commit 2bbda65

19 files changed

Lines changed: 865 additions & 169 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A **secure multi-user** web application for tracking recurring expenses and inco
66

77
---
88

9-
## 🎉 What's New in v3.6.0
9+
## 🎉 What's New in v3.6.1
1010

1111
**Shared Bills & Split Expenses** - The biggest feature release yet! Now you can share bills with other users and track split payments seamlessly.
1212

66.9 KB
Loading

apps/mobile/assets/icon.png

62.2 KB
Loading

apps/mobile/package-lock.json

Lines changed: 50 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/mobile/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"react-native": "0.81.5",
3232
"react-native-chart-kit": "^6.12.0",
3333
"react-native-gesture-handler": "^2.30.0",
34+
"react-native-gifted-charts": "^1.4.70",
35+
"react-native-linear-gradient": "^2.8.3",
3436
"react-native-reanimated": "~4.1.1",
3537
"react-native-safe-area-context": "~5.6.0",
3638
"react-native-screens": "~4.16.0",

apps/mobile/src/api/client.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ const REFRESH_TOKEN_KEY = 'billmanager_refresh_token';
88
const CURRENT_DATABASE_KEY = 'billmanager_current_database';
99
const LAST_SYNC_KEY = 'billmanager_last_sync';
1010

11+
// Secure storage options for sensitive data (tokens)
12+
// WHEN_UNLOCKED_THIS_DEVICE_ONLY: Data is only accessible when device is unlocked
13+
// AND is NOT backed up to iCloud or migrated to a new device
14+
const SECURE_STORE_OPTIONS: SecureStore.SecureStoreOptions = {
15+
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
16+
};
17+
1118
// API Configuration
1219
// For local development, use your machine's IP (not localhost)
1320
// Physical device (Expo Go): use your computer's local IP
@@ -115,8 +122,8 @@ class BillManagerApi {
115122
this.accessToken = access_token;
116123
this.refreshToken = refresh_token;
117124

118-
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, access_token);
119-
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refresh_token);
125+
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, access_token, SECURE_STORE_OPTIONS);
126+
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refresh_token, SECURE_STORE_OPTIONS);
120127

121128
// Set first database as default if available
122129
if (response.data.data.databases?.length > 0) {
@@ -153,7 +160,7 @@ class BillManagerApi {
153160

154161
if (response.data.success && response.data.data) {
155162
this.accessToken = response.data.data.access_token;
156-
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, this.accessToken);
163+
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, this.accessToken, SECURE_STORE_OPTIONS);
157164
return true;
158165
}
159166
return false;

apps/mobile/src/context/AuthContext.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
5858
loadServerType(),
5959
]);
6060

61-
console.log('[AuthContext] initializeAuth - hasToken:', hasToken, 'serverType:', serverType);
61+
if (__DEV__) {
62+
console.log('[AuthContext] initializeAuth - hasToken:', hasToken, 'serverType:', serverType);
63+
}
6264

6365
if (hasToken) {
6466
// Verify token is still valid by fetching user info
6567
const response = await api.getUserInfo();
66-
console.log('[AuthContext] initializeAuth - getUserInfo response:', JSON.stringify(response, null, 2));
6768

6869
let userData: User | null = null;
6970
let databases: DatabaseInfo[] = [];
@@ -98,7 +99,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
9899
}
99100

100101
if (userData) {
101-
console.log('[AuthContext] initializeAuth - Setting user:', userData);
102+
if (__DEV__) {
103+
console.log('[AuthContext] initializeAuth - User authenticated:', userData.username);
104+
}
102105
setState({
103106
isLoading: false,
104107
isAuthenticated: true,
@@ -109,7 +112,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
109112
});
110113
return;
111114
} else {
112-
console.log('[AuthContext] initializeAuth - No user in response, clearing auth');
115+
if (__DEV__) {
116+
console.log('[AuthContext] initializeAuth - No user in response, clearing auth');
117+
}
113118
}
114119
}
115120

@@ -122,7 +127,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
122127
serverType,
123128
});
124129
} catch (error) {
125-
console.error('Auth initialization error:', error);
130+
if (__DEV__) {
131+
console.error('Auth initialization error:', error);
132+
}
126133
const serverType = await loadServerType();
127134
setState({
128135
isLoading: false,
@@ -153,7 +160,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
153160
try {
154161
// First, login to get tokens
155162
const loginResponse = await api.login(username, password);
156-
console.log('[AuthContext] Login response:', JSON.stringify(loginResponse, null, 2));
163+
if (__DEV__) {
164+
console.log('[AuthContext] Login response success:', loginResponse.success);
165+
}
157166

158167
if (!loginResponse.success) {
159168
return { success: false, error: loginResponse.error || 'Login failed' };
@@ -163,9 +172,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
163172
const serverType = await loadServerType();
164173

165174
// Fetch user info from /me endpoint after login
166-
console.log('[AuthContext] Fetching user info from /me');
167175
const userInfoResponse = await api.getUserInfo();
168-
console.log('[AuthContext] getUserInfo response:', JSON.stringify(userInfoResponse, null, 2));
169176

170177
let userData: User | null = null;
171178
let databases: DatabaseInfo[] = [];
@@ -208,10 +215,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
208215
return { success: true };
209216
}
210217

211-
console.error('[AuthContext] Failed to get user info from /me');
218+
if (__DEV__) {
219+
console.error('[AuthContext] Failed to get user info from /me');
220+
}
212221
return { success: false, error: 'Failed to get user info' };
213222
} catch (error) {
214-
console.error('[AuthContext] Login error:', error);
223+
if (__DEV__) {
224+
console.error('[AuthContext] Login error:', error);
225+
}
215226
return { success: false, error: 'An unexpected error occurred' };
216227
}
217228
}, []);

0 commit comments

Comments
 (0)