Compare commits

..

10 Commits

Author SHA1 Message Date
428dc50aa1 feat: oauth auth page 2025-05-24 20:59:56 +02:00
2663264f50 feat: register oauth authorize page 2025-05-24 20:59:48 +02:00
ffba961d72 fmt: don't use curly braces 2025-05-24 20:59:40 +02:00
5a939c0771 feat: link to login page to add account 2025-05-24 20:59:25 +02:00
87916f96fd feat: use oauth provider 2025-05-24 20:59:15 +02:00
c6c03e9cb6 fix: db context import 2025-05-24 20:59:04 +02:00
6fd7171450 feat: root dark overlay 2025-05-24 20:58:54 +02:00
ae07d2d3d9 feat: refactor db context 2025-05-24 20:58:36 +02:00
65545a0d71 feat: expose host 2025-05-24 20:58:09 +02:00
d423d9ba62 feat: oauth routes 2025-05-24 20:58:04 +02:00
15 changed files with 221 additions and 19 deletions

49
internal/oauth/routes.go Normal file
View File

@ -0,0 +1,49 @@
package oauth
import (
"encoding/json"
"log"
"net/http"
"gitea.local/admin/hspguard/internal/web"
"github.com/go-chi/chi/v5"
)
type OAuthHandler struct{}
func NewOAuthHandler() *OAuthHandler {
return &OAuthHandler{}
}
func (h *OAuthHandler) RegisterRoutes(r chi.Router) {
r.Get("/oauth/authorize", h.authorizeEndpoint)
r.Get("/oauth/token", h.tokenEndpoint)
}
func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
log.Println("[OAUTH] New request to token endpoint")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func (h *OAuthHandler) authorizeEndpoint(w http.ResponseWriter, r *http.Request) {
log.Println("[OAUTH] New request to authorize endpoint")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func (h *OAuthHandler) Metadata(w http.ResponseWriter, r *http.Request) {
type Response struct {
TokenEndpoint string `json:"token_endpoint"`
AuthEndpoint string `json:"authorization_endpoint"`
}
encoder := json.NewEncoder(w)
if err := encoder.Encode(Response{
TokenEndpoint: "http://192.168.178.21:3001/api/v1/oauth/token",
AuthEndpoint: "http://192.168.178.21:5173/authorize",
}); err != nil {
web.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}

View File

@ -4,9 +4,10 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
import IndexPage from "./pages/Index";
import LoginPage from "./pages/Login";
import RegisterPage from "./pages/Register";
import { useDbContext } from "./context/db/db";
import { useDbContext } from "./context/db";
import { openDB } from "idb";
import AgreementPage from "./pages/Agreement";
import OAuthAuthorizePage from "./pages/OAuthAuthorize";
const router = createBrowserRouter([
{
@ -25,6 +26,10 @@ const router = createBrowserRouter([
path: "/register",
element: <RegisterPage />,
},
{
path: "/authorize",
element: <OAuthAuthorizePage />,
},
]);
const App: FC = () => {

View File

@ -1,5 +1,5 @@
import { useCallback, useState, type FC, type ReactNode } from "react";
import { DbContext } from "./db";
import { DbContext } from ".";
import type { IDBPDatabase } from "idb";
interface IDBProvider {

View File

@ -0,0 +1,33 @@
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;
}
export const OAuthContext = createContext<OAuthContextValues>({
active: false,
clientID: "",
redirectURI: "",
scope: [],
state: "",
nonce: "",
setActive: () => {},
setClientID: () => {},
setRedirectURI: () => {},
setScope: () => {},
setState: () => {},
setNonce: () => {},
});
export const useOAuthContext = () => useContext(OAuthContext);

View File

@ -0,0 +1,36 @@
import { useState, type FC, type ReactNode } from "react";
import { OAuthContext } from ".";
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("");
return (
<OAuthContext.Provider
value={{
active,
clientID,
redirectURI,
scope,
state,
nonce,
setActive,
setClientID,
setRedirectURI,
setScope,
setState,
setNonce,
}}
>
{children}
</OAuthContext.Provider>
);
};

View File

@ -1,7 +1,8 @@
import { useDbContext } from "@/context/db/db";
import { useDbContext } from "@/context/db";
import { type LocalAccount, useAccountRepo } from "@/repository/account";
import { CirclePlus, User } from "lucide-react";
import { useEffect, useState, type FC } from "react";
import { Link } from "react-router-dom";
const AccountList: FC = () => {
const [accounts, setAccounts] = useState<LocalAccount[]>([]);
@ -72,6 +73,7 @@ const AccountList: FC = () => {
</div>
</div>
))}
<Link to="/login">
<div className="flex flex-row items-center p-4 border-gray-200 dark:border-gray-700/65 border-b border-r-0 border-l-0 select-none cursor-pointer hover:bg-gray-50/50 dark:hover:bg-gray-800/10 transition-colors mb-0">
<div>
<div className="rounded-full p-2 text-gray-900 dark:text-gray-200 mr-3">
@ -82,6 +84,7 @@ const AccountList: FC = () => {
Add new account
</p>
</div>
</Link>
</>
);
};

View File

@ -3,11 +3,14 @@ import App from "./App";
import "./index.css";
import { DbProvider } from "./context/db/provider";
import { OAuthProvider } from "./context/oauth/provider";
const root = document.getElementById("root")!;
createRoot(root).render(
<DbProvider>
<OAuthProvider>
<App />
</OAuthProvider>
</DbProvider>
);

View File

@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
const AgreementPage: FC = () => {
return (
<div
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]`}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-[425px] sm:min-w-[425px] sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">

View File

@ -10,7 +10,7 @@ const IndexPage: FC = () => {
// console.log(overlay);
return (
<div
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]`}
// style={{ backgroundImage: `url(${overlay})` }}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">

View File

@ -70,9 +70,7 @@ export default function LoginPage() {
);
return (
<div
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
>
<div className="relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]">
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-96 sm:min-w-96 sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/70 backdrop-blur-md">
<div className="flex flex-col items-center pt-16 sm:pt-0">

View File

@ -0,0 +1,74 @@
import { Card, CardContent } from "@/components/ui/card";
import { useOAuthContext } from "@/context/oauth";
import AccountList from "@/feature/AccountList";
import { useEffect, type FC } from "react";
import { useSearchParams } from "react-router-dom";
const OAuthAuthorizePage: FC = () => {
const [searchParams] = useSearchParams();
const {
setActive,
setClientID,
setRedirectURI,
setScope,
setState,
setNonce,
} = useOAuthContext();
useEffect(() => {
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") ?? "");
}, [
searchParams,
setActive,
setClientID,
setNonce,
setRedirectURI,
setScope,
setState,
]);
return (
<div
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]`}
// style={{ backgroundImage: `url(${overlay})` }}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-[700px] sm:min-w-[700px] sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">
<div className="flex sm:flex-row flex-col sm:items-stretch items-center pt-16 sm:pt-0">
<div className="flex flex-col items-center flex-1">
<img
src="/icon.png"
alt="icon"
className="w-16 h-16 mb-4 mt-2 sm:mt-6"
/>
<div className="px-4 sm:mt-4 mt-8">
<h2 className="text-2xl font-bold text-gray-800 text-left w-full dark:text-gray-100">
Select Account
</h2>
<h4 className="text-base mb-3 text-gray-400 text-left dark:text-gray-300">
Choose one of the accounts below in order to proceed to home
lab services and tools.
</h4>
</div>
</div>
{/* <LogIn className="w-8 h-8 text-gray-700 mb-4" /> */}
<CardContent className="w-full space-y-4 flex-1">
<AccountList />
</CardContent>
</div>
</Card>
</div>
</div>
);
};
export default OAuthAuthorizePage;

View File

@ -74,7 +74,7 @@ export default function RegisterPage() {
return (
<div
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(dark-overlay.jpg)]`}
className={`relative min-h-screen bg-cover bg-center bg-white dark:bg-black bg-[url(/overlay.jpg)] dark:bg-[url(/dark-overlay.jpg)]`}
>
<div className="relative z-10 flex items-center justify-center min-h-screen">
<Card className="sm:w-96 sm:min-w-96 sm:max-w-96 sm:min-h-auto p-3 min-h-screen w-full min-w-full shadow-lg bg-white/65 dark:bg-black/65 backdrop-blur-md">

View File

@ -1,4 +1,4 @@
import { useDbContext } from "@/context/db/db";
import { useDbContext } from "@/context/db";
import { deriveDeviceKey, getDeviceId } from "@/util/deviceId";
import { useCallback } from "react";

View File

@ -17,6 +17,7 @@ export default defineConfig(({ mode }) => ({
},
},
allowedHosts: true,
host: "0.0.0.0",
}
: undefined,
build: {