Files
next-go-blog/lib/features/auth/authSlice.ts
Beyhan Oğur 6d95e27114 first commit
2026-04-26 22:16:43 +03:00

301 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;