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"); }} > - + Social Media Links + {loading ? ( + + ) : ( + <> + {!isAuthenticated && ( + { + e.preventDefault(); + keycloak.login(); + }} + > + Login to tombutcher.work + + )} + + )} 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} );