155 lines
4.8 KiB
TypeScript
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;
|
|
};
|