diff --git a/templates/sites/basic/src/pushkin/front-end/package.json b/templates/sites/basic/src/pushkin/front-end/package.json index d2eac5834..747ad087e 100644 --- a/templates/sites/basic/src/pushkin/front-end/package.json +++ b/templates/sites/basic/src/pushkin/front-end/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@auth0/auth0-react": "^2.2.4", "aphrodite": "^2.4.0", "axios": "^1.7.7", "bootstrap": "^4.3.1", diff --git a/templates/sites/basic/src/pushkin/front-end/src/App.js b/templates/sites/basic/src/pushkin/front-end/src/App.js index 960868436..67c51c03a 100644 --- a/templates/sites/basic/src/pushkin/front-end/src/App.js +++ b/templates/sites/basic/src/pushkin/front-end/src/App.js @@ -12,6 +12,7 @@ import Header from "./components/Layout/Header"; import Footer from "./components/Layout/Footer"; import TakeQuiz from "./components/Quizzes/TakeQuiz"; import Results from "./components/Quizzes/Results"; +import Profile from "./components/Authentication/Profile"; //import pages import HomePage from "./pages/Home"; @@ -49,6 +50,10 @@ function App() { + + + + diff --git a/templates/sites/basic/src/pushkin/front-end/src/actions/AuthSync.jsx b/templates/sites/basic/src/pushkin/front-end/src/actions/AuthSync.jsx new file mode 100644 index 000000000..68f9ba58c --- /dev/null +++ b/templates/sites/basic/src/pushkin/front-end/src/actions/AuthSync.jsx @@ -0,0 +1,41 @@ +import { useEffect } from "react"; +import { useAuth0 } from "@auth0/auth0-react"; +import { useDispatch, useSelector } from "react-redux"; +import { setAuth0User, clearAuthUser } from "./userInfo"; + +function AuthSync() { + const { isAuthenticated, user, isLoading, getAccessTokenSilently } = useAuth0(); + const dispatch = useDispatch(); + const sessionUserID = useSelector((state) => state.userInfo.userID); // get current Redux userID + + useEffect(() => { + if (isLoading) return; // wait until Auth0 finishes + + const syncAuth = async () => { + if (isAuthenticated && user) { + let token = null; + try { + token = await getAccessTokenSilently(); + } catch (err) { + console.warn("No token obtained", err); + } + + console.log("Dispatching SET_AUTH0_USER with user:", user, "token:", token); + dispatch(setAuth0User(user, token)); + } else { + // Only clear if there is no session-based userID + if (!sessionUserID) { + dispatch(clearAuthUser()); + } else { + console.log("Preserving session userID:", sessionUserID); + } + } + }; + + syncAuth(); + }, [isAuthenticated, user, isLoading, dispatch, getAccessTokenSilently, sessionUserID]); + + return null; +} + +export default AuthSync; diff --git a/templates/sites/basic/src/pushkin/front-end/src/actions/userInfo.js b/templates/sites/basic/src/pushkin/front-end/src/actions/userInfo.js index 6a5b72125..4652719d6 100644 --- a/templates/sites/basic/src/pushkin/front-end/src/actions/userInfo.js +++ b/templates/sites/basic/src/pushkin/front-end/src/actions/userInfo.js @@ -1,24 +1,42 @@ +export const GET_USER = 'GET_USER'; export const SET_USER_ID = 'SET_USER_ID'; +export const SET_AUTH0_USER = 'SET_AUTH0_USER'; +export const CLEAR_AUTH_USER = 'CLEAR_AUTH_USER'; export const GET_SESSION_USER = 'GET_SESSION_USER'; -export const GET_USER = 'GET_SESSION_USER'; -export function getSessionUser() { +// For session or Auth0 user +export function getUser(isSessionAuthenticated, user) { return { - type: GET_SESSION_USER + type: GET_USER, + isSessionAuthenticated, + user, + userID: user?.id || null, }; } -export function getUser(isAuthenticated, user) { +// For setting userID after saga logic +export function setUserID(id) { return { - type: GET_USER, - isAuthenticated: isAuthenticated, - user: user + type: SET_USER_ID, + id, }; } -export function setUserID(userID) { +// For Auth0 login +export function setAuth0User(user, token) { return { - type: SET_USER_ID, - id: userID + type: SET_AUTH0_USER, + payload: { + user, + token, + userID: user?.sub || null, + }, + }; +} + +// For logout +export function clearAuthUser() { + return { + type: CLEAR_AUTH_USER, }; } diff --git a/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Login.js b/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Login.js new file mode 100644 index 000000000..5dfe45a91 --- /dev/null +++ b/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Login.js @@ -0,0 +1,12 @@ +import { useAuth0 } from "@auth0/auth0-react"; +import React from "react"; +import { Button } from "react-bootstrap"; + + +const LoginButton = () => { + const { loginWithRedirect } = useAuth0(); + + return ; +}; + +export default LoginButton; diff --git a/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Logout.js b/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Logout.js new file mode 100644 index 000000000..ca06d0184 --- /dev/null +++ b/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Logout.js @@ -0,0 +1,15 @@ +import { useAuth0 } from "@auth0/auth0-react"; +import React from "react"; +import { Button } from "react-bootstrap"; + +const LogoutButton = () => { + const { logout } = useAuth0(); + + return ( + + ); +}; + +export default LogoutButton; diff --git a/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Profile.js b/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Profile.js new file mode 100644 index 000000000..da24d86c4 --- /dev/null +++ b/templates/sites/basic/src/pushkin/front-end/src/components/Authentication/Profile.js @@ -0,0 +1,33 @@ +import { useAuth0 } from "@auth0/auth0-react"; +import React from "react"; +import { Container, Button } from 'react-bootstrap'; +import { pushkinConfig } from '../../.pushkin'; + +const Profile = () => { + const { user, isAuthenticated, isLoading } = useAuth0(); + const authDomain = pushkinConfig?.addons?.authDomain || ''; + + if (isLoading) { + return
Loading ...
; + } + + return ( + isAuthenticated && ( + + {user.name} +
+

Username: {user.name}

+

Email: {user.email}

+ {authDomain && ( +

+ +

+ )} +
+ ) + ); +}; + +export default Profile; diff --git a/templates/sites/basic/src/pushkin/front-end/src/components/Layout/Header.js b/templates/sites/basic/src/pushkin/front-end/src/components/Layout/Header.js index 4fb3c1b70..1ee5b49c4 100644 --- a/templates/sites/basic/src/pushkin/front-end/src/components/Layout/Header.js +++ b/templates/sites/basic/src/pushkin/front-end/src/components/Layout/Header.js @@ -12,6 +12,9 @@ import { Nav, Navbar, Button, Image } from 'react-bootstrap'; //other import { CONFIG } from '../../config'; +import LoginButton from '../Authentication/Login'; +import LogoutButton from '../Authentication/Logout'; +import { useAuth0 } from "@auth0/auth0-react"; const mapStateToProps = (state) => { return { @@ -20,12 +23,29 @@ const mapStateToProps = (state) => { }; const Header = (props) => { - const isAuthenticated = false; - const user = null; + // Use Auth0 if enabled, otherwise fall back to session-based auth + let isAuthenticated = false; + let user = null; + let isLoading = false; + + if (CONFIG.useAuth && CONFIG.authDomain && CONFIG.authClientID) { + const auth0Data = useAuth0(); + isAuthenticated = auth0Data.isAuthenticated; + user = auth0Data.user; + isLoading = auth0Data.isLoading; + } useEffect(() => { - props.dispatch(getUser(isAuthenticated, user)); - }, [isAuthenticated]); + // Only dispatch getUser for legacy session-based auth + // Auth0 users are handled by AuthSync component + if (!isLoading && !(CONFIG.useAuth && CONFIG.authDomain && CONFIG.authClientID)) { + props.dispatch(getUser(isAuthenticated, user)); + } + }, [isAuthenticated, isLoading, user, props]); + + if (isLoading) { + return null; // or a loading spinner if preferred + } return ( { About + {CONFIG.useAuth && isAuthenticated ? + + My account + + : null} + {CONFIG.useAuth && ( + + )} ); diff --git a/templates/sites/basic/src/pushkin/front-end/src/config.js b/templates/sites/basic/src/pushkin/front-end/src/config.js new file mode 100644 index 000000000..242b0b9d7 --- /dev/null +++ b/templates/sites/basic/src/pushkin/front-end/src/config.js @@ -0,0 +1,60 @@ +// Global configuration file + +//import util from 'util'; +//import fs from 'fs'; +//import jsYaml from 'js-yaml'; +import { pushkinConfig } from './.pushkin.js' +import { debug, codespaces, codespaceName } from './.env.js' + +// Front-end configuration file + +// --- API --- +let apiEndpoint; +let frontEndURL; +let logoutURL; +if (debug) { + // Debug / Test + let rootDomain; + if (codespaces) { + // Make sure to point to the Codespaces URL if testing in a codespace + rootDomain = 'https://' + codespaceName + '-80.app.github.dev'; + } else { + rootDomain = 'http://localhost'; + } + apiEndpoint = rootDomain + '/api'; + frontEndURL = rootDomain + '/callback'; + logoutURL = rootDomain; +} else { + // Production + const rootDomain = pushkinConfig.info.rootDomain; + if (pushkinConfig.apiEndpoint) { + //What's in the YAML can override default + apiEndpoint = pushkinConfig.apiEndpoint + } else{ + apiEndpoint = 'https://api.' + rootDomain; + } + frontEndURL = 'https://' + rootDomain + '/callback'; + logoutURL = 'https://' + rootDomain; +} + +export const CONFIG = { + production: !debug, + debug: debug, + + apiEndpoint: apiEndpoint, + frontEndURL: frontEndURL, + logoutURL: logoutURL, + + useForum: pushkinConfig.addons.useForum, + useAuth: pushkinConfig.addons.useAuth, + + whoAmI: pushkinConfig.info.whoAmI, + hashtags: pushkinConfig.info.hashtags, + email: pushkinConfig.info.email, + shortName: pushkinConfig.info.shortName, + salt: pushkinConfig.salt, + + fc: pushkinConfig.fc, + authClientID: pushkinConfig.addons?.authClientID || '', + authDomain: pushkinConfig.addons?.authDomain || '' +}; diff --git a/templates/sites/basic/src/pushkin/front-end/src/index.js b/templates/sites/basic/src/pushkin/front-end/src/index.js index 8b76e1620..d028e2e58 100644 --- a/templates/sites/basic/src/pushkin/front-end/src/index.js +++ b/templates/sites/basic/src/pushkin/front-end/src/index.js @@ -21,6 +21,10 @@ import createSagaMiddleware from 'redux-saga'; import rootReducer from './reducers/index'; import rootSaga from './sagas/index'; +// Auth0 integration +import { Auth0Provider } from '@auth0/auth0-react'; +import AuthSync from "./actions/AuthSync"; + // //Stylin // import './index.css'; // drop?? // import './styles/styles.less'; //Bootstrap styles @@ -50,10 +54,31 @@ const onRedirectCallback = (appState) => { //Renders the front end const root = createRoot(document.getElementById('root')); +// Conditionally wrap with Auth0Provider if useAuth is enabled +const AppWithAuth = () => { + if (CONFIG.useAuth && CONFIG.authDomain && CONFIG.authClientID) { + return ( + + + + + ); + } + return ; +}; + root.render( - + ); diff --git a/templates/sites/basic/src/pushkin/front-end/src/reducers/userInfo.js b/templates/sites/basic/src/pushkin/front-end/src/reducers/userInfo.js index 67928e8ab..27383ff60 100644 --- a/templates/sites/basic/src/pushkin/front-end/src/reducers/userInfo.js +++ b/templates/sites/basic/src/pushkin/front-end/src/reducers/userInfo.js @@ -1,7 +1,17 @@ -import { SET_USER_ID } from '../actions/userInfo'; +import { + GET_USER, + SET_USER_ID, + SET_AUTH0_USER, + CLEAR_AUTH_USER +} from '../actions/userInfo'; const initialState = { - userID: null + isSessionAuthenticated: false, + isAuthenticated: false, // Auth0 + user: null, + userID: null, + token: null, // Auth0 + authMode: null // 'legacy' or 'auth0' }; export default function error(state = initialState, action) { @@ -11,6 +21,32 @@ export default function error(state = initialState, action) { ...state, userID: action.id }; + + case GET_USER: + return { + ...state, + isSessionAuthenticated: action.isSessionAuthenticated, + user: action.user, + userID: action.userID || action.user?.id || state.userID, + authMode: 'legacy' + }; + + case SET_AUTH0_USER: + console.log("Reducer received SET_AUTH0_USER", action.payload); + return { + ...state, + isAuthenticated: true, + user: action.payload.user, + token: action.payload.token, + userID: action.payload.userID || action.payload.user?.sub ||null, + authMode: 'auth0' + }; + + case CLEAR_AUTH_USER: + return { + ...initialState + }; + default: return state; } diff --git a/templates/sites/basic/src/pushkin/front-end/src/sagas/userInfo.js b/templates/sites/basic/src/pushkin/front-end/src/sagas/userInfo.js index 7497da38c..e12d9f18b 100644 --- a/templates/sites/basic/src/pushkin/front-end/src/sagas/userInfo.js +++ b/templates/sites/basic/src/pushkin/front-end/src/sagas/userInfo.js @@ -1,13 +1,27 @@ import { SET_USER_ID, GET_USER } from '../actions/userInfo'; -//import { put, takeEvery, takeLatest, all } from 'redux-saga/effects'; import { put, takeLatest } from 'redux-saga/effects'; import session from '../utils/session'; export function* getUserLogic(action) { console.log('Saga2 initialized...'); - const id = action.isAuthenticated ? action.user : yield session.get(); - console.log(id); - yield put({ type: SET_USER_ID, id: id }); + + try { + let userId; + + if (action.isAuthenticated && action.user) { + userId = action.user.sub || action.user.email; + console.log("Using Auth0 user ID:", userId); + } else { + userId = session.get(); // no need for yield/call since it's sync + console.log("Using session-based user ID:", userId); + } + + yield put({ type: SET_USER_ID, id: userId }); + + } catch (error) { + console.error('Error in getUserLogic saga:', error); + // Optional: dispatch an error action + } } export function* getUser() {