2025-10-25 11:39:24 +02:00

320 lines
9.6 KiB
TypeScript

import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { ThunkSuccess, ThunkError, IUserResponse } from "../types";
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;
}
const initialState: AuthState = {
isLoggedIn: false,
authMessage: "",
status: "idle",
error: null,
user: null,
tokenInfo: null,
mustChangePassword: false,
};
// ---------------- 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 }) => {
try {
const res = await fetch("/api/auth/validate", { method: "POST" });
const data = await res.json();
if (!res.ok || !data.valid) {
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
// ---------------- 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 = "";
},
},
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) => {
state.status = "succeeded";
state.isLoggedIn = false;
state.authMessage = action.payload.message;
state.user = null;
state.tokenInfo = null;
state.mustChangePassword = false;
})
.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) => {
state.status = "succeeded";
state.isLoggedIn = false;
state.authMessage = action.payload.message;
state.user = null;
state.tokenInfo = null;
state.mustChangePassword = false;
})
.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;
})
// 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 } = authSlice.actions;
export default authSlice.reducer;