301 lines
9.1 KiB
TypeScript
301 lines
9.1 KiB
TypeScript
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;
|