Compare commits
7 Commits
9ee30d1e23
...
0efc90567b
Author | SHA1 | Date | |
---|---|---|---|
0efc90567b | |||
a5466f1b10 | |||
8e946cbee5 | |||
a3a6b5e4d7 | |||
ad0a0f5626 | |||
2389058ddc | |||
ce44ef3e62 |
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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);
|
@ -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>
|
||||
);
|
||||
};
|
@ -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();
|
||||
|
@ -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 />);
|
||||
|
@ -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
53
web/src/store/oauth.ts
Normal 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()}`);
|
||||
}
|
||||
},
|
||||
}));
|
Reference in New Issue
Block a user