393 lines
10 KiB
TypeScript
393 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback, useEffect } from "react";
|
|
import { useRouter } from "@/components/navigation";
|
|
import { toast } from "sonner";
|
|
import Cookies from "js-cookie";
|
|
import {
|
|
LoginFormData,
|
|
ProfileData,
|
|
AuthState,
|
|
AuthContextType,
|
|
EmailValidationData,
|
|
OTPData,
|
|
} from "@/types/auth";
|
|
import {
|
|
login,
|
|
getProfile,
|
|
postEmailValidation,
|
|
postSetupEmail,
|
|
verifyOTPByUsername,
|
|
doLogin,
|
|
} from "@/service/auth";
|
|
import {
|
|
setAuthCookies,
|
|
setProfileCookies,
|
|
clearAllCookies,
|
|
isUserEligible,
|
|
isProtectedRole,
|
|
getNavigationPath,
|
|
showAuthError,
|
|
showAuthSuccess,
|
|
loginRateLimiter,
|
|
AUTH_CONSTANTS,
|
|
} from "@/lib/auth-utils";
|
|
import { warning } from "@/lib/swal";
|
|
import { setCookiesEncrypt } from "@/lib/utils";
|
|
|
|
export const useAuth = (): AuthContextType => {
|
|
const router = useRouter();
|
|
const [state, setState] = useState<AuthState>({
|
|
isAuthenticated: false,
|
|
user: null,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const checkAuth = async () => {
|
|
try {
|
|
setState((prev) => ({ ...prev, loading: true }));
|
|
} catch (error) {
|
|
setState((prev) => ({
|
|
...prev,
|
|
isAuthenticated: false,
|
|
user: null,
|
|
error: "Authentication check failed",
|
|
}));
|
|
} finally {
|
|
setState((prev) => ({ ...prev, loading: false }));
|
|
}
|
|
};
|
|
|
|
checkAuth();
|
|
}, []);
|
|
|
|
const login = useCallback(
|
|
async (
|
|
credentials: LoginFormData,
|
|
options?: { skipRedirect?: boolean },
|
|
): Promise<void> => {
|
|
try {
|
|
setState((prev) => ({ ...prev, loading: true, error: null }));
|
|
|
|
// Check rate limiting
|
|
if (!loginRateLimiter.canAttempt(credentials.username)) {
|
|
const remainingTime = loginRateLimiter.getRemainingTime(
|
|
credentials.username,
|
|
);
|
|
const minutes = Math.ceil(remainingTime / (60 * 1000));
|
|
throw new Error(
|
|
`Too many login attempts. Please try again in ${minutes} minutes.`,
|
|
);
|
|
}
|
|
|
|
// Attempt login
|
|
const response = await doLogin({
|
|
...credentials,
|
|
});
|
|
|
|
if (response?.error) {
|
|
loginRateLimiter.recordAttempt(credentials.username);
|
|
throw new Error("Invalid username or password");
|
|
}
|
|
|
|
const { access_token, refresh_token } = response?.data?.data || {};
|
|
|
|
if (!access_token || !refresh_token) {
|
|
throw new Error("Invalid response from server");
|
|
}
|
|
|
|
// Set auth cookies
|
|
setAuthCookies(access_token, refresh_token);
|
|
|
|
// Get user profile
|
|
const profileResponse = await getProfile(access_token);
|
|
const profile = profileResponse?.data?.data;
|
|
|
|
if (!profile) {
|
|
throw new Error("Failed to fetch user profile");
|
|
}
|
|
|
|
const dateTime: any = new Date();
|
|
|
|
const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
|
|
|
|
Cookies.set("access_token", response?.data?.data?.access_token, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("time_refresh", newTime, {
|
|
expires: 1,
|
|
});
|
|
if (response?.data?.data?.approvalWorkflowInfo?.hasWorkflowSetup) {
|
|
Cookies.set(
|
|
"default_workflow",
|
|
response?.data?.data?.approvalWorkflowInfo?.defaultWorkflowId,
|
|
{
|
|
expires: 1,
|
|
},
|
|
);
|
|
}
|
|
|
|
Cookies.set("is_first_login", "true", {
|
|
secure: true,
|
|
sameSite: "strict",
|
|
});
|
|
// await saveActivity(
|
|
// {
|
|
// activityTypeId: 1,
|
|
// url: "https://dev.mikulnews.com/auth",
|
|
// userId: profile?.data?.data?.id,
|
|
// },
|
|
// response?.data?.data?.access_token
|
|
// );
|
|
Cookies.set("profile_picture", profile?.profilePictureUrl, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("uie", profile?.id, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("ufne", profile?.fullname, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("ulie", profile?.userLevelGroup, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("username", profile?.username, {
|
|
expires: 1,
|
|
});
|
|
setCookiesEncrypt("urie", profile?.userRoleId, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("roleName", profile?.roleName, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("masterPoldaId", profile?.masterPoldaId, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("ulne", profile?.userLevelId, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("urce", profile?.roleCode, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("email", profile?.email, {
|
|
expires: 1,
|
|
});
|
|
Cookies.set("status", "login", {
|
|
expires: 1,
|
|
});
|
|
|
|
// Reset rate limiter on successful login
|
|
loginRateLimiter.resetAttempts(credentials.username);
|
|
|
|
if (!options?.skipRedirect) {
|
|
if (profile?.userRoleId === 4 || profile?.userRoleId === 5) {
|
|
router.push("/");
|
|
} else {
|
|
router.push("/admin/dashboard");
|
|
}
|
|
}
|
|
|
|
// if (profile?.userRoleId === 4 || profile?.userRoleId === 5) {
|
|
// router.push("/");
|
|
// } else {
|
|
// router.push("/admin/dashboard");
|
|
// }
|
|
} catch (error: any) {
|
|
const errorMessage = error?.message || "Login failed";
|
|
setState((prev) => ({
|
|
...prev,
|
|
loading: false,
|
|
error: errorMessage,
|
|
}));
|
|
showAuthError(error, "Login failed");
|
|
}
|
|
},
|
|
[router],
|
|
);
|
|
|
|
const logout = useCallback((): void => {
|
|
clearAllCookies();
|
|
setState({
|
|
isAuthenticated: false,
|
|
user: null,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
router.push("/auth");
|
|
}, [router]);
|
|
|
|
const refreshToken = useCallback(async (): Promise<void> => {
|
|
try {
|
|
setState((prev) => ({ ...prev, loading: true }));
|
|
// Add token refresh logic here
|
|
// This would typically call an API to refresh the access token
|
|
} catch (error) {
|
|
logout();
|
|
} finally {
|
|
setState((prev) => ({ ...prev, loading: false }));
|
|
}
|
|
}, [logout]);
|
|
|
|
return {
|
|
...state,
|
|
login,
|
|
logout,
|
|
refreshToken,
|
|
};
|
|
};
|
|
|
|
// Hook for email validation step
|
|
export const useEmailValidation = () => {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const validateEmail = useCallback(
|
|
async (credentials: LoginFormData): Promise<string> => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const response = await postEmailValidation(credentials);
|
|
|
|
if (response?.error) {
|
|
throw new Error(response?.message || "Email validation failed");
|
|
}
|
|
|
|
const message = response?.data?.messages[0];
|
|
|
|
switch (message) {
|
|
case "Continue to setup email":
|
|
return "setup";
|
|
case "Email is valid and OTP has been sent":
|
|
return "otp";
|
|
case "Username & password valid":
|
|
return "success";
|
|
case "Skip to login":
|
|
return "skip";
|
|
default:
|
|
return "login";
|
|
}
|
|
} catch (error: any) {
|
|
const errorMessage = error?.message || "Email validation failed";
|
|
setError(errorMessage);
|
|
showAuthError(error, "Email validation failed");
|
|
throw error;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
return {
|
|
validateEmail,
|
|
loading,
|
|
error,
|
|
};
|
|
};
|
|
|
|
// Hook for email setup step
|
|
export const useEmailSetup = () => {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const setupEmail = useCallback(
|
|
async (
|
|
credentials: LoginFormData,
|
|
emailData: EmailValidationData,
|
|
): Promise<string> => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const data = {
|
|
username: credentials.username,
|
|
password: credentials.password,
|
|
oldEmail: emailData.oldEmail,
|
|
newEmail: emailData.newEmail,
|
|
};
|
|
|
|
const response = await postSetupEmail(data);
|
|
|
|
if (response?.error) {
|
|
throw new Error(response.message || "Email setup failed");
|
|
}
|
|
|
|
const message = response?.data?.message;
|
|
|
|
switch (message) {
|
|
case "Email is valid and OTP has been sent":
|
|
return "otp";
|
|
case "The old email is not same":
|
|
throw new Error("Email is invalid");
|
|
default:
|
|
return "success";
|
|
}
|
|
} catch (error: any) {
|
|
const errorMessage = error?.message || "Email setup failed";
|
|
setError(errorMessage);
|
|
showAuthError(error, "Email setup failed");
|
|
throw error;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
return {
|
|
setupEmail,
|
|
loading,
|
|
error,
|
|
};
|
|
};
|
|
|
|
// Hook for OTP verification
|
|
export const useOTPVerification = () => {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const verifyOTP = useCallback(
|
|
async (username: string, otp: string): Promise<boolean> => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
if (otp.length !== 6) {
|
|
throw new Error("OTP must be exactly 6 digits");
|
|
}
|
|
const data = {
|
|
username: username,
|
|
otpCode: otp,
|
|
};
|
|
const response = await verifyOTPByUsername(data);
|
|
|
|
if (response?.error) {
|
|
throw new Error(response.message || "OTP verification failed");
|
|
}
|
|
|
|
return response?.message === "success";
|
|
} catch (error: any) {
|
|
const errorMessage = error?.message || "OTP verification failed";
|
|
setError(errorMessage);
|
|
showAuthError(error, "OTP verification failed");
|
|
throw error;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
return {
|
|
verifyOTP,
|
|
loading,
|
|
error,
|
|
};
|
|
};
|