import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"; import { fetchClient } from "@/lib/api/fetchClient"; import Cookies from "js-cookie"; // Types export interface Permission { id: number; name: string; description: string; } export interface Role { id: number; name: string; description: string; permissions?: Permission[] | null; } export interface SocialAccount { id: number; provider: string; email: string; } export interface User { id: string; username: string; email: string; roles: Role[]; avatar_url?: string; email_verified?: boolean; is_oauth_user?: boolean; social_accounts?: SocialAccount[]; created_at?: string; updated_at?: string; } interface AuthState { user: User | null; isAuthenticated: boolean; isLoading: boolean; error: string | null; } interface LoginResponse { user_id: string; username: string; email: string; access_token: string; refresh_token: string; roles: Role[]; avatar?: string; } interface RegisterResponse { user_id: string; username: string; email: string; avatar: string; email_verified: boolean; message: string; roles: Role[]; verification_token: string; } // Initial State const initialState: AuthState = { user: null, isAuthenticated: false, isLoading: false, error: null, }; // Async Thunks export const login = createAsyncThunk( "auth/login", async (credentials: any, { rejectWithValue }) => { try { const response: LoginResponse = await fetchClient("/auth/login", { method: "POST", body: JSON.stringify(credentials), }); // Validate Check: Ensure tokens exist if (!response.access_token || !response.refresh_token) { return rejectWithValue("Giriş başarısız: Token alınamadı. Lütfen emailinizi doğruladığınızdan emin olun."); } // Store tokens in cookies Cookies.set("access_token", response.access_token, { secure: true, sameSite: 'strict' }); Cookies.set("refresh_token", response.refresh_token, { secure: true, sameSite: 'strict' }); // Store user info in localStorage for convenience (optional, can be removed if strict cookie only) if (typeof window !== "undefined") { localStorage.setItem("user", JSON.stringify({ id: response.user_id, username: response.username, email: response.email, roles: response.roles, avatar_url: response.avatar })); } return response; } catch (error: any) { return rejectWithValue(error.message); } } ); export const register = createAsyncThunk( "auth/register", async (credentials: any, { rejectWithValue }) => { try { const response: RegisterResponse = await fetchClient("/auth/register", { method: "POST", body: JSON.stringify(credentials), }); // NOTE: Register does NOT return tokens anymore. // We do NOT set cookies here. // We do NOT set localStorage user here. return response; } catch (error: any) { return rejectWithValue(error.message); } } ); export const fetchProfile = createAsyncThunk( "auth/fetchProfile", async (_, { rejectWithValue }) => { try { const response = await fetchClient("/profile"); const data = response as any; // Map API response to User type, specifically avatar -> avatar_url return { ...data, avatar_url: data.avatar, roles: data.roles || [] } as User; } catch (error: any) { return rejectWithValue(error.message); } } ); export const updateProfile = createAsyncThunk( "auth/updateProfile", async (formData: FormData, { rejectWithValue }) => { try { const response = await fetchClient("/profile", { method: "PUT", body: formData, }); const data = (response as any).user; // Map API response to User type, specifically avatar -> avatar_url return { ...data, avatar_url: data.avatar, roles: data.roles || [] } as User; } catch (error: any) { return rejectWithValue(error.message); } } ); export const changePassword = createAsyncThunk( "auth/changePassword", async (data: any, { rejectWithValue }) => { try { const response = await fetchClient("/profile/password", { method: "PUT", body: JSON.stringify(data), }); return response; } catch (error: any) { return rejectWithValue(error.message); } } ); export const changeEmail = createAsyncThunk( "auth/changeEmail", async (data: any, { rejectWithValue }) => { try { const response = await fetchClient("/profile/email", { method: "PUT", body: JSON.stringify(data), }); return response; } catch (error: any) { return rejectWithValue(error.message); } } ); // Slice const authSlice = createSlice({ name: "auth", initialState, reducers: { logout: (state) => { state.user = null; state.isAuthenticated = false; state.error = null; Cookies.remove("access_token"); Cookies.remove("refresh_token"); if (typeof window !== "undefined") { localStorage.removeItem("user"); } }, restoreSession: (state) => { if (typeof window !== "undefined") { const userStr = localStorage.getItem("user"); const token = Cookies.get("access_token"); if (userStr && token) { try { const parsedUser = JSON.parse(userStr); // Migration for legacy data: map avatar to avatar_url if needed if (parsedUser.avatar && !parsedUser.avatar_url) { parsedUser.avatar_url = parsedUser.avatar; } state.user = parsedUser; state.isAuthenticated = true; } catch (e) { // Corrupt user data localStorage.removeItem("user"); Cookies.remove("access_token"); Cookies.remove("refresh_token"); } } } } }, extraReducers: (builder) => { // Login builder.addCase(login.pending, (state) => { state.isLoading = true; state.error = null; }); builder.addCase(login.fulfilled, (state, action) => { state.isLoading = false; state.isAuthenticated = true; state.user = { id: action.payload.user_id, username: action.payload.username, email: action.payload.email, roles: action.payload.roles, avatar_url: action.payload.avatar }; }); builder.addCase(login.rejected, (state, action) => { state.isLoading = false; state.error = action.payload as string; }); // Register builder.addCase(register.pending, (state) => { state.isLoading = true; state.error = null; }); builder.addCase(register.fulfilled, (state, action) => { state.isLoading = false; // isAuthenticated remains false because we need email verification state.isAuthenticated = false; // We do not set the user state effectively until they login state.user = null; }); builder.addCase(register.rejected, (state, action) => { state.isLoading = false; state.error = action.payload as string; }); // Fetch Profile builder.addCase(fetchProfile.fulfilled, (state, action) => { state.user = action.payload; // Sync with localStorage so next refresh has fresh data if (typeof window !== "undefined") { localStorage.setItem("user", JSON.stringify(action.payload)); } }); // Update Profile builder.addCase(updateProfile.fulfilled, (state, action) => { state.user = action.payload; // Sync with localStorage so next refresh has fresh data if (typeof window !== "undefined") { localStorage.setItem("user", JSON.stringify(action.payload)); } }); }, }); export const { logout, restoreSession } = authSlice.actions; export default authSlice.reducer;