project setup

This commit is contained in:
Sone
2025-11-02 10:08:56 +01:00
commit 568838003e
174 changed files with 27100 additions and 0 deletions
+169
View File
@@ -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>
);
};
+37
View File
@@ -0,0 +1,37 @@
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";
let inMemoryToken: string | null = null;
export const getToken = (): string | null => inMemoryToken;
export const setToken = async (token: string | null) => {
inMemoryToken = token;
try {
if (Platform.OS === "web") {
if (token) localStorage.setItem("auth_token", token);
else localStorage.removeItem("auth_token");
} else {
if (token) await SecureStore.setItemAsync("auth_token", token);
else await SecureStore.deleteItemAsync("auth_token");
}
} catch (e) {
console.warn("Token persistence error:", e);
}
};
export const loadPersistedToken = async (): Promise<string | null> => {
try {
if (Platform.OS === "web") {
const t = localStorage.getItem("auth_token");
inMemoryToken = t;
return t;
}
const t = await SecureStore.getItemAsync("auth_token");
inMemoryToken = t;
return t;
} catch (e) {
console.warn("Token load error:", e);
return null;
}
};