first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:16:43 +03:00
commit 6d95e27114
97 changed files with 15687 additions and 0 deletions

View File

@@ -0,0 +1,300 @@
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;