eletrotupi / tcc/ commit / 00a1c75

frontend: use `/me` endpoint, fix minor lint issues, and logout user if 401

Pedro Lucas Porcellis porcellis@eletrotupi.com 1 month ago 00a1c753d286aaa005996dc78a2911f038825877
Parents: c8a77ef
3 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; }