website/lib/actions/auth.ts

442 lines
12 KiB
TypeScript

import { API_ENDPOINTS } from "@/lib/api_urls";
import type {
RegisterInput,
VerifyOtpInput,
LoginInput,
ResendOtpInput,
ForgotPasswordInput,
VerifyPasswordResetOtpInput,
ResetPasswordInput,
TokenRefreshInput,
UpdateProfileInput,
} 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;
}
// 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;
}
// 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),
});
// 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
}
}
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);
return normalizeAuthResponse(data);
}
// 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);
return normalizeAuthResponse(data);
}
// 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);
}
// 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);
}
// 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"),
};
}
// Check if user has valid authentication
export function hasValidAuth(): boolean {
const tokens = getStoredTokens();
if (!tokens.access) return false;
return !isTokenExpired(tokens.access);
}
// 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));
document.cookie = `auth_user=${encodeURIComponent(JSON.stringify(user))}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
}
// 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();
if (tokens.access && !isTokenExpired(tokens.access)) {
return { Authorization: `Bearer ${tokens.access}` };
}
return {};
}
// 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 [];
}
// Get user profile
export async function getProfile(): Promise<User> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(API_ENDPOINTS.auth.getProfile, {
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.user) {
return data.user;
}
if (data.id) {
return data;
}
throw new Error("Invalid profile response format");
}
// Update user profile
export async function updateProfile(input: UpdateProfileInput): Promise<User> {
const tokens = getStoredTokens();
if (!tokens.access) {
throw new Error("Authentication required.");
}
const response = await fetch(API_ENDPOINTS.auth.updateProfile, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokens.access}`,
},
body: JSON.stringify(input),
});
const data = await response.json();
if (!response.ok) {
const errorMessage = extractErrorMessage(data);
throw new Error(errorMessage);
}
// Handle different response formats
if (data.user) {
return data.user;
}
if (data.id) {
return data;
}
throw new Error("Invalid profile response format");
}