project setup
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import { getApiUrl, baseApi } from "@/store/api/baseApi";
|
||||
import { getToken, setToken, loadPersistedToken } from "@/auth/token";
|
||||
import { store } from "@/store/index";
|
||||
import { User } from "@/store/models/User.model";
|
||||
|
||||
WebBrowser.maybeCompleteAuthSession();
|
||||
|
||||
type AuthContextType = {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
loading: boolean;
|
||||
bootstrapping: boolean;
|
||||
isAuthenticated: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (p: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const useAuthContext = () => {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuthContext must be used within AuthProvider");
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export type AuthProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [tokenState, setTokenState] = useState<string | null>(getToken());
|
||||
const [bootstrapping, setBootstrapping] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isAuthenticated = !!tokenState && !!user;
|
||||
|
||||
const syncUser = async (jwt?: string | null) => {
|
||||
const useToken = jwt ?? tokenState;
|
||||
if (!useToken) return;
|
||||
try {
|
||||
const res = await fetch(`${getApiUrl()?.replace(/\/$/, "")}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${useToken}` },
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to fetch current user");
|
||||
const u: User = await res.json();
|
||||
setUser(u);
|
||||
} catch (e) {
|
||||
console.warn("/auth/me failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const t = await loadPersistedToken();
|
||||
setTokenState(t);
|
||||
if (t) await syncUser(t);
|
||||
} finally {
|
||||
setBootstrapping(false);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const login: AuthContextType["login"] = async (email, password) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${getApiUrl()?.replace(/\/$/, "")}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const status = res.status;
|
||||
let msg = "Invalid credentials";
|
||||
try {
|
||||
const errData = await res.json();
|
||||
msg = errData?.message || errData?.error || msg;
|
||||
} catch {
|
||||
// non-JSON response, keep default msg
|
||||
}
|
||||
// Only replace the message for explicit 'not activated' cases
|
||||
if (status === 403 && /not activated/i.test(msg)) {
|
||||
msg =
|
||||
"Account is not activated. Please use your invitation link to set a password.";
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
const data = await res.json();
|
||||
await setToken(data.token);
|
||||
setTokenState(data.token);
|
||||
setUser(data.user);
|
||||
// Clear/refresh caches after login for correct user context
|
||||
store.dispatch(baseApi.util.resetApiState());
|
||||
store.dispatch(baseApi.util.invalidateTags(["Users", "Todos"]));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const register: AuthContextType["register"] = async ({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
password,
|
||||
}) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${getApiUrl()?.replace(/\/$/, "")}/auth/register`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ firstName, lastName, email, password }),
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error("Registration failed");
|
||||
const data = await res.json();
|
||||
await setToken(data.token);
|
||||
setTokenState(data.token);
|
||||
setUser(data.user);
|
||||
// Invalidate users cache to refresh activation statuses after register
|
||||
store.dispatch(baseApi.util.invalidateTags(["Users"]));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const logout: AuthContextType["logout"] = async () => {
|
||||
await setToken(null);
|
||||
setTokenState(null);
|
||||
setUser(null);
|
||||
// Clear RTK Query caches on logout to prevent stale cross-user data
|
||||
store.dispatch(baseApi.util.resetApiState());
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
user,
|
||||
token: tokenState,
|
||||
loading,
|
||||
bootstrapping,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user