2025-10-29 19:34:10 +01:00

382 lines
12 KiB
TypeScript

import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { ThunkSuccess, ThunkError, IUserResponse } from "../types";
import { IEditUserForm } from "../../features/UserRoles/User.interfaces";
import { RootState } from "../store";
import toast from "react-hot-toast";
interface TokenInfo {
expiresAt: string;
timeUntilExpiration: number;
expiresInHours: number;
}
interface AuthState {
isLoggedIn: boolean;
authMessage: string;
status: "idle" | "loading" | "succeeded" | "failed";
error: string | null;
user: IUserResponse | null;
tokenInfo: TokenInfo | null;
mustChangePassword: boolean;
addedUser: IUserResponse | null;
}
const initialState: AuthState = {
isLoggedIn: false,
authMessage: "",
status: "idle",
error: null,
user: null,
tokenInfo: null,
mustChangePassword: false,
addedUser: null,
};
// ---------------- Login ----------------
export const login = createAsyncThunk<
ThunkSuccess<{
user: IUserResponse | null;
tokenInfo: TokenInfo | null;
mustChangePassword: boolean;
}>,
{ email: string; password: string },
{ rejectValue: ThunkError }
>(
"auth/login",
async (
{ email, password }: { email: string; password: string },
{ rejectWithValue }
) => {
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
toast.error(data.message || "Login failed");
return rejectWithValue((data.message as string) || "Login failed");
}
// After login, validate session right away
const validateRes = await fetch("/api/auth/validate", { method: "POST" });
const validateData = await validateRes.json();
toast.success(data.message || "Login successful");
return {
message: data.message || "Login successful",
user: data.user || null,
tokenInfo: validateData.tokenInfo || null,
mustChangePassword: data.must_change_password || false,
} as ThunkSuccess<{
user: IUserResponse | null;
tokenInfo: TokenInfo | null;
mustChangePassword: boolean;
}>;
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during login"
);
}
}
);
// ---------------- Logout ----------------
export const logout = createAsyncThunk<
ThunkSuccess,
void,
{ rejectValue: ThunkError }
>("auth/logout", async (_, { rejectWithValue }) => {
try {
const res = await fetch("/api/auth/logout", { method: "DELETE" });
const data = await res.json();
if (!res.ok)
return rejectWithValue((data.message as string) || "Logout failed");
// Persisted state will be cleared by reducers below; avoid direct persistor usage here.
return { message: data.message || "Logged out successfully" };
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during logout"
);
}
});
// ---------------- Auto Logout ----------------
export const autoLogout = createAsyncThunk<
ThunkSuccess,
string,
{ rejectValue: ThunkError }
>("auth/autoLogout", async (reason: string, { rejectWithValue }) => {
try {
await fetch("/api/auth/logout", { method: "DELETE" });
return { message: `Auto-logout: ${reason}` };
} catch (err: unknown) {
return rejectWithValue((err as Error).message || "Auto-logout failed");
}
});
// ---------------- Change Password ----------------
export const changePassword = createAsyncThunk<
ThunkSuccess<{ success: boolean }>,
{ email: string; newPassword: string; currentPassword?: string },
{ rejectValue: ThunkError }
>(
"auth/changePassword",
async ({ email, newPassword, currentPassword }, { rejectWithValue }) => {
try {
const res = await fetch("/api/auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, newPassword, currentPassword }),
});
const data = await res.json();
if (!res.ok)
return rejectWithValue(
(data.message as string) || "Password change failed"
);
return {
success: data.success,
message: data.message || "Password changed successfully",
};
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during password change"
);
}
}
);
// ---------------- Unified Validate Auth ----------------
export const validateAuth = createAsyncThunk<
{ tokenInfo: TokenInfo | null },
void,
{ rejectValue: ThunkError }
>("auth/validate", async (_, { rejectWithValue, dispatch }) => {
try {
const res = await fetch("/api/auth/validate", { method: "POST" });
const data = await res.json();
if (!res.ok || !data.success) {
toast.error(data.message || "Session invalid or expired");
dispatch(autoLogout("Session invalid or expired"));
return rejectWithValue("Session invalid or expired");
}
return {
tokenInfo: data.tokenInfo || null,
};
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during validation"
);
}
});
// TODO - Creaye a new thunk to update the user stuff
// ---------------- Add User ----------------
export const addUser = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse; success: boolean }>,
IEditUserForm,
{ rejectValue: ThunkError; state: RootState }
>("auth/addUser", async (userData, { rejectWithValue, getState }) => {
try {
const state = getState();
const currentUserId = state.auth.user?.id;
console.log("[DEBUG] [ADD-USER] [currentUserId]: ", currentUserId);
const res = await fetch("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...userData, creator: currentUserId || "" }),
});
const data = await res.json();
console.log("[DEBUG] [ADD-USER] [data]: ", data);
if (!res.ok) {
return rejectWithValue(data.message || "Failed to create user");
}
return {
success: data.success,
message: data.message || "User created successfully",
user: data.user,
} as ThunkSuccess<{ user: IUserResponse; success: boolean }>;
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during user creation"
);
}
});
// ---------------- Update User Details ----------------
export const updateUserDetails = createAsyncThunk<
ThunkSuccess<{ user: IUserResponse }>,
{ id: string; updates: Partial<IUserResponse> },
{ rejectValue: ThunkError }
>("auth/updateUserDetails", async ({ id, updates }, { rejectWithValue }) => {
try {
const res = await fetch(`/api/dashboard/admin/users/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updates),
});
const data = await res.json();
if (!res.ok) {
return rejectWithValue(data.message || "Failed to update user details");
}
return {
message: data.message || "User details updated successfully",
user: data.user,
};
} catch (err: unknown) {
return rejectWithValue(
(err as Error).message || "Network error during user update"
);
}
});
// ---------------- Slice ----------------
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
setAuthMessage: (state, action: PayloadAction<string>) => {
state.authMessage = action.payload;
},
clearAuthMessage: state => {
state.authMessage = "";
},
clearAddedUser: state => {
state.addedUser = null;
},
},
extraReducers: builder => {
builder
// Login
.addCase(login.pending, state => {
state.status = "loading";
state.authMessage = "Attempting login...";
})
.addCase(login.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = true;
state.authMessage = action.payload.message;
state.user = action.payload.user;
state.tokenInfo = action.payload.tokenInfo;
state.mustChangePassword = action.payload.mustChangePassword;
})
.addCase(login.rejected, (state, action) => {
state.status = "failed";
state.isLoggedIn = false;
state.error = action.payload as string;
state.authMessage = action.payload as string;
})
// Logout
.addCase(logout.fulfilled, (state, action) => {
return {
...initialState,
status: "succeeded",
authMessage: action.payload.message,
};
})
.addCase(logout.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.authMessage = action.payload as string;
})
// Auto Logout
.addCase(autoLogout.fulfilled, (state, action) => {
return {
...initialState,
status: "succeeded",
isLoggedIn: false,
authMessage: action.payload.message,
};
})
.addCase(autoLogout.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
})
// Change Password
.addCase(changePassword.pending, state => {
state.status = "loading";
state.authMessage = "Changing password...";
})
.addCase(changePassword.fulfilled, (state, action) => {
state.status = "succeeded";
state.authMessage = action.payload.message;
state.mustChangePassword = action.payload.success;
})
.addCase(changePassword.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.authMessage = action.payload as string;
})
// Validate Auth
.addCase(validateAuth.pending, state => {
state.status = "loading";
})
.addCase(validateAuth.fulfilled, (state, action) => {
state.status = "succeeded";
state.isLoggedIn = true;
state.tokenInfo = action.payload.tokenInfo;
})
.addCase(validateAuth.rejected, (state, action) => {
state.status = "failed";
state.isLoggedIn = false;
state.tokenInfo = null;
state.error = action.payload as string;
})
// Add User
.addCase(addUser.pending, state => {
state.status = "loading";
state.authMessage = "Creating user...";
state.addedUser = null;
})
.addCase(addUser.fulfilled, (state, action) => {
state.status = "succeeded";
state.authMessage = action.payload.message;
state.addedUser = action.payload.user;
})
.addCase(addUser.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.authMessage = action.payload as string;
state.addedUser = null;
})
// Update User Details
.addCase(updateUserDetails.pending, state => {
state.status = "loading";
state.authMessage = "Updating user details...";
})
.addCase(updateUserDetails.fulfilled, (state, action) => {
state.status = "succeeded";
state.user = action.payload.user;
state.authMessage = action.payload.message;
})
.addCase(updateUserDetails.rejected, (state, action) => {
state.status = "failed";
state.error = action.payload as string;
state.authMessage = action.payload as string;
});
},
});
export const { setAuthMessage, clearAuthMessage, clearAddedUser } =
authSlice.actions;
export default authSlice.reducer;