diff --git a/devfront/src/app/routes.tsx b/devfront/src/app/routes.tsx
index 0bbba406..7da4c279 100644
--- a/devfront/src/app/routes.tsx
+++ b/devfront/src/app/routes.tsx
@@ -1,5 +1,8 @@
import { Navigate, createBrowserRouter } from "react-router-dom";
import AppLayout from "../components/layout/AppLayout";
+import AuthCallbackPage from "../features/auth/AuthCallbackPage";
+import AuthGuard from "../features/auth/AuthGuard";
+import LoginPage from "../features/auth/LoginPage";
import ClientConsentsPage from "../features/clients/ClientConsentsPage";
import ClientDetailsPage from "../features/clients/ClientDetailsPage";
import ClientGeneralPage from "../features/clients/ClientGeneralPage";
@@ -7,16 +10,29 @@ import ClientsPage from "../features/clients/ClientsPage";
export const router = createBrowserRouter(
[
+ {
+ path: "/login",
+ element: ,
+ },
+ {
+ path: "/callback",
+ element: ,
+ },
{
path: "/",
- element: ,
+ element: ,
children: [
- { index: true, element: },
- { path: "clients", element: },
- { path: "clients/new", element: },
- { path: "clients/:id", element: },
- { path: "clients/:id/consents", element: },
- { path: "clients/:id/settings", element: },
+ {
+ element: ,
+ children: [
+ { index: true, element: },
+ { path: "clients", element: },
+ { path: "clients/new", element: },
+ { path: "clients/:id", element: },
+ { path: "clients/:id/consents", element: },
+ { path: "clients/:id/settings", element: },
+ ],
+ },
],
},
],
diff --git a/devfront/src/features/auth/AuthCallbackPage.tsx b/devfront/src/features/auth/AuthCallbackPage.tsx
new file mode 100644
index 00000000..638a7924
--- /dev/null
+++ b/devfront/src/features/auth/AuthCallbackPage.tsx
@@ -0,0 +1,19 @@
+import { useEffect } from "react";
+import { useAuth } from "react-oidc-context";
+import { useNavigate } from "react-router-dom";
+
+export default function AuthCallbackPage() {
+ const auth = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (auth.isAuthenticated) {
+ navigate("/", { replace: true });
+ } else if (auth.error) {
+ console.error("Auth Error:", auth.error);
+ navigate("/login", { replace: true });
+ }
+ }, [auth.isAuthenticated, auth.error, navigate]);
+
+ return
Loading Auth...
;
+}
diff --git a/devfront/src/features/auth/AuthGuard.tsx b/devfront/src/features/auth/AuthGuard.tsx
new file mode 100644
index 00000000..50abe838
--- /dev/null
+++ b/devfront/src/features/auth/AuthGuard.tsx
@@ -0,0 +1,20 @@
+import { useAuth } from "react-oidc-context";
+import { Navigate, Outlet } from "react-router-dom";
+
+export default function AuthGuard() {
+ const auth = useAuth();
+
+ if (auth.isLoading) {
+ return Loading...
;
+ }
+
+ if (auth.error) {
+ return Auth Error: {auth.error.message}
;
+ }
+
+ if (!auth.isAuthenticated) {
+ return ;
+ }
+
+ return ;
+}
diff --git a/devfront/src/features/auth/LoginPage.tsx b/devfront/src/features/auth/LoginPage.tsx
new file mode 100644
index 00000000..212448ee
--- /dev/null
+++ b/devfront/src/features/auth/LoginPage.tsx
@@ -0,0 +1,23 @@
+import { useAuth } from "react-oidc-context";
+
+export default function LoginPage() {
+ const auth = useAuth();
+
+ const handleLogin = () => {
+ auth.signinRedirect();
+ };
+
+ return (
+
+
+
DevFront Login
+
+
+
+ );
+}
diff --git a/devfront/src/features/auth/authApi.ts b/devfront/src/features/auth/authApi.ts
new file mode 100644
index 00000000..9fa27955
--- /dev/null
+++ b/devfront/src/features/auth/authApi.ts
@@ -0,0 +1,22 @@
+import apiClient from "../../lib/apiClient";
+
+export interface Tenant {
+ id: string;
+ name: string;
+ slug: string;
+}
+
+export interface UserProfile {
+ id: string;
+ email: string;
+ name: string;
+ role: string;
+ companyCode?: string;
+ tenantId?: string;
+ tenant?: Tenant;
+}
+
+export async function fetchMe() {
+ const { data } = await apiClient.get("/user/me");
+ return data;
+}
diff --git a/devfront/src/lib/auth.ts b/devfront/src/lib/auth.ts
new file mode 100644
index 00000000..c9582c9c
--- /dev/null
+++ b/devfront/src/lib/auth.ts
@@ -0,0 +1,20 @@
+import { UserManager, WebStorageStateStore } from "oidc-client-ts";
+import type { AuthProviderProps } from "react-oidc-context";
+
+export const oidcConfig: AuthProviderProps = {
+ authority: import.meta.env.VITE_OIDC_AUTHORITY || "http://localhost:3000/api/v1/auth/oidc", // Backend Proxy URL
+ client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "devfront-client",
+ redirect_uri: `${window.location.origin}/callback`,
+ response_type: "code",
+ scope: "openid offline_access profile email", // offline_access for refresh token
+ post_logout_redirect_uri: window.location.origin,
+ userStore: new WebStorageStateStore({ store: window.localStorage }),
+ automaticSilentRenew: true,
+};
+
+export const userManager = new UserManager({
+ ...oidcConfig,
+ authority: oidcConfig.authority || "",
+ client_id: oidcConfig.client_id || "",
+ redirect_uri: oidcConfig.redirect_uri || "",
+});
diff --git a/devfront/src/main.tsx b/devfront/src/main.tsx
index e311b5b9..b718dd50 100644
--- a/devfront/src/main.tsx
+++ b/devfront/src/main.tsx
@@ -1,11 +1,13 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+import { AuthProvider } from "react-oidc-context";
import { RouterProvider } from "react-router-dom";
import { queryClient } from "./app/queryClient";
import { router } from "./app/routes";
import { Toaster } from "./components/ui/toaster";
import "./index.css";
+import { oidcConfig } from "./lib/auth";
const rootElement = document.getElementById("root");
@@ -15,9 +17,11 @@ if (!rootElement) {
createRoot(rootElement).render(
-
-
-
-
+
+
+
+
+
+
,
);