app-p3qnvc/frontend/src/context/AuthContext.tsx
2026-09-29 19:38:32 +05:30

155 lines
4.8 KiB
TypeScript

import React, { createContext, useContext, useState, useEffect } from 'react';
import { api } from '../api';
import { authLink } from '../lib/mAuth';
export interface UserData {
username: string;
fullName?: string;
full_name?: string;
email: string;
phone: string;
citizen_id: string;
[key: string]: any;
}
interface AuthContextType {
user: UserData | null;
token: string | null;
login: () => Promise<void>;
checkAuth: () => Promise<UserData | null>;
logout: () => void;
updateUser: (userData: UserData) => void;
isAuthenticated: boolean;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<UserData | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const updateUser = (newUserData: UserData) => {
setUser(newUserData);
localStorage.setItem('auth_user', JSON.stringify(newUserData));
};
/**
* Calls the backend check-in endpoint to sync the mAuth user
* with the local database and get the full user profile.
*/
const handleCheckIn = async (authToken: string) => {
try {
const currentOrigin = localStorage.getItem('mworld_origin') || window.location.origin;
const res = await api.post('/user/check-in', {
mauth_token: authToken,
});
if (res.data?.success && res.data.user) {
setUser(res.data.user);
localStorage.setItem('auth_user', JSON.stringify(res.data.user));
return res.data.user;
}
} catch (err) {
console.error("Check-in failed:", err);
logout();
}
return null;
};
// Public compatibility method for generated onboarding and role guards.
// It always reuses the scaffolded check-in path instead of inventing a
// second token-validation flow in individual pages.
const checkAuth = async (): Promise<UserData | null> => {
const authToken = token || localStorage.getItem('user_auth_token') || localStorage.getItem('auth_token');
if (!authToken) return null;
setToken(authToken);
return handleCheckIn(authToken);
};
/**
* mAuth Native Initializer:
* 1. Check for user_auth_token in localStorage (set by /auth route).
* 2. If present, call backend check-in to validate & sync user data.
* The backend handles mAuth token verification — we must NOT call
* fetchUserData here too, as that would consume/invalidate the token
* before the backend can use it.
* 3. If absent or invalid, clear auth state so the login button is shown.
*/
useEffect(() => {
const initializer = async () => {
const origin = window.location.origin.toString();
if (!localStorage.getItem('mworld_origin')) {
localStorage.setItem('mworld_origin', origin);
}
const uat = localStorage.getItem('user_auth_token');
if (uat) {
setToken(uat);
// Restore cached user data immediately for fast UI
const savedUser = localStorage.getItem('auth_user');
if (savedUser && savedUser !== 'undefined') {
try {
setUser(JSON.parse(savedUser));
} catch { /* ignore parse errors */ }
}
// Validate token & sync user via backend check-in
// (backend calls mAuth internally to verify the token)
const freshUser = await handleCheckIn(uat);
if (!freshUser) {
// Token is invalid or check-in failed — clean up
localStorage.removeItem('user_auth_token');
logout();
}
}
// If no token present, user stays logged out (login button shown)
setIsLoading(false);
};
initializer();
}, []);
/**
* Triggers the mAuth native login flow:
* Generates an auth link and redirects the user to mAuth in the same window.
*/
const login = async (): Promise<void> => {
setIsLoading(true);
try {
const currentOrigin = localStorage.getItem('mworld_origin') || window.location.origin;
const link = await authLink(currentOrigin);
if (!link) {
setIsLoading(false);
return;
}
// Native flow: redirect in same window (not window.open)
window.location.href = link;
} catch (error) {
console.error("Login failed:", error);
setIsLoading(false);
}
};
const logout = () => {
setUser(null);
setToken(null);
localStorage.removeItem('auth_user');
localStorage.removeItem('user_auth_token');
};
return (
<AuthContext.Provider value={{ user, token, login, checkAuth, logout, updateUser, isAuthenticated: !!user, isLoading }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
};