2025-11-23 13:29:31 +00:00
|
|
|
import { API_ENDPOINTS } from "@/lib/api_urls";
|
|
|
|
|
import type {
|
|
|
|
|
RegisterInput,
|
|
|
|
|
VerifyOtpInput,
|
|
|
|
|
LoginInput,
|
|
|
|
|
ResendOtpInput,
|
|
|
|
|
ForgotPasswordInput,
|
|
|
|
|
VerifyPasswordResetOtpInput,
|
|
|
|
|
ResetPasswordInput,
|
|
|
|
|
TokenRefreshInput,
|
|
|
|
|
} from "@/lib/schema/auth";
|
|
|
|
|
import type { AuthResponse, ApiError, AuthTokens, User } from "@/lib/models/auth";
|
|
|
|
|
|
|
|
|
|
// Helper function to extract error message from API response
|
|
|
|
|
function extractErrorMessage(error: ApiError): string {
|
|
|
|
|
// Check for main error messages
|
|
|
|
|
if (error.detail) {
|
|
|
|
|
// Handle both string and array formats
|
|
|
|
|
if (Array.isArray(error.detail)) {
|
|
|
|
|
return error.detail.join(", ");
|
|
|
|
|
}
|
|
|
|
|
return String(error.detail);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error.message) {
|
|
|
|
|
if (Array.isArray(error.message)) {
|
|
|
|
|
return error.message.join(", ");
|
|
|
|
|
}
|
|
|
|
|
return String(error.message);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error.error) {
|
|
|
|
|
if (Array.isArray(error.error)) {
|
|
|
|
|
return error.error.join(", ");
|
|
|
|
|
}
|
|
|
|
|
return String(error.error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for field-specific errors (common in Django REST Framework)
|
|
|
|
|
const fieldErrors: string[] = [];
|
|
|
|
|
Object.keys(error).forEach((key) => {
|
|
|
|
|
if (key !== "detail" && key !== "message" && key !== "error") {
|
|
|
|
|
const fieldError = error[key];
|
|
|
|
|
if (Array.isArray(fieldError)) {
|
|
|
|
|
fieldErrors.push(`${key}: ${fieldError.join(", ")}`);
|
|
|
|
|
} else if (typeof fieldError === "string") {
|
|
|
|
|
fieldErrors.push(`${key}: ${fieldError}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (fieldErrors.length > 0) {
|
|
|
|
|
return fieldErrors.join(". ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "An error occurred";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper function to handle API responses
|
|
|
|
|
async function handleResponse<T>(response: Response): Promise<T> {
|
|
|
|
|
let data: any;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
data = await response.json();
|
|
|
|
|
} catch {
|
|
|
|
|
// If response is not JSON, use status text
|
|
|
|
|
throw new Error(response.statusText || "An error occurred");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const error: ApiError = data;
|
|
|
|
|
const errorMessage = extractErrorMessage(error);
|
|
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data as T;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 21:13:18 +00:00
|
|
|
// Helper function to normalize auth response
|
|
|
|
|
function normalizeAuthResponse(data: AuthResponse): AuthResponse {
|
|
|
|
|
// Normalize tokens: if tokens are at root level, move them to tokens object
|
|
|
|
|
if (data.access && data.refresh && !data.tokens) {
|
|
|
|
|
data.tokens = {
|
|
|
|
|
access: data.access,
|
|
|
|
|
refresh: data.refresh,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize user: only map isVerified to is_verified if needed
|
|
|
|
|
if (data.user) {
|
|
|
|
|
const user = data.user as any;
|
|
|
|
|
if (user.isVerified !== undefined && user.is_verified === undefined) {
|
|
|
|
|
user.is_verified = user.isVerified;
|
|
|
|
|
}
|
|
|
|
|
data.user = user;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 13:29:31 +00:00
|
|
|
// Register a new user
|
|
|
|
|
export async function registerUser(input: RegisterInput): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.register, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-23 21:13:18 +00:00
|
|
|
// Handle response - check if it's a 500 error that might indicate OTP sending failure
|
|
|
|
|
// but user registration might have succeeded
|
|
|
|
|
if (!response.ok && response.status === 500) {
|
|
|
|
|
try {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
// If the error message mentions OTP or email sending, it might be a partial success
|
|
|
|
|
const errorMessage = extractErrorMessage(data);
|
|
|
|
|
if (errorMessage.toLowerCase().includes("otp") ||
|
|
|
|
|
errorMessage.toLowerCase().includes("email") ||
|
|
|
|
|
errorMessage.toLowerCase().includes("send") ||
|
|
|
|
|
errorMessage.toLowerCase().includes("ssl") ||
|
|
|
|
|
errorMessage.toLowerCase().includes("certificate")) {
|
|
|
|
|
// Return a partial success response - user might be created, allow OTP resend
|
|
|
|
|
// This allows the user to proceed to OTP verification and use resend OTP
|
|
|
|
|
return {
|
|
|
|
|
message: "User registered, but OTP email could not be sent. Please use resend OTP.",
|
|
|
|
|
} as AuthResponse;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// If we can't parse the error, continue to normal error handling
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 13:29:31 +00:00
|
|
|
return handleResponse<AuthResponse>(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify OTP
|
|
|
|
|
export async function verifyOtp(input: VerifyOtpInput): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.verifyOtp, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await handleResponse<AuthResponse>(response);
|
2025-11-23 21:13:18 +00:00
|
|
|
return normalizeAuthResponse(data);
|
2025-11-23 13:29:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Login user
|
|
|
|
|
export async function loginUser(input: LoginInput): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.login, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await handleResponse<AuthResponse>(response);
|
2025-11-23 21:13:18 +00:00
|
|
|
return normalizeAuthResponse(data);
|
2025-11-23 13:29:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resend OTP
|
|
|
|
|
export async function resendOtp(input: ResendOtpInput): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.resendOtp, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return handleResponse<AuthResponse>(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Forgot password
|
|
|
|
|
export async function forgotPassword(input: ForgotPasswordInput): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.forgotPassword, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return handleResponse<AuthResponse>(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify password reset OTP
|
|
|
|
|
export async function verifyPasswordResetOtp(
|
|
|
|
|
input: VerifyPasswordResetOtpInput
|
|
|
|
|
): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.verifyPasswordResetOtp, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return handleResponse<AuthResponse>(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset password
|
|
|
|
|
export async function resetPassword(input: ResetPasswordInput): Promise<AuthResponse> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.resetPassword, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return handleResponse<AuthResponse>(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh access token
|
|
|
|
|
export async function refreshToken(input: TokenRefreshInput): Promise<AuthTokens> {
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.tokenRefresh, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(input),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return handleResponse<AuthTokens>(response);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
// Decode JWT token to check expiration
|
|
|
|
|
function decodeJWT(token: string): { exp?: number; [key: string]: any } | null {
|
|
|
|
|
try {
|
|
|
|
|
const parts = token.split(".");
|
|
|
|
|
if (parts.length !== 3) return null;
|
|
|
|
|
|
|
|
|
|
const payload = parts[1];
|
|
|
|
|
const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
|
|
|
|
|
return decoded;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if token is expired
|
|
|
|
|
export function isTokenExpired(token: string | null): boolean {
|
|
|
|
|
if (!token) return true;
|
|
|
|
|
|
|
|
|
|
const decoded = decodeJWT(token);
|
|
|
|
|
if (!decoded || !decoded.exp) return true;
|
|
|
|
|
|
|
|
|
|
// exp is in seconds, Date.now() is in milliseconds
|
|
|
|
|
const expirationTime = decoded.exp * 1000;
|
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
|
|
|
|
// Consider token expired if it expires within the next 5 seconds (buffer)
|
|
|
|
|
return currentTime >= (expirationTime - 5000);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 13:29:31 +00:00
|
|
|
// Get stored tokens
|
|
|
|
|
export function getStoredTokens(): { access: string | null; refresh: string | null } {
|
|
|
|
|
if (typeof window === "undefined") {
|
|
|
|
|
return { access: null, refresh: null };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
access: localStorage.getItem("auth_access_token"),
|
|
|
|
|
refresh: localStorage.getItem("auth_refresh_token"),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
// Check if user has valid authentication
|
|
|
|
|
export function hasValidAuth(): boolean {
|
|
|
|
|
const tokens = getStoredTokens();
|
|
|
|
|
if (!tokens.access) return false;
|
|
|
|
|
|
|
|
|
|
return !isTokenExpired(tokens.access);
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 13:29:31 +00:00
|
|
|
// Store tokens
|
|
|
|
|
export function storeTokens(tokens: AuthTokens): void {
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
|
|
|
|
localStorage.setItem("auth_access_token", tokens.access);
|
|
|
|
|
localStorage.setItem("auth_refresh_token", tokens.refresh);
|
|
|
|
|
|
|
|
|
|
// Also set cookies for middleware
|
|
|
|
|
document.cookie = `auth_access_token=${tokens.access}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
|
|
|
|
document.cookie = `auth_refresh_token=${tokens.refresh}; path=/; max-age=${30 * 24 * 60 * 60}; SameSite=Lax`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store user
|
|
|
|
|
export function storeUser(user: User): void {
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
|
|
|
|
localStorage.setItem("auth_user", JSON.stringify(user));
|
2025-11-23 21:13:18 +00:00
|
|
|
document.cookie = `auth_user=${encodeURIComponent(JSON.stringify(user))}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
|
2025-11-23 13:29:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get stored user
|
|
|
|
|
export function getStoredUser(): User | null {
|
|
|
|
|
if (typeof window === "undefined") return null;
|
|
|
|
|
|
|
|
|
|
const userStr = localStorage.getItem("auth_user");
|
|
|
|
|
if (!userStr) return null;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(userStr) as User;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear auth data
|
|
|
|
|
export function clearAuthData(): void {
|
|
|
|
|
if (typeof window === "undefined") return;
|
|
|
|
|
|
|
|
|
|
localStorage.removeItem("auth_access_token");
|
|
|
|
|
localStorage.removeItem("auth_refresh_token");
|
|
|
|
|
localStorage.removeItem("auth_user");
|
|
|
|
|
|
|
|
|
|
// Also clear cookies
|
|
|
|
|
document.cookie = "auth_access_token=; path=/; max-age=0";
|
|
|
|
|
document.cookie = "auth_refresh_token=; path=/; max-age=0";
|
|
|
|
|
document.cookie = "auth_user=; path=/; max-age=0";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get auth header for API requests
|
|
|
|
|
export function getAuthHeader(): { Authorization: string } | {} {
|
|
|
|
|
const tokens = getStoredTokens();
|
2025-11-23 22:28:02 +00:00
|
|
|
if (tokens.access && !isTokenExpired(tokens.access)) {
|
2025-11-23 13:29:31 +00:00
|
|
|
return { Authorization: `Bearer ${tokens.access}` };
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-23 22:28:02 +00:00
|
|
|
// Get all users (Admin only)
|
|
|
|
|
export async function getAllUsers(): Promise<User[]> {
|
|
|
|
|
const tokens = getStoredTokens();
|
|
|
|
|
|
|
|
|
|
if (!tokens.access) {
|
|
|
|
|
throw new Error("Authentication required.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch(API_ENDPOINTS.auth.allUsers, {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
Authorization: `Bearer ${tokens.access}`,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const errorMessage = extractErrorMessage(data);
|
|
|
|
|
throw new Error(errorMessage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle different response formats
|
|
|
|
|
if (data.users) {
|
|
|
|
|
return data.users;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(data)) {
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|