diff --git a/package-lock.json b/package-lock.json
index 3839fb1..6f46b80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@tsparticles/slim": "^3.8.1",
"antd": "^5.24.2",
"framer-motion": "^12.4.7",
+ "keycloak-js": "^26.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
@@ -15159,6 +15160,12 @@
"node": ">=4.0"
}
},
+ "node_modules/keycloak-js": {
+ "version": "26.1.4",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.1.4.tgz",
+ "integrity": "sha512-4h2RicCzIAtsjKIG8DIO+8NKlpWX2fiNkbS0jlbtjZFbIGGjbQBzjS/5NkyWlzxamXVow9prHTIgIiwfo3GAmQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
diff --git a/package.json b/package.json
index e37daf5..fecf5fd 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@tsparticles/slim": "^3.8.1",
"antd": "^5.24.2",
"framer-motion": "^12.4.7",
+ "keycloak-js": "^26.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
diff --git a/public/global.css b/public/global.css
index 0a84193..176f539 100644
--- a/public/global.css
+++ b/public/global.css
@@ -509,3 +509,16 @@ h1 {
flex: 1 1 0px;
margin-left: 15px;
}
+
+.tbauthbutton {
+ background: rgba(255, 255, 255, 0.1) !important;
+ color: white !important;
+ border: none;
+ border-radius: 20px;
+}
+
+.tbauthbutton:hover {
+ box-shadow:
+ 6px 6px 12px #c5c5c52b,
+ -6px -6px 12px #ffffff30;
+}
diff --git a/public/silent-check-sso.html b/public/silent-check-sso.html
new file mode 100644
index 0000000..ad5f88b
--- /dev/null
+++ b/public/silent-check-sso.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Silent Check SSO
+
+
+
+
+
diff --git a/src/App.jsx b/src/App.jsx
index edc6965..8c29e35 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,9 +1,10 @@
import React, { useState, useEffect } from "react";
-import { Layout, Space } from "antd";
+import { Layout, Space, Spin } from "antd";
import { useLocation, useNavigate } from "react-router-dom";
import { AnimatePresence, motion } from "framer-motion";
import ParticlesBackground from "./components/ParticlesBackground";
import { useMediaQuery } from "react-responsive";
+import { useKeycloak } from "./utils/KeycloakProvider";
import MainView from "./views/MainView";
import NotFoundView from "./views/NotFoundView";
import CVView from "./views/CVView";
@@ -13,7 +14,7 @@ import ExperienceView from "./views/ExperienceView";
import BlogsView from "./views/BlogsView";
import BlogView from "./views/BlogView";
import CacheReloadView from "./utils/CacheReloadView";
-import { LinkOutlined } from "@ant-design/icons";
+import { LinkOutlined, UserOutlined, LoadingOutlined } from "@ant-design/icons";
const { Content } = Layout;
const { Footer } = Layout;
@@ -24,6 +25,8 @@ const App = () => {
const isMobile = useMediaQuery({ maxWidth: 600 });
+ const { isAuthenticated, loading, keycloak } = useKeycloak();
+
// Get the query parameter from the URL
const getQueryParam = (param) => {
const searchParams = new URLSearchParams(location.search); // Use location.search directly
@@ -217,8 +220,35 @@ const App = () => {
setCurrentView("socials");
}}
>
-
+
+ {loading ? (
+
+ ) : (
+ <>
+ {!isAuthenticated && (
+ {
+ e.preventDefault();
+ keycloak.login();
+ }}
+ >
+
+
+ )}
+ >
+ )}
diff --git a/src/main.jsx b/src/main.jsx
index 3128d2c..9c6c4e0 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -3,6 +3,7 @@ import ReactGA from "react-ga4";
import { BrowserRouter } from "react-router-dom";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+import { KeycloakProvider } from "./utils/KeycloakProvider";
import App from "./App.jsx";
import "antd/dist/reset.css"; // Import Ant Design styles
import "../public/global.css";
@@ -11,9 +12,11 @@ import "../public/fonts.css";
ReactGA.initialize("G-MN5S04W1HB");
createRoot(document.getElementById("root")).render(
-
-
-
-
- ,
+
+
+
+
+
+
+ ,
);
diff --git a/src/utils/KeycloakProvider.jsx b/src/utils/KeycloakProvider.jsx
new file mode 100644
index 0000000..607f9db
--- /dev/null
+++ b/src/utils/KeycloakProvider.jsx
@@ -0,0 +1,49 @@
+import React, { createContext, useContext, useEffect, useState } from "react";
+import Keycloak from "keycloak-js";
+
+// Initialize Keycloak
+const keycloak = new Keycloak({
+ url: "https://auth.tombutcher.work", // Your Keycloak server
+ realm: "master", // Your Keycloak realm
+ clientId: "web-client", // Your Keycloak client ID
+});
+
+const KeycloakContext = createContext(null);
+
+export const KeycloakProvider = ({ children }) => {
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ keycloak
+ .init({
+ onLoad: "check-sso",
+ silentCheckSsoRedirectUri:
+ window.location.origin + "/silent-check-sso.html",
+ })
+ .then((authenticated) => {
+ setIsAuthenticated(authenticated);
+ if (authenticated) {
+ setUser({
+ username: keycloak.tokenParsed?.preferred_username,
+ email: keycloak.tokenParsed?.email,
+ firstName: keycloak.tokenParsed?.given_name,
+ lastName: keycloak.tokenParsed?.family_name,
+ roles: keycloak.tokenParsed?.realm_access?.roles || [],
+ });
+ }
+ })
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useKeycloak = () => useContext(KeycloakContext);
diff --git a/src/views/MainView.jsx b/src/views/MainView.jsx
index 90d6cfc..202cdba 100644
--- a/src/views/MainView.jsx
+++ b/src/views/MainView.jsx
@@ -1,10 +1,79 @@
// views/MainView.jsx
import React from "react";
-import { Button, Flex } from "antd";
+import { Button, Flex, Dropdown, message } from "antd";
+import { UserOutlined } from "@ant-design/icons";
import { useMediaQuery } from "react-responsive";
+import { useKeycloak } from "../utils/KeycloakProvider";
const MainView = ({ setCurrentView }) => {
const isMobile = useMediaQuery({ maxWidth: 600 });
+ const { isAuthenticated, user, keycloak } = useKeycloak();
+
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const authItems = [
+ {
+ label: user?.username,
+ key: "1",
+ icon: (
+
+ ),
+ disabled: true,
+ },
+ {
+ label: user?.email,
+ key: "2",
+ icon: (
+
+ ),
+ disabled: true,
+ },
+ {
+ label: "Manage Account",
+ key: "3",
+ icon: (
+
+ ),
+ },
+ {
+ label: "Logout",
+ key: "4",
+ icon: (
+
+ ),
+ },
+ ];
+
+ const onAuthItemClick = ({ key }) => {
+ switch (key) {
+ case "3":
+ window.location.href = keycloak.createAccountUrl();
+ break;
+ case "4":
+ keycloak.logout();
+ break;
+ default:
+ message.info(`Click on item ${key}`);
+ }
+ };
+
+ const authMenuProps = {
+ items: authItems,
+ onClick: onAuthItemClick,
+ };
+
return (
{
)}
+ {isAuthenticated ? (
+ <>
+
+
+
+ >
+ ) : null}
);