Compare commits

...

7 Commits

8 changed files with 92 additions and 135 deletions

View File

@ -2,6 +2,7 @@ package oauth
import (
"gitea.local/admin/hspguard/internal/config"
imiddleware "gitea.local/admin/hspguard/internal/middleware"
"gitea.local/admin/hspguard/internal/repository"
"github.com/go-chi/chi/v5"
)
@ -20,9 +21,13 @@ func NewOAuthHandler(repo *repository.Queries, cfg *config.AppConfig) *OAuthHand
func (h *OAuthHandler) RegisterRoutes(router chi.Router) {
router.Route("/oauth", func(r chi.Router) {
r.Post("/token", h.tokenEndpoint)
r.Group(func(protected chi.Router) {
authMiddleware := imiddleware.NewAuthMiddleware(h.cfg)
protected.Use(authMiddleware.Runner)
r.Post("/code", h.getAuthCode)
protected.Post("/code", h.getAuthCode)
})
r.Get("/authorize", h.AuthorizeClient)
r.Post("/token", h.tokenEndpoint)
})
}

View File

@ -25,13 +25,22 @@ const processRefreshQueue = async (token: string | null) => {
refreshQueue = [];
};
const logout = async (accountId: string) => {
const db = useDbStore.getState().db;
const requireSignIn = useAuth.getState().requireSignIn;
if (db) {
await deleteAccount(db, accountId);
}
requireSignIn?.();
};
const refreshToken = async (
accountId: string,
refreshToken: string,
): Promise<{ access: string; refresh: string }> => {
const db = useDbStore.getState().db;
const loadAccounts = useAuth.getState().loadAccounts;
const requireSignIn = useAuth.getState().requireSignIn;
if (!db) {
console.log("No database connection available.");
@ -52,8 +61,7 @@ const refreshToken = async (
return { access: response.access, refresh: response.refresh };
} catch (err) {
console.error("Token refresh failed:", err);
await deleteAccount(db, accountId);
requireSignIn?.();
await logout(accountId);
processRefreshQueue(null);
throw err;
} finally {
@ -93,6 +101,7 @@ axios.interceptors.request.use(
}
if (!token) {
await logout(account!.accountId);
throw new Error("No token available");
}

View File

@ -1,35 +0,0 @@
import { createContext, useContext } from "react";
interface OAuthContextValues {
active: boolean;
clientID: string;
redirectURI: string;
scope: string[];
state: string;
nonce: string;
setActive: (state: boolean) => void;
setClientID: (id: string) => void;
setRedirectURI: (uri: string) => void;
setScope: (scopes: string[]) => void;
setState: (state: string) => void;
setNonce: (nonce: string) => void;
selectSession: (token: string) => Promise<void>;
}
export const OAuthContext = createContext<OAuthContextValues>({
active: false,
clientID: "",
redirectURI: "",
scope: [],
state: "",
nonce: "",
setActive: () => {},
setClientID: () => {},
setRedirectURI: () => {},
setScope: () => {},
setState: () => {},
setNonce: () => {},
selectSession: async () => {},
});
export const useOAuthContext = () => useContext(OAuthContext);

View File

@ -1,54 +0,0 @@
import { useCallback, useState, type FC, type ReactNode } from "react";
import { OAuthContext } from ".";
import { codeApi } from "@/api/code";
interface IOAuthProvider {
children: ReactNode;
}
export const OAuthProvider: FC<IOAuthProvider> = ({ children }) => {
const [active, setActive] = useState(false);
const [clientID, setClientID] = useState("");
const [redirectURI, setRedirectURI] = useState("");
const [scope, setScope] = useState<string[]>([]);
const [state, setState] = useState("");
const [nonce, setNonce] = useState("");
const selectSession = useCallback(
async (token: string) => {
if (active && redirectURI) {
const codeResponse = await codeApi(token, nonce);
const params = new URLSearchParams({
code: codeResponse.code,
state,
});
window.location.replace(`${redirectURI}?${params.toString()}`);
}
},
[active, nonce, redirectURI, state],
);
return (
<OAuthContext.Provider
value={{
active,
clientID,
redirectURI,
scope,
state,
nonce,
setActive,
setClientID,
setRedirectURI,
setScope,
setState,
setNonce,
selectSession,
}}
>
{children}
</OAuthContext.Provider>
);
};

View File

@ -9,21 +9,13 @@ import {
useSearchParams,
} from "react-router";
import BackgroundLayout from "./BackgroundLayout";
import { useOAuthContext } from "@/context/oauth";
import { useOAuth } from "@/store/oauth";
const AuthLayout = () => {
const { connecting, db, connect } = useDbStore();
const [searchParams] = useSearchParams();
const {
active,
setActive,
setClientID,
setRedirectURI,
setScope,
setState,
setNonce,
} = useOAuthContext();
const parseSearchParams = useOAuth((state) => state.parseSearchParams);
const dbConnected = useMemo(() => !!db, [db]);
@ -45,7 +37,8 @@ const AuthLayout = () => {
const navigate = useNavigate();
const isAuthPage = useMemo(() => {
return location.pathname.startsWith("/auth");
const pathname = location.pathname.replace(/\/$/i, "");
return pathname !== "/auth" && pathname.startsWith("/auth");
}, [location.pathname]);
const loading = useMemo(() => {
@ -68,37 +61,28 @@ const AuthLayout = () => {
connecting,
]);
// OAuth
useEffect(() => {
if (!active) {
setActive(true);
setClientID(searchParams.get("client_id") ?? "");
setRedirectURI(searchParams.get("redirect_uri") ?? "");
const scope = searchParams.get("scope") ?? "";
setScope(scope.split(" ").filter((s) => s.length > 0));
setState(searchParams.get("state") ?? "");
setNonce(searchParams.get("nonce") ?? "");
}
}, [
active,
searchParams,
setActive,
setClientID,
setNonce,
setRedirectURI,
setScope,
setState,
]);
console.log(
"parsing url search params:",
Object.fromEntries(searchParams.entries()),
);
parseSearchParams(searchParams);
}, [parseSearchParams, searchParams]);
// Database
useEffect(() => {
connect();
}, [connect]);
// Account Manager
useEffect(() => {
if (dbConnected) {
loadAccounts();
}
}, [dbConnected, loadAccounts]);
// Fetch Profile
useEffect(() => {
if (dbConnected && !loadingAccounts && activeAccount) {
authenticate();

View File

@ -2,7 +2,6 @@ import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
import { OAuthProvider } from "./context/oauth/provider";
if (typeof window.guard !== "object") {
window.guard = {
@ -13,8 +12,4 @@ if (typeof window.guard !== "object") {
const root = document.getElementById("root")!;
createRoot(root).render(
<OAuthProvider>
<App />
</OAuthProvider>,
);
createRoot(root).render(<App />);

View File

@ -5,20 +5,20 @@ import { ArrowLeftRight, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import Avatar from "@/feature/Avatar";
import { useAuth } from "@/store/auth";
import { useOAuthContext } from "@/context/oauth";
import { useOAuth } from "@/store/oauth";
const AuthorizePage: FC = () => {
const promptAccountSelection = useAuth((state) => state.deleteActiveAccount);
const activeAccount = useAuth((state) => state.activeAccount);
const oauth = useOAuthContext();
const profile = useAuth((state) => state.profile);
const selectSession = useOAuth((state) => state.selectSession);
const handleAgree = useCallback(() => {
if (!activeAccount) return;
oauth.selectSession(activeAccount.access);
}, [activeAccount, oauth]);
selectSession(activeAccount.access);
}, [activeAccount, selectSession]);
return (
<div

53
web/src/store/oauth.ts Normal file
View File

@ -0,0 +1,53 @@
import { codeApi } from "@/api/code";
import { create } from "zustand";
export interface OAuthState {
active: boolean;
clientID: string;
redirectURI: string;
scope: string[];
state: string;
nonce: string;
parseSearchParams: (params: URLSearchParams) => void;
selectSession: (token: string) => Promise<void>;
}
export const useOAuth = create<OAuthState>((set, get) => ({
active: false,
clientID: "",
redirectURI: "",
scope: [],
state: "",
nonce: "",
parseSearchParams: (params) => {
if (get().active) return;
set({
active: true,
clientID: params.get("client_id") ?? "",
redirectURI: params.get("redirect_uri") ?? "",
scope: (params.get("scope") ?? "")
.trim()
.split(" ")
.filter((s) => s.length > 0),
state: params.get("state") ?? "",
nonce: params.get("nonce") ?? "",
});
},
selectSession: async (token) => {
const { active, redirectURI, nonce, state } = get();
if (active && redirectURI) {
const codeResponse = await codeApi(token, nonce);
const params = new URLSearchParams({
code: codeResponse.code,
state,
});
window.location.replace(`${redirectURI}?${params.toString()}`);
}
},
}));