diff --git a/src/frontend/app/desk/.env.development b/src/frontend/app/desk/.env.development new file mode 100644 index 0000000..472706a --- /dev/null +++ b/src/frontend/app/desk/.env.development @@ -0,0 +1,3 @@ +NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080/ +NEXT_PUBLIC_KEYCLOAK_REALM=people +NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=people-front diff --git a/src/frontend/app/desk/package.json b/src/frontend/app/desk/package.json index 984d7ea..2aaaaf7 100644 --- a/src/frontend/app/desk/package.json +++ b/src/frontend/app/desk/package.json @@ -17,7 +17,8 @@ "@openfun/cunningham-react": "2.4.0", "next": "14.0.4", "react": "18.2.0", - "react-dom": "18.2.0" + "react-dom": "18.2.0", + "zustand": "4.4.7" }, "devDependencies": { "@testing-library/jest-dom": "6.2.0", @@ -35,6 +36,7 @@ "eslint-plugin-testing-library": "6.2.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", + "keycloak-js": "23.0.3", "prettier": "3.1.1", "stylelint": "16.1.0", "stylelint-config-standard": "36.0.0", diff --git a/src/frontend/app/desk/src/app/page.tsx b/src/frontend/app/desk/src/app/page.tsx index f274349..8905987 100644 --- a/src/frontend/app/desk/src/app/page.tsx +++ b/src/frontend/app/desk/src/app/page.tsx @@ -1,10 +1,23 @@ 'use client'; -import { Button } from '@openfun/cunningham-react'; +import { Button, Loader } from '@openfun/cunningham-react'; +import { useEffect } from 'react'; import styles from './page.module.css'; +import useAuthStore from '@/auth/useAuthStore'; + export default function Home() { + const { initAuth, authenticated } = useAuthStore(); + + useEffect(() => { + initAuth(); + }, [initAuth]); + + if (!authenticated) { + return ; + } + return (

Hello world!

diff --git a/src/frontend/app/desk/src/auth/keycloak.ts b/src/frontend/app/desk/src/auth/keycloak.ts new file mode 100644 index 0000000..2e386fe --- /dev/null +++ b/src/frontend/app/desk/src/auth/keycloak.ts @@ -0,0 +1,23 @@ +import Keycloak, { KeycloakConfig } from 'keycloak-js'; + +const keycloakConfig: KeycloakConfig = { + url: process.env.NEXT_PUBLIC_KEYCLOAK_URL, + realm: process.env.NEXT_PUBLIC_KEYCLOAK_REALM || '', + clientId: process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || '', +}; + +export const initKeycloak = (onAuthSuccess: (_token?: string) => void) => { + const keycloak = new Keycloak(keycloakConfig); + keycloak + .init({ + onLoad: 'login-required', + }) + .then((authenticated) => { + if (authenticated) { + onAuthSuccess(keycloak.token); + } + }) + .catch(() => { + throw new Error('Failed to initialize Keycloak.'); + }); +}; diff --git a/src/frontend/app/desk/src/auth/useAuthStore.tsx b/src/frontend/app/desk/src/auth/useAuthStore.tsx new file mode 100644 index 0000000..2e4c3c3 --- /dev/null +++ b/src/frontend/app/desk/src/auth/useAuthStore.tsx @@ -0,0 +1,32 @@ +import { create } from 'zustand'; + +import { initKeycloak } from './keycloak'; + +interface AuthStore { + initialized: boolean; + authenticated: boolean; + token: string | null; + initAuth: () => void; +} + +const useAuthStore = create((set) => ({ + initialized: false, + authenticated: false, + token: null, + + initAuth: () => + set((state) => { + if (process.env.NODE_ENV === 'development' && !state.initialized) { + initKeycloak((token) => set({ authenticated: true, token })); + return { initialized: true }; + } + + /** + * TODO: Implement OIDC production authentication + */ + + return {}; + }), +})); + +export default useAuthStore; diff --git a/src/frontend/app/desk/yarn.lock b/src/frontend/app/desk/yarn.lock index f1808f8..40e06eb 100644 --- a/src/frontend/app/desk/yarn.lock +++ b/src/frontend/app/desk/yarn.lock @@ -2657,6 +2657,11 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== +base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -4752,6 +4757,11 @@ jest@29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" +js-sha256@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d" + integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4856,6 +4866,20 @@ json5@^2.2.3: object.assign "^4.1.4" object.values "^1.1.6" +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + +keycloak-js@23.0.3: + version "23.0.3" + resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-23.0.3.tgz#91c71ac5c8e3bbafe9c7e5b8ef81756ffdd023ac" + integrity sha512-T28rdeRgGdOvIUMl6Wo9IiHEGcDxZhUZxXBwk3EwDFWrn8uhWBngDFxdsdsv7qGGKAMKrkZVL2JN/h81sllN7A== + dependencies: + base64-js "^1.5.1" + js-sha256 "^0.10.1" + jwt-decode "^4.0.0" + keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -6282,6 +6306,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -6513,3 +6542,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@4.4.7: + version "4.4.7" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.4.7.tgz#355406be6b11ab335f59a66d2cf9815e8f24038c" + integrity sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw== + dependencies: + use-sync-external-store "1.2.0"