feat: implement Google OAuth support for Android and iOS
- Added support for Google OAuth with separate client IDs for Android and iOS. - Updated `verify_google_id_token` to validate `aud` against both client IDs and check `email_verified`. - Modified `google_oauth_handler` to accept and process the new client IDs. - Enhanced security by enforcing explicit JWT algorithm validation. - Updated mobile app to handle Google OAuth flow using `expo-auth-session`. - Fixed API request to send `id_token` in snake_case as expected by the backend. - Added necessary environment variables for Google client IDs in mobile app. - Implemented intent filter for Google OAuth redirect in AndroidManifest.xml.
This commit is contained in:
@@ -17,7 +17,8 @@ export class ApiAuthRepository implements AuthRepository {
|
||||
}
|
||||
|
||||
async loginWithGoogle(input: GoogleOAuthInput): Promise<AuthSession> {
|
||||
const { data } = await httpClient.post('/auth/oauth/google', input);
|
||||
// Backend espera snake_case: { id_token }
|
||||
const { data } = await httpClient.post('/auth/oauth/google', { id_token: input.idToken });
|
||||
return this.fetchSession(data.access_token as string, data.refresh_token as string);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ApiSpoolPresetRepository } from '@adapters/remote/ApiSpoolPresetReposit
|
||||
import {
|
||||
LoginUseCase,
|
||||
RegisterUseCase,
|
||||
GoogleLoginUseCase,
|
||||
ForgotPasswordUseCase,
|
||||
ResendVerificationUseCase,
|
||||
ResetPasswordUseCase,
|
||||
@@ -32,6 +33,7 @@ const presetRepository = new ApiSpoolPresetRepository();
|
||||
// Use cases de autenticação
|
||||
export const loginUseCase = new LoginUseCase(authRepository);
|
||||
export const registerUseCase = new RegisterUseCase(authRepository);
|
||||
export const googleLoginUseCase = new GoogleLoginUseCase(authRepository);
|
||||
export const resendVerificationUseCase = new ResendVerificationUseCase(authRepository);
|
||||
export const forgotPasswordUseCase = new ForgotPasswordUseCase(authRepository);
|
||||
export const resetPasswordUseCase = new ResetPasswordUseCase(authRepository);
|
||||
|
||||
@@ -2,7 +2,8 @@ import { create } from 'zustand';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import axios from 'axios';
|
||||
import type { User, AuthSession } from '@domain/User';
|
||||
import { SECURE_STORE_ACCESS_TOKEN, SECURE_STORE_REFRESH_TOKEN, API_BASE_URL } from '@shared/constants';
|
||||
import { SECURE_STORE_ACCESS_TOKEN, SECURE_STORE_REFRESH_TOKEN } from '@shared/constants';
|
||||
import { httpClient } from '@adapters/remote/httpClient';
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
@@ -56,27 +57,38 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
loadStoredSession: async () => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const accessToken = await SecureStore.getItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
||||
const refreshToken = await SecureStore.getItemAsync(SECURE_STORE_REFRESH_TOKEN);
|
||||
if (accessToken) {
|
||||
const { data } = await axios.get(`${API_BASE_URL}/users/me`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const user: User = {
|
||||
id: String(data.id),
|
||||
email: data.email as string,
|
||||
name: (data.name as string | null) ?? null,
|
||||
googleId: (data.google_id as string | null) ?? null,
|
||||
createdAt: data.created_at as string,
|
||||
};
|
||||
set({ user, accessToken, refreshToken, isAuthenticated: true, isLoading: false });
|
||||
} else {
|
||||
const storedToken = await SecureStore.getItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
||||
if (!storedToken) {
|
||||
set({ isLoading: false });
|
||||
return;
|
||||
}
|
||||
// Usa httpClient para que o interceptor de refresh atue automaticamente:
|
||||
// se o access token expirou mas o refresh token é válido, a sessão é renovada
|
||||
// em vez de forçar logout desnecessariamente.
|
||||
const { data } = await httpClient.get('/users/me');
|
||||
// Lê tokens após possível refresh (o interceptor pode ter atualizado o SecureStore)
|
||||
const [accessToken, refreshToken] = await Promise.all([
|
||||
SecureStore.getItemAsync(SECURE_STORE_ACCESS_TOKEN),
|
||||
SecureStore.getItemAsync(SECURE_STORE_REFRESH_TOKEN),
|
||||
]);
|
||||
const user: User = {
|
||||
id: String(data.id),
|
||||
email: data.email as string,
|
||||
name: (data.name as string | null) ?? null,
|
||||
googleId: (data.google_id as string | null) ?? null,
|
||||
createdAt: data.created_at as string,
|
||||
};
|
||||
set({ user, accessToken, refreshToken, isAuthenticated: true, isLoading: false });
|
||||
} catch (err) {
|
||||
// Limpa sessão apenas em falha de autenticação (401/403).
|
||||
// Erros de rede (sem conexão) não forçam logout — o usuário pode estar offline.
|
||||
const isAuthError =
|
||||
axios.isAxiosError(err) &&
|
||||
(err.response?.status === 401 || err.response?.status === 403);
|
||||
if (isAuthError || !axios.isAxiosError(err)) {
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_REFRESH_TOKEN);
|
||||
}
|
||||
} catch {
|
||||
// Token inválido ou expirado — limpa a sessão
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_ACCESS_TOKEN);
|
||||
await SecureStore.deleteItemAsync(SECURE_STORE_REFRESH_TOKEN);
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user