Skip to content
Snippets Groups Projects
Commit 54ccbb5f authored by xaralis's avatar xaralis
Browse files

Initial login flow

parent 02604c88
Branches
No related tags found
No related merge requests found
......@@ -17,6 +17,7 @@
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
......
......@@ -1400,6 +1400,27 @@
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@react-keycloak/core": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@react-keycloak/core/-/core-2.2.1.tgz",
"integrity": "sha512-MKoawiLMamhCrcEQ79svZZV8lw+ojkcz4LMGeCzHULlZB8sTDY3uBg7S8NxhpdPdyAjQoQ5ExKL4Il75OBIamg==",
"requires": {
"@babel/runtime": "^7.9.0",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.0.1"
}
},
"@react-keycloak/web": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@react-keycloak/web/-/web-2.1.4.tgz",
"integrity": "sha512-HZry6/UIeStGc2reqYhRFAkmVGCb4fQfOdOK2bJ6RCatN5uuJ+sDu5w5L97JtGNirTqP2eKsTUM3Dx1EvgJ4ng==",
"requires": {
"@babel/runtime": "^7.9.0",
"@react-keycloak/core": "^2.2.1",
"hoist-non-react-statics": "^3.3.2",
"prop-types": "^15.7.2"
}
},
"@sentry/browser": {
"version": "5.23.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.23.0.tgz",
......@@ -1716,7 +1737,8 @@
"@testing-library/user-event": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-7.2.1.tgz",
"integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA=="
"integrity": "sha512-oZ0Ib5I4Z2pUEcoo95cT1cr6slco9WY7yiPpG+RGNkj8YcYgJnM7pXmYmorNOReh8MIGcKSqXyeGjxnr8YiZbA==",
"dev": true
},
"@types/babel__core": {
"version": "7.1.9",
......@@ -8192,6 +8214,11 @@
}
}
},
"js-sha256": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
"integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
......@@ -8340,6 +8367,15 @@
"object.assign": "^4.1.0"
}
},
"keycloak-js": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-10.0.2.tgz",
"integrity": "sha512-7nkg4Ob1khHGcNbuK36AMndKUEuIQFpNlWU9ygWs7nSBPCI9VZ8dJjjXfKJHm0ewgcqLFGPIJ6bxxRlfcQ6sLg==",
"requires": {
"base64-js": "1.3.1",
"js-sha256": "0.9.0"
}
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
......@@ -10948,6 +10984,22 @@
}
}
},
"pullstate": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/pullstate/-/pullstate-1.20.4.tgz",
"integrity": "sha512-SksJ70iYNrC+YsGjMx54pXT2/iYYUu3zg9hezjHNjPPwyViglo4lh+N0I4DwB6xw92s+9NagD3AXMGMfiIt76g==",
"requires": {
"fast-deep-equal": "^3.1.3",
"immer": "^7.0.1"
},
"dependencies": {
"immer": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.15.tgz",
"integrity": "sha512-yM7jo9+hvYgvdCQdqvhCNRRio0SCXc8xDPzA25SvKWa7b1WVPjLwQs1VYU5JPXjcJPTqAa5NP5dqpORGYBQ2AA=="
}
}
},
"pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
......@@ -11313,6 +11365,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
},
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
......
......@@ -3,9 +3,11 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-keycloak/web": "^2.1.4",
"@sentry/react": "^5.23.0",
"@testing-library/user-event": "^7.2.1",
"classnames": "^2.2.6",
"keycloak-js": "^10.0.2",
"pullstate": "^1.20.4",
"react": "^16.13.1",
"react-device-detect": "^1.13.1",
"react-dom": "^16.13.1",
......@@ -94,6 +96,7 @@
},
"devDependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/user-event": "^7.2.1",
"@testing-library/react": "^9.5.0",
"babel-core": "^6.26.3",
"babel-eslint": "^10.1.0",
......
import React from "react";
import {
BrowserRouter as Router,
Route,
Switch,
} from "react-router-dom";
import React, { useCallback, useEffect } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { KeycloakProvider } from "@react-keycloak/web";
import * as Sentry from "@sentry/react";
import { AuthStore } from "stores";
import Footer from "components/Footer";
import Navbar from "components/Navbar";
import Home from "pages/Home";
import ForVisitors from "pages/ForVisitors";
import ForPress from "pages/ForPress";
import Program from "pages/Program";
import keycloak from "./keycloak";
/**
* If configured, set up Sentry client that reports uncaught errors down to
* https://sentry.io.
......@@ -25,19 +22,65 @@ if (process.env.REACT_APP_SENTRY_DSN) {
});
}
const onKeycloakEvent = (event) => {
if (["onAuthRefreshSuccess", "onAuthSuccess"].includes(event)) {
Sentry.setUser(keycloak.tokenParsed);
AuthStore.update((state) => {
state.isAuthenticated = true;
state.user = {
name: keycloak.tokenParsed.name,
groups: keycloak.tokenParsed.groups,
};
});
}
};
const LoadingComponent = <p className="block mt-4 font-bold">Načítání ...</p>;
const BaseApp = () => {
const loadGroupMappings = useCallback(async () => {
const resp = await fetch("https://iapi.pirati.cz/v1/groups");
const mappings = await resp.json();
AuthStore.update((state) => {
state.groupMappings = mappings;
});
}, []);
useEffect(() => {
loadGroupMappings();
}, [loadGroupMappings]);
return (
<Router>
<Navbar />
<Switch>
<Route exact path="/" children={<Home />} />
<Route exact path="/program" children={<Program />} />
</Switch>
<Footer />
</Router>
);
};
const AuthenticatedApp = () => {
const keycloakInitConfig = {
onLoad: "check-sso",
// Necessary to prevent Keycloak cookie issues:
// @see: https://stackoverflow.com/a/63588334/303184
checkLoginIframe: false,
};
return (
<>
<Router>
<Navbar />
<Switch>
<Route exact path="/" children={<Home />} />
<Route exact path="/program" children={<Program />} />
<Route exact path="/pro-ucastniky" children={<ForVisitors />} />
<Route exact path="/media" children={<ForPress />} />
</Switch>
<Footer />
</Router>
<KeycloakProvider
keycloak={keycloak}
initConfig={keycloakInitConfig}
LoadingComponent={LoadingComponent}
onEvent={onKeycloakEvent}
>
<BaseApp />
</KeycloakProvider>
</>
);
};
......@@ -61,7 +104,7 @@ const ErrorBoundaryFallback = () => (
const App = Sentry.withProfiler(() => {
return (
<Sentry.ErrorBoundary fallback={ErrorBoundaryFallback} showDialog>
<BaseApp />
<AuthenticatedApp />
</Sentry.ErrorBoundary>
);
});
......
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
import React from "react";
import { render } from "@testing-library/react";
test('renders learn react link', () => {
import App from "./App";
test("renders learn react link", () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
......
import React from "react";
import classNames from "classnames";
const Button = ({
className,
icon,
hoverActive = true,
fullwidth = false,
children,
...props
}) => {
const btnClass = classNames(
"btn",
{
"btn--icon": !!icon,
"btn--hoveractive": hoverActive,
"btn--fullwidth md:btn--autowidth": fullwidth,
},
className
);
return (
<button className={btnClass} {...props}>
<div className="btn__body-wrap">
<div className="btn__body ">{children}</div>
{!!icon && (
<div className="btn__icon">
<i className={icon}></i>
</div>
)}
</div>
</button>
);
};
export default Button;
......@@ -15,68 +15,7 @@ const Footer = () => {
sdílet za stejných podmínek.
</p>
</section>
<section className="footer__main-links bg-grey-700 text-white lg:grid grid-cols-3 gap-4">
<div className="pt-4 lg:py-0 border-t border-grey-400 lg:border-t-0">
<div className="footer-collapsible">
<span className="text-xl uppercase text-white footer-collapsible__toggle">
Program
</span>{" "}
<div className="">
<ul className="mt-6 space-y-2 text-grey-200">
<li>
<a href="#">Pátek</a>
</li>{" "}
<li>
<a href="#">Sobota</a>
</li>{" "}
<li>
<a href="#">Neděle</a>
</li>
</ul>
</div>
</div>
</div>
<div className="pt-8 pb-4 lg:py-0">
<div className="footer-collapsible">
<span className="text-xl uppercase text-white footer-collapsible__toggle">
Pro účastníky
</span>{" "}
<div className="">
<ul className="mt-6 space-y-2 text-grey-200">
<li>
<a href="#">Registrace účastníků</a>
</li>{" "}
<li>
<a href="#">Kudy na fórum</a>
</li>
<li>
<a href="#">Ubytování</a>
</li>
</ul>
</div>
</div>
</div>{" "}
<div className="py-4 lg:py-0 border-t border-grey-400 lg:border-t-0">
<div className="footer-collapsible">
<span className="text-xl uppercase text-white footer-collapsible__toggle">
Pro média
</span>{" "}
<div className="">
<ul className="mt-6 space-y-2 text-grey-200">
<li>
<a href="#">Registrace novinářů</a>
</li>{" "}
<li>
<a href="#">Informace pro novináře</a>
</li>{" "}
<li>
<a href="#">Presskit</a>
</li>
</ul>
</div>
</div>
</div>{" "}
</section>
<section className="footer__main-links bg-grey-700 text-white lg:grid grid-cols-3 gap-4"></section>
<section className="footer__social lg:text-right">
<div className="flex flex-col md:flex-row lg:flex-col lg:items-end space-y-2 md:space-y-0 md:space-x-2 lg:space-x-0 lg:space-y-2">
<button className="btn btn--icon btn--blue-300 btn--hoveractive text-lg btn--fullwidth sm:btn--autowidth">
......@@ -137,7 +76,7 @@ const Footer = () => {
<a
href="mailto:example@example.com"
className="contact-line contact-line--responsive icon-link badge__link"
>
>
<i className="ico--envelope"></i>
<span>example@example.com</span>
</a>
......
import React from "react";
import { NavLink } from "react-router-dom";
const HomeHero = () => {
return (
<article
className="hero py-24"
>
<div className="container container--default text-center">
<h1 className="head-alt-lg mb-4">12. Celostátní fórum</h1>
<h2 className="head-alt-xl">České pirátské strany</h2>
<p className="head-heavy-base mt-4">11. - 12. ledna, Pardubice</p>
<p className="head-heavy-sm">Kulturní centrum IDEON</p>
<div className="mt-4 md:mt-8 space-x-4">
<NavLink className="btn btn--blue-300 btn--icon btn--hoveractive btn--fullwidth md:btn--autowidth text-xl" to="/registrace">
<div className="btn__body-wrap">
<div className="btn__body ">Registrace</div>
<div className="btn__icon ">
<i className="ico--book"></i>
</div>
</div>
</NavLink>
<NavLink className="btn btn--violet-400 btn--icon btn--hoveractive btn--fullwidth md:btn--autowidth text-xl" to="/program">
<div className="btn__body-wrap">
<div className="btn__body ">Program</div>
<div className="btn__icon ">
<i className="ico--calendar"></i>
</div>
</div>
</NavLink>
</div>
</div>
</article>
);
};
export default HomeHero;
import React, { useState } from "react";
import React, { useCallback, useState } from "react";
import { isBrowser } from "react-device-detect";
import { NavLink } from "react-router-dom";
import { useKeycloak } from "@react-keycloak/web";
import { AuthStore } from "stores";
import Button from "components/Button";
const Navbar = () => {
const [showMenu, setShowMenu] = useState(isBrowser);
const { keycloak } = useKeycloak();
const { isAuthenticated, user } = AuthStore.useState();
const login = useCallback(() => {
keycloak.login();
}, [keycloak]);
const logout = useCallback(() => {
keycloak.logout();
}, [keycloak]);
return (
<nav className="navbar navbar--simple">
......@@ -13,48 +26,55 @@ const Navbar = () => {
<img
src={`${process.env.REACT_APP_STYLEGUIDE_URL}/images/logo-round-white.svg`}
className="w-8"
alt="Pirátská strana"
/>
</NavLink>
<NavLink to="/" className="pl-4 font-bold text-xl lg:border-r lg:border-grey-300 lg:pr-8 hover:no-underline">
<NavLink
to="/"
className="pl-4 font-bold text-xl lg:border-r lg:border-grey-300 lg:pr-8 hover:no-underline"
>
Celostátní fórum 2021
</NavLink>
</div>
<div className="navbar__menutoggle my-4 flex justify-end lg:hidden">
<a
href="#"
<button
onClick={() => setShowMenu(!showMenu)}
className="no-underline hover:no-underline"
>
<i className="ico--menu text-3xl"></i>
</a>
</button>
</div>
{showMenu && (
<>
<div className="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto">
<ul className="navbar-menu text-white">
<li className="navbar-menu__item">
<NavLink className="navbar-menu__link" to="/program">Program</NavLink>
</li>
<li className="navbar-menu__item">
<NavLink className="navbar-menu__link" to="/registrace">Registrace</NavLink>
</li>
<li className="navbar-menu__item">
<NavLink className="navbar-menu__link" to="/pro-ucastniky">Pro účastníky</NavLink>
</li>
<li className="navbar-menu__item">
<NavLink className="navbar-menu__link" to="/media">Pro média</NavLink>
<NavLink className="navbar-menu__link" to="/program">
Program
</NavLink>
</li>
</ul>
</div>
<div className="navbar__actions navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto self-start flex flex-col sm:flex-row lg:flex-col sm:space-x-4 space-y-2 sm:space-y-0 lg:space-y-2 xl:flex-row xl:space-x-2 xl:space-y-0">
<NavLink className="btn btn--white btn--icon btn--hoveractive btn--fullwidth md:btn--autowidth text-sm" to="/program">
<div className="btn__body-wrap">
<div className="btn__body ">Osobní zóna</div>
<div className="btn__icon">
<i className="ico--users"></i>
{!isAuthenticated && (
<Button className="btn--white" onClick={login}>
Přihlásit se
</Button>
)}
{isAuthenticated && (
<div className="flex items-center space-x-4">
<span className="head-heavy-2xs">{user.name}</span>
<div className="avatar avatar--2xs">
<img
src="http://placeimg.com/100/100/people"
alt="Avatar"
/>
</div>
<button onClick={logout}>
<i className="ico--log-out"></i>
</button>
</div>
</NavLink>
)}
</div>
</>
)}
......
export default {
styleguideUrl: "https://styleguide.pir-test.eu/latest"
styleguideUrl: "https://styleguide.pir-test.eu/latest",
};
.h-screen {
min-height: 100vh;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
......
import Keycloak from "keycloak-js";
// Setup Keycloak instance as needed
// Pass initialization options as required or leave blank to load from 'keycloak.json'
const keycloak = Keycloak({
url: "https://auth.pirati.cz/auth",
realm: "pirati",
clientId: "cf-online",
});
// keycloak.init({
// onLoad: "check-sso",
// });
export default keycloak;
import React from "react";
const ForPress = () => {
return (
<>
Press
</>
);
};
export default ForPress;
import React from "react";
const ForVisitors = () => {
return (
<>
Visitors
</>
);
};
export default ForVisitors;
import React from "react";
import HomeHero from "components/HomeHero";
const Home = () => {
return (
<>
<HomeHero />
<hr />
<section className="container container--default my-8 text-center">
<h1 className="head-alt-base mb-4">Celostátní fórum</h1>
<p>Celostátní fórum Pirátské strany je nejvyšším orgánem strany a zasedání se podle možností účastní každý člen strany.</p>
</section>
<hr />
<section className="container container--default my-8 text-center">
<h1 className="head-alt-base mb-4">Základní informace</h1>
<p>Celostátní fórum Pirátské strany je nejvyšším orgánem strany a zasedání se podle možností účastní každý člen strany.</p>
</section>
<p>Homepage</p>
</>
);
};
......
import React from "react";
const Schedule = () => {
return (
<>
Schedule
</>
);
return <>Schedule</>;
};
export default Schedule;
......@@ -11,9 +11,9 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
......@@ -21,7 +21,7 @@ const isLocalhost = Boolean(
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
......@@ -31,7 +31,7 @@ export function register(config) {
return;
}
window.addEventListener('load', () => {
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
......@@ -42,8 +42,8 @@ export function register(config) {
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://bit.ly/CRA-PWA"
);
});
} else {
......@@ -57,21 +57,21 @@ export function register(config) {
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
"New content is available and will be used when all " +
"tabs for this page are closed. See https://bit.ly/CRA-PWA."
);
// Execute callback
......@@ -82,7 +82,7 @@ function registerValidSW(swUrl, config) {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
......@@ -93,25 +93,25 @@ function registerValidSW(swUrl, config) {
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
headers: { "Service-Worker": "script" },
})
.then(response => {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
(contentType != null && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
......@@ -123,18 +123,18 @@ function checkValidServiceWorker(swUrl, config) {
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
"No internet connection found. App is running in offline mode."
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready
.then(registration => {
.then((registration) => {
registration.unregister();
})
.catch(error => {
.catch((error) => {
console.error(error.message);
});
}
......
......@@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
import "@testing-library/jest-dom/extend-expect";
import { Store } from "pullstate";
export const AuthStore = new Store({
isAuthenticated: false,
user: {
name: null,
groups: null,
},
groupMappings: null,
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment