320 lines
9.6 KiB
TypeScript
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;
|