Init first files

This commit is contained in:
2026-02-12 09:09:13 +01:00
parent 6535699b0e
commit d1d8ae43a4
61 changed files with 2424 additions and 0 deletions

89
frontend/src/state.jsx Normal file
View File

@@ -0,0 +1,89 @@
import React, { createContext, useContext, useMemo, useState } from "react";
import { API_URL } from "./api";
const AuthCtx = createContext(null);
function loadStorage() {
try {
return JSON.parse(localStorage.getItem("nexapg_auth") || "null");
} catch {
return null;
}
}
export function AuthProvider({ children }) {
const initial = loadStorage();
const [tokens, setTokens] = useState(initial?.tokens || null);
const [me, setMe] = useState(initial?.me || null);
const persist = (nextTokens, nextMe) => {
if (nextTokens && nextMe) {
localStorage.setItem("nexapg_auth", JSON.stringify({ tokens: nextTokens, me: nextMe }));
} else {
localStorage.removeItem("nexapg_auth");
}
};
const refresh = async () => {
if (!tokens?.refreshToken) return null;
const res = await fetch(`${API_URL}/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: tokens.refreshToken }),
});
if (!res.ok) {
setTokens(null);
setMe(null);
persist(null, null);
return null;
}
const data = await res.json();
const nextTokens = { accessToken: data.access_token, refreshToken: data.refresh_token };
setTokens(nextTokens);
persist(nextTokens, me);
return nextTokens;
};
const login = async (email, password) => {
const res = await fetch(`${API_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error("Login failed");
const data = await res.json();
const nextTokens = { accessToken: data.access_token, refreshToken: data.refresh_token };
const meRes = await fetch(`${API_URL}/me`, {
headers: { Authorization: `Bearer ${nextTokens.accessToken}` },
});
if (!meRes.ok) throw new Error("Could not load user profile");
const profile = await meRes.json();
setTokens(nextTokens);
setMe(profile);
persist(nextTokens, profile);
};
const logout = async () => {
try {
if (tokens?.accessToken) {
await fetch(`${API_URL}/auth/logout`, {
method: "POST",
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
}
} finally {
setTokens(null);
setMe(null);
persist(null, null);
}
};
const value = useMemo(() => ({ tokens, me, login, logout, refresh }), [tokens, me]);
return <AuthCtx.Provider value={value}>{children}</AuthCtx.Provider>;
}
export function useAuth() {
const ctx = useContext(AuthCtx);
if (!ctx) throw new Error("useAuth must be used inside AuthProvider");
return ctx;
}