feat: PKCE auth + CI/CD deploy
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 25s
Some checks failed
CI/CD — Build & Deploy / Build & Deploy (push) Failing after 25s
- Frontend: PKCE flow (oauth.js, api.js centralized, cookie-based AuthContext) - Backend: token introspection, cookies httpOnly, refresh endpoint - Replaced localStorage JWT with httpOnly session cookies - useSaveSync migrated to cookie auth - cookie-parser added - Gitea CI workflow (vps-runner pattern)
This commit is contained in:
@@ -1,166 +1,79 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import PropTypes from "prop-types";
|
||||
const decodeJwtPayload = (token) =>
|
||||
JSON.parse(atob(token.split(".")[1]));
|
||||
import React, { createContext, useContext, useState, useMemo, useEffect } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { apiFetch } from "../lib/api";
|
||||
|
||||
const AuthContext = createContext();
|
||||
const AuthContext = createContext();
|
||||
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const jwtToken = localStorage.getItem("token");
|
||||
|
||||
if (jwtToken) {
|
||||
try {
|
||||
const decodedPayload = decodeJwtPayload(jwtToken);
|
||||
const res = await fetch(
|
||||
`${import.meta.env.VITE_BACKEND_URL}/api/users/${decodedPayload.user}`,
|
||||
{
|
||||
headers: { "x-auth-token": jwtToken },
|
||||
}
|
||||
);
|
||||
if (!res.ok) throw new Error("Failed to fetch user");
|
||||
const data = await res.json();
|
||||
setUser(data);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
localStorage.removeItem("token");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const loginWithOAuth = async (token) => {
|
||||
const res = await fetch(
|
||||
`${import.meta.env.VITE_BACKEND_URL}/api/auth/callback?code=${encodeURIComponent(token)}`
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(data.message || "OAuth login failed");
|
||||
}
|
||||
|
||||
localStorage.setItem("token", data.token);
|
||||
setUser(data.user);
|
||||
return data.user;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem("token");
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const data = await apiFetch("/auth/me");
|
||||
setUser(data);
|
||||
} catch {
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const editUser = async (updatedFields) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_BACKEND_URL}/api/users/${user.id}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-auth-token": localStorage.getItem("token"),
|
||||
},
|
||||
body: JSON.stringify(updatedFields),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const updatedUser = await response.json();
|
||||
setUser((prevUser) => ({
|
||||
...prevUser,
|
||||
...updatedUser.user,
|
||||
}));
|
||||
return "User updated successfully";
|
||||
}
|
||||
if (response.status === 400) {
|
||||
console.error("Bad Request:", response.statusText);
|
||||
throw new Error("Bad Request");
|
||||
} else if (response.status === 401) {
|
||||
console.error("Unauthorized:", response.statusText);
|
||||
throw new Error("Unauthorized");
|
||||
} else {
|
||||
console.error("Error updating user:", response.statusText);
|
||||
throw new Error("Error updating user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
throw new Error("An error occurred during user update");
|
||||
}
|
||||
};
|
||||
|
||||
const sendPasswordResetEmail = async (email) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${import.meta.env.VITE_BACKEND_URL}/api/forgot-password`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ mail: email }),
|
||||
}
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
return "Password reset email sent successfully";
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
throw new Error(data.message || "Error sending password reset email");
|
||||
} catch (error) {
|
||||
console.error("Error sending password reset email:", error);
|
||||
throw new Error(
|
||||
"An error occurred while sending the password reset email"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const authContextValue = useMemo(() => {
|
||||
return {
|
||||
user,
|
||||
loading,
|
||||
logout,
|
||||
loginWithOAuth,
|
||||
editUser,
|
||||
sendPasswordResetEmail,
|
||||
setUser: (newUser) => {
|
||||
setUser(newUser);
|
||||
},
|
||||
};
|
||||
}, [user, loading, logout]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={authContextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
AuthProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { AuthProvider, useAuth };
|
||||
useEffect(() => {
|
||||
refresh().finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onExpired = () => setUser(null);
|
||||
window.addEventListener("auth:expired", onExpired);
|
||||
return () => window.removeEventListener("auth:expired", onExpired);
|
||||
}, []);
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await apiFetch("/auth/logout", { method: "POST" });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const editUser = async (updatedFields) => {
|
||||
const data = await apiFetch(`/users/${user.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(updatedFields),
|
||||
});
|
||||
setUser((prev) => ({ ...prev, ...data.user }));
|
||||
return "User updated successfully";
|
||||
};
|
||||
|
||||
const authContextValue = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
loading,
|
||||
logout,
|
||||
refresh,
|
||||
editUser,
|
||||
setUser: (newUser) => setUser(newUser),
|
||||
}),
|
||||
[user, loading]
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={authContextValue}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
AuthProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { AuthProvider, useAuth };
|
||||
|
||||
Reference in New Issue
Block a user