frontend: use `/me` endpoint, fix minor lint issues, and logout user if 401
Parents:
c8a77ef3 file(s) changed
- frontend/context/AuthContext.tsx +21 -26
- frontend/lib/api/client.ts +14 -0
- frontend/lib/api/types.ts +1 -1
frontend/context/AuthContext.tsx
1 1
import React, { createContext, useContext, useEffect, useState } from "react";
2 2
import { apiClient, User } from "@/lib/api";
3 3
import { queryClient, storage } from "@/lib/queryClient";
4 4
5 5
interface AuthProviderProps {
▸ 15 unchanged lines
21 21
logout: () => Promise<void>;
22 22
forgotPassword: (
23 23
email: string,
24 24
) => Promise<{ message: string; token?: string }>;
25 25
resetPassword: (token: string, password: string) => Promise<void>;
26 -
activate: (code: string) => Promise<void>;
26 +
activate: (code: number) => Promise<void>;
27 27
requestActivateCode: () => Promise<void>;
28 28
}
29 29
30 30
export function sanitizeUser(data: any): User {
31 31
return {
▸ 24 unchanged lines
56 56
export const AuthProvider = ({ children }: AuthProviderProps) => {
57 57
const [user, setUser] = useState<User | null>(null);
58 58
const [isLoading, setIsLoading] = useState(true);
59 59
const isAuthenticated = !!user;
60 60
61 -
// Check for existing authentication on app start
62 61
useEffect(() => {
63 62
checkAuthState();
64 63
}, []);
65 64
65 +
useEffect(() => {
66 +
apiClient.registerUnauthorizedHandler(() => {
67 +
logout();
68 +
});
69 +
}, []);
70 +
66 71
const checkAuthState = async () => {
67 -
try {
72 +
const doAuthCheck = async () => {
68 73
const { token } = await apiClient.getStoredAuthData();
69 -
70 74
if (token) {
71 -
const verifyResponse = await apiClient.verifyToken();
72 -
if (
73 -
verifyResponse.valid &&
74 -
verifyResponse.userId &&
75 -
verifyResponse.email
76 -
) {
77 -
// TODO: Fetch the full user profile
78 -
// For now, we'll create a basic user object
79 -
setUser({
80 -
id: verifyResponse.userId,
81 -
email: verifyResponse.email,
82 -
...verifyResponse.user,
83 -
});
84 -
}
75 +
await apiClient.verifyToken();
76 +
const { user } = await apiClient.getMe();
77 +
setUser(user);
85 78
}
79 +
};
80 +
81 +
const timeout = new Promise<never>((_, reject) =>
82 +
setTimeout(() => reject(new Error("Auth check timed out")), 10_000),
83 +
);
84 +
85 +
try {
86 +
await Promise.race([doAuthCheck(), timeout]);
86 87
} catch (error) {
87 88
console.error("Auth check failed:", error);
88 -
// Token is invalid, wipe out any stored data
89 89
await apiClient.logout();
90 90
} finally {
91 91
setIsLoading(false);
92 92
}
93 93
};
▸ 54 unchanged lines
148 148
await apiClient.resetPassword(token, password);
149 149
};
150 150
151 151
const activate = async (code: number) => {
152 152
const response = await apiClient.activate(code);
153 -
154 -
if (response.user) {
155 -
setUser(response.user);
156 -
}
157 -
158 -
return response;
153 +
setUser(response.user);
159 154
};
160 155
161 156
const requestActivateCode = async () => {
162 157
await apiClient.requestActivateCode();
163 158
};
164 159
165 -
const updateAuthUser = (user) => {
160 +
const updateAuthUser = (user: User) => {
166 161
setUser(user);
167 162
};
168 163
169 164
const value: AuthContextType = {
170 165
user,
▸ 9 unchanged lines
180 175
requestActivateCode,
181 176
};
182 177
183 178
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
184 179
};
frontend/lib/api/client.ts
1 1
import * as SecureStore from "expo-secure-store";
2 2
import {
3 3
User,
4 4
AuthResponse,
5 5
VerifyTokenResponse,
▸ 35 unchanged lines
41 41
42 42
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
43 43
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
44 44
45 45
class ApiClient {
46 +
private onUnauthorizedCallback?: () => void;
47 +
48 +
registerUnauthorizedHandler(cb: () => void) {
49 +
this.onUnauthorizedCallback = cb;
50 +
}
51 +
46 52
private async getStoredToken(): Promise<string | null> {
47 53
try {
48 54
return await SecureStore.getItemAsync(TOKEN_KEY!);
49 55
} catch (error) {
50 56
console.error("Failed to get stored token:", error);
▸ 40 unchanged lines
91 97
};
92 98
93 99
const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
94 100
95 101
if (!response.ok) {
102 +
if (response.status === 401) {
103 +
this.onUnauthorizedCallback?.();
104 +
}
105 +
96 106
const error = await response
97 107
.json()
98 108
.catch(() => ({ error: "Network error" }));
99 109
100 110
console.error("[API]: ", error, response.status);
▸ 71 unchanged lines
172 182
async logout(): Promise<void> {
173 183
await this.removeToken();
174 184
}
175 185
176 186
// User-related
187 +
async getMe(): Promise<{ user: User }> {
188 +
return this.request('/users/me');
189 +
}
190 +
177 191
async updateUser(user: UserUpdatePayload): Promise<{ user: User }> {
178 192
return await this.request(`/users/${user.id}`, {
179 193
method: "PUT",
180 194
body: JSON.stringify({ user }),
181 195
});
▸ 262 unchanged lines
444 458
return this.request(`/triggers/${triggerId}/link-mood/${moodId}`, { method: 'DELETE' });
445 459
}
446 460
}
447 461
448 462
export const apiClient = new ApiClient();
frontend/lib/api/types.ts
1 1
export interface User {
2 2
id: number;
3 3
email: string;
4 4
firstName: string;
5 5
lastName?: string;
▸ 34 unchanged lines
40 40
export interface ResetPasswordResponse {
41 41
message: string;
42 42
}
43 43
44 44
export interface ActivateResponse {
45 -
activated: boolean;
45 +
user: User;
46 46
}
47 47
48 48
// User
49 49
export interface UserUpdatePayload extends Partial<User> {
50 50
id: number;
▸ 320 unchanged lines
371 371
moodId?: number;
372 372
}
373 373
export interface MedicineRegimenResponse { regimen: MedicineRegimen; }
374 374
375 375
export interface LinkMoodPayload { moodId: number; perceivedImpact: number; }