Compare commits
7 Commits
9ee30d1e23
...
0efc90567b
Author | SHA1 | Date | |
---|---|---|---|
0efc90567b | |||
a5466f1b10 | |||
8e946cbee5 | |||
a3a6b5e4d7 | |||
ad0a0f5626 | |||
2389058ddc | |||
ce44ef3e62 |
@ -2,6 +2,7 @@ package oauth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.local/admin/hspguard/internal/config"
|
"gitea.local/admin/hspguard/internal/config"
|
||||||
|
imiddleware "gitea.local/admin/hspguard/internal/middleware"
|
||||||
"gitea.local/admin/hspguard/internal/repository"
|
"gitea.local/admin/hspguard/internal/repository"
|
||||||
"github.com/go-chi/chi/v5"
|
"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) {
|
func (h *OAuthHandler) RegisterRoutes(router chi.Router) {
|
||||||
router.Route("/oauth", func(r 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.Get("/authorize", h.AuthorizeClient)
|
||||||
|
r.Post("/token", h.tokenEndpoint)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,22 @@ const processRefreshQueue = async (token: string | null) => {
|
|||||||
refreshQueue = [];
|
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 (
|
const refreshToken = async (
|
||||||
accountId: string,
|
accountId: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
): Promise<{ access: string; refresh: string }> => {
|
): Promise<{ access: string; refresh: string }> => {
|
||||||
const db = useDbStore.getState().db;
|
const db = useDbStore.getState().db;
|
||||||
const loadAccounts = useAuth.getState().loadAccounts;
|
const loadAccounts = useAuth.getState().loadAccounts;
|
||||||
const requireSignIn = useAuth.getState().requireSignIn;
|
|
||||||
|
|
||||||
if (!db) {
|
if (!db) {
|
||||||
console.log("No database connection available.");
|
console.log("No database connection available.");
|
||||||
@ -52,8 +61,7 @@ const refreshToken = async (
|
|||||||
return { access: response.access, refresh: response.refresh };
|
return { access: response.access, refresh: response.refresh };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Token refresh failed:", err);
|
console.error("Token refresh failed:", err);
|
||||||
await deleteAccount(db, accountId);
|
await logout(accountId);
|
||||||
requireSignIn?.();
|
|
||||||
processRefreshQueue(null);
|
processRefreshQueue(null);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -93,6 +101,7 @@ axios.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
await logout(account!.accountId);
|
||||||
throw new Error("No token available");
|
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,
|
useSearchParams,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
import BackgroundLayout from "./BackgroundLayout";
|
import BackgroundLayout from "./BackgroundLayout";
|
||||||
import { useOAuthContext } from "@/context/oauth";
|
import { useOAuth } from "@/store/oauth";
|
||||||
|
|
||||||
const AuthLayout = () => {
|
const AuthLayout = () => {
|
||||||
const { connecting, db, connect } = useDbStore();
|
const { connecting, db, connect } = useDbStore();
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const {
|
const parseSearchParams = useOAuth((state) => state.parseSearchParams);
|
||||||
active,
|
|
||||||
setActive,
|
|
||||||
setClientID,
|
|
||||||
setRedirectURI,
|
|
||||||
setScope,
|
|
||||||
setState,
|
|
||||||
setNonce,
|
|
||||||
} = useOAuthContext();
|
|
||||||
|
|
||||||
const dbConnected = useMemo(() => !!db, [db]);
|
const dbConnected = useMemo(() => !!db, [db]);
|
||||||
|
|
||||||
@ -45,7 +37,8 @@ const AuthLayout = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const isAuthPage = useMemo(() => {
|
const isAuthPage = useMemo(() => {
|
||||||
return location.pathname.startsWith("/auth");
|
const pathname = location.pathname.replace(/\/$/i, "");
|
||||||
|
return pathname !== "/auth" && pathname.startsWith("/auth");
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
const loading = useMemo(() => {
|
const loading = useMemo(() => {
|
||||||
@ -68,37 +61,28 @@ const AuthLayout = () => {
|
|||||||
connecting,
|
connecting,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// OAuth
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active) {
|
console.log(
|
||||||
setActive(true);
|
"parsing url search params:",
|
||||||
setClientID(searchParams.get("client_id") ?? "");
|
Object.fromEntries(searchParams.entries()),
|
||||||
setRedirectURI(searchParams.get("redirect_uri") ?? "");
|
);
|
||||||
const scope = searchParams.get("scope") ?? "";
|
parseSearchParams(searchParams);
|
||||||
setScope(scope.split(" ").filter((s) => s.length > 0));
|
}, [parseSearchParams, searchParams]);
|
||||||
setState(searchParams.get("state") ?? "");
|
|
||||||
setNonce(searchParams.get("nonce") ?? "");
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
active,
|
|
||||||
searchParams,
|
|
||||||
setActive,
|
|
||||||
setClientID,
|
|
||||||
setNonce,
|
|
||||||
setRedirectURI,
|
|
||||||
setScope,
|
|
||||||
setState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
// Database
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connect();
|
connect();
|
||||||
}, [connect]);
|
}, [connect]);
|
||||||
|
|
||||||
|
// Account Manager
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dbConnected) {
|
if (dbConnected) {
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
}
|
}
|
||||||
}, [dbConnected, loadAccounts]);
|
}, [dbConnected, loadAccounts]);
|
||||||
|
|
||||||
|
// Fetch Profile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dbConnected && !loadingAccounts && activeAccount) {
|
if (dbConnected && !loadingAccounts && activeAccount) {
|
||||||
authenticate();
|
authenticate();
|
||||||
|
@ -2,7 +2,6 @@ import { createRoot } from "react-dom/client";
|
|||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { OAuthProvider } from "./context/oauth/provider";
|
|
||||||
|
|
||||||
if (typeof window.guard !== "object") {
|
if (typeof window.guard !== "object") {
|
||||||
window.guard = {
|
window.guard = {
|
||||||
@ -13,8 +12,4 @@ if (typeof window.guard !== "object") {
|
|||||||
|
|
||||||
const root = document.getElementById("root")!;
|
const root = document.getElementById("root")!;
|
||||||
|
|
||||||
createRoot(root).render(
|
createRoot(root).render(<App />);
|
||||||
<OAuthProvider>
|
|
||||||
<App />
|
|
||||||
</OAuthProvider>,
|
|
||||||
);
|
|
||||||
|
@ -5,20 +5,20 @@ import { ArrowLeftRight, ChevronDown } from "lucide-react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Avatar from "@/feature/Avatar";
|
import Avatar from "@/feature/Avatar";
|
||||||
import { useAuth } from "@/store/auth";
|
import { useAuth } from "@/store/auth";
|
||||||
import { useOAuthContext } from "@/context/oauth";
|
import { useOAuth } from "@/store/oauth";
|
||||||
|
|
||||||
const AuthorizePage: FC = () => {
|
const AuthorizePage: FC = () => {
|
||||||
const promptAccountSelection = useAuth((state) => state.deleteActiveAccount);
|
const promptAccountSelection = useAuth((state) => state.deleteActiveAccount);
|
||||||
const activeAccount = useAuth((state) => state.activeAccount);
|
const activeAccount = useAuth((state) => state.activeAccount);
|
||||||
|
|
||||||
const oauth = useOAuthContext();
|
|
||||||
|
|
||||||
const profile = useAuth((state) => state.profile);
|
const profile = useAuth((state) => state.profile);
|
||||||
|
|
||||||
|
const selectSession = useOAuth((state) => state.selectSession);
|
||||||
|
|
||||||
const handleAgree = useCallback(() => {
|
const handleAgree = useCallback(() => {
|
||||||
if (!activeAccount) return;
|
if (!activeAccount) return;
|
||||||
oauth.selectSession(activeAccount.access);
|
selectSession(activeAccount.access);
|
||||||
}, [activeAccount, oauth]);
|
}, [activeAccount, selectSession]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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