diff --git a/apps/web-graphpolaris/project.json b/apps/web-graphpolaris/project.json index 8f4cc9fa653651d96862e45b3466964b1ecdcc72..b3632e1739bf0207d33a72ecab62d724a1523c95 100644 --- a/apps/web-graphpolaris/project.json +++ b/apps/web-graphpolaris/project.json @@ -3,9 +3,25 @@ "sourceRoot": "apps/web-graphpolaris/src", "projectType": "application", "targets": { + "dev": { + "executor": "@nrwl/web:dev-server", + "options": { + "buildTarget": "web-graphpolaris:build", + "host": "local.datastrophe.science.uu.nl", + "port": 4200, + "watch": true, + "hmr": true, + "ssl": true, + "sslCert": "./certs/local-cert.pem", + "sslKey": "./certs/local-key.pem", + "open": true + } + }, "build": { "executor": "@nrwl/web:webpack", - "outputs": ["{options.outputPath}"], + "outputs": [ + "{options.outputPath}" + ], "defaultConfiguration": "production", "options": { "compiler": "babel", @@ -19,18 +35,18 @@ "apps/web-graphpolaris/src/favicon.ico", "apps/web-graphpolaris/src/assets" ], - "styles": ["apps/web-graphpolaris/src/styles.scss"], + "styles": [ + "apps/web-graphpolaris/src/styles.scss" + ], "scripts": [], "webpackConfig": "@nrwl/react/plugins/webpack" }, "configurations": { "production": { - "fileReplacements": [ - { - "replace": "apps/web-graphpolaris/src/environments/environment.ts", - "with": "apps/web-graphpolaris/src/environments/environment.prod.ts" - } - ], + "fileReplacements": [{ + "replace": "apps/graphpolaris/src/environments/environment.ts", + "with": "apps/graphpolaris/src/environments/environment.prod.ts" + }], "optimization": true, "outputHashing": "all", "sourceMap": false, @@ -55,14 +71,20 @@ }, "lint": { "executor": "@nrwl/linter:eslint", - "outputs": ["{options.outputFile}"], + "outputs": [ + "{options.outputFile}" + ], "options": { - "lintFilePatterns": ["apps/web-graphpolaris/**/*.{ts,tsx,js,jsx}"] + "lintFilePatterns": [ + "apps/graphpolaris/**/*.{ts,tsx,js,jsx}" + ] } }, "test": { "executor": "@nrwl/jest:jest", - "outputs": ["coverage/apps/web-graphpolaris"], + "outputs": [ + "coverage/apps/graphpolaris" + ], "options": { "jestConfig": "apps/web-graphpolaris/jest.config.js", "passWithNoTests": true @@ -91,7 +113,9 @@ }, "build-storybook": { "executor": "@nrwl/storybook:build", - "outputs": ["{options.outputPath}"], + "outputs": [ + "{options.outputPath}" + ], "options": { "uiFramework": "@storybook/react", "outputPath": "dist/storybook/graphpolaris", @@ -107,4 +131,4 @@ } }, "tags": [] -} +} \ No newline at end of file diff --git a/apps/web-graphpolaris/src/app/app.tsx b/apps/web-graphpolaris/src/app/app.tsx index c33f6c28dfe1585435a0388de4f5f5e9c1a1dac4..9528eddb4fc2d3fd58fc23d9cd854281215bd073 100644 --- a/apps/web-graphpolaris/src/app/app.tsx +++ b/apps/web-graphpolaris/src/app/app.tsx @@ -1,20 +1,41 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars +import { useEffect, useState } from 'react'; import { assignNewGraphQueryResult, - changeDataPointColors, useAppDispatch, } from '@graphpolaris/shared/data-access/store'; +import { AuthorizationHandler } from '@graphpolaris/shared/data-access/authorization'; import GridLayout from 'react-grid-layout'; import Panel from '../components/panels/panel'; import { RawJSONVis } from '../components/visualisations/rawjsonvis/rawjsonvis'; import SemanticSubstrates from '../components/visualisations/semanticsubstrates/semanticsubstrates'; +import LoginScreen from '../components/login/loginScreen'; import { OurThemeProvider } from '@graphpolaris/shared/data-access/theme'; export function App() { const dispatch = useAppDispatch(); + const [userAuthorized, setUserAuthorized] = useState<boolean>(false); + + const authCallback = () => { + setUserAuthorized(true); + }; + + AuthorizationHandler.instance().setCallback(authCallback); + + // Attempt to Authorize the user + const authorize = async () => { + const authorized = await AuthorizationHandler.instance().Authorize(); + setUserAuthorized(authorized); + }; + + useEffect(() => { + authorize(); + }, []); + return ( <OurThemeProvider> + {!userAuthorized && <LoginScreen />} <GridLayout className="layout" cols={10} diff --git a/apps/web-graphpolaris/src/assets/login-screen/github.png b/apps/web-graphpolaris/src/assets/login-screen/github.png new file mode 100644 index 0000000000000000000000000000000000000000..55f90676824d4901f6fe7a50ed11f6d4af3d6fef Binary files /dev/null and b/apps/web-graphpolaris/src/assets/login-screen/github.png differ diff --git a/apps/web-graphpolaris/src/assets/login-screen/google.png b/apps/web-graphpolaris/src/assets/login-screen/google.png new file mode 100644 index 0000000000000000000000000000000000000000..f27bb2433042aea5fc34e19fcf90944430ec331b Binary files /dev/null and b/apps/web-graphpolaris/src/assets/login-screen/google.png differ diff --git a/apps/web-graphpolaris/src/components/login/loginScreen.tsx b/apps/web-graphpolaris/src/components/login/loginScreen.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e5d4f467bea53f1c4e80e300d01ce8ddd18af631 --- /dev/null +++ b/apps/web-graphpolaris/src/components/login/loginScreen.tsx @@ -0,0 +1,129 @@ +import { AuthorizationHandler } from '@graphpolaris/shared/data-access/authorization'; +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-family: 'Arial'; + position: absolute; + left: 0; + top: 0; + // Cover the screen + width: 100vw; + height: 100vh; + + display: flex; + justify-content: center; + align-items: center; +`; + +const Background = styled.div` + position: absolute; + + width: 100%; + height: 100%; + + z-index: 1; + + // Blur + background: rgba( + 0, + 0, + 0, + 0.4 + ); // Make sure this color has an opacity of less than 1 + backdrop-filter: blur(8px); // This be the blur +`; + +const Content = styled.div` + background-color: white; + box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); + padding: 2em; + z-index: 2; + border-radius: 8px; + + display: flex; + flex-direction: column; + gap: 1em; + align-items: center; + justify-content: center; + + // Give children 0 padding and margin + * { + display: flex; + margin: 0; + padding: 0; + + // Same width flexbox items + flex: 1 1 0px; + + max-height: 3em; + + &:hover { + cursor: pointer; + } + } +`; + +const LoginScreen = () => { + const openSignInWindow = (url: string) => { + // remove any existing event listeners + window.removeEventListener('message', receiveMessage); + + // window features + const strWindowFeatures = + 'toolbar=no, menubar=no, width=600, height=700, top=100, left=100'; + + window.open(url, 'Google Oauth', strWindowFeatures); + + // add the listener for receiving a message from the popup + window.addEventListener('message', (event) => receiveMessage(event), false); + }; + + const receiveMessage = (event: MessageEvent) => { + // Do we trust the sender of this message? (might be + // different from what we originally opened) + if (!event.isTrusted) { + return; + } + + // Set access token + AuthorizationHandler.instance().SetAccessToken(event.data); + }; + + return ( + <Wrapper> + <Background></Background> + <Content> + <h1>Sign In</h1> + <img + onClick={() => + openSignInWindow( + 'https://datastrophe.science.uu.nl/user/sign-in?provider=1' + ) + } + src="assets/login-screen/google.png" + alt="sign up with google" + /> + <img + onClick={() => + openSignInWindow( + 'https://datastrophe.science.uu.nl/user/sign-in?provider=2' + ) + } + src="assets/login-screen/github.png" + alt="sign up with github" + /> + <p + onClick={() => + openSignInWindow( + 'https://datastrophe.science.uu.nl/user/create_free/' + ) + } + > + Developer + </p> + </Content> + </Wrapper> + ); +}; + +export default LoginScreen; diff --git a/apps/web-graphpolaris/src/components/login/popup.tsx b/apps/web-graphpolaris/src/components/login/popup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d895b7cfdc62f1aa3250dd88d5487a750ec133ac --- /dev/null +++ b/apps/web-graphpolaris/src/components/login/popup.tsx @@ -0,0 +1,17 @@ +const LoginPopupComponent = () => { + if (window.opener) { + // Get the access token from the query params + const urlParams = new URLSearchParams(window.location.search); + const accessToken = urlParams.get('access_token'); + + // Send the access token to the parent window + window.opener.postMessage(accessToken, '*'); + + // Close this window + window.close(); + } + + return <h1>Loading...</h1>; +}; + +export default LoginPopupComponent; diff --git a/apps/web-graphpolaris/src/components/panels/panel.tsx b/apps/web-graphpolaris/src/components/panels/panel.tsx index 5189e4d410f05b865992f6a272fd371c550f91e3..f5214c7a160a96df7015cca2eeb1c09f0b6463fc 100644 --- a/apps/web-graphpolaris/src/components/panels/panel.tsx +++ b/apps/web-graphpolaris/src/components/panels/panel.tsx @@ -9,7 +9,10 @@ interface Props { const Wrapper = styled.div<{ color: string }>` background-color: ${(props) => props.color}; - font: 'Arial'; + font-family: 'Arial'; + + // Light shadow + box-shadow: 0 3px 10px rgb(0 0 0 / 0.2); height: 100%; width: 100%; diff --git a/apps/web-graphpolaris/src/main.tsx b/apps/web-graphpolaris/src/main.tsx index 0eb1b52efbd1c4aab21e9ad90fe9845c7f697abd..cd0a1c80a895818d45e24e56cc4722ac0e55ece6 100644 --- a/apps/web-graphpolaris/src/main.tsx +++ b/apps/web-graphpolaris/src/main.tsx @@ -1,15 +1,22 @@ +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; import { store } from '@graphpolaris/shared/data-access/store'; import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import App from './app/app'; +import LoginPopupComponent from './components/login/popup'; ReactDOM.render( - <StrictMode> - <Provider store={store}> - <App /> - </Provider> - </StrictMode>, + <Provider store={store}> + <Router> + <Routes> + {/* Route to auth component in popup */} + <Route path="/auth" element={<LoginPopupComponent />}></Route> + {/* App */} + <Route path="/" element={<App />}></Route> + </Routes> + </Router> + </Provider>, document.getElementById('root') ); diff --git a/certs/local-cert.pem b/certs/local-cert.pem new file mode 100644 index 0000000000000000000000000000000000000000..0db26ae7faa1397a8c0d7ab4c2c6b4acf6b3ffaa --- /dev/null +++ b/certs/local-cert.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEwzCCAyugAwIBAgIRAIb+R7ZfGJ3oiTEycYz8cGUwDQYJKoZIhvcNAQELBQAw +gckxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTFPME0GA1UECwxGdGhp +anNoZWlqZGVuQFRoaWpzcy1NQlAuZG9tYWluX25vdF9zZXQuaW52YWxpZCAoVGhp +anMgdmFuIGRlciBIZWlqZGVuKTFWMFQGA1UEAwxNbWtjZXJ0IHRoaWpzaGVpamRl +bkBUaGlqc3MtTUJQLmRvbWFpbl9ub3Rfc2V0LmludmFsaWQgKFRoaWpzIHZhbiBk +ZXIgSGVpamRlbikwHhcNMjIwMjA5MTYyOTQxWhcNMjQwNTA5MTUyOTQxWjB6MScw +JQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxTzBNBgNVBAsM +RnRoaWpzaGVpamRlbkBUaGlqc3MtTUJQLmRvbWFpbl9ub3Rfc2V0LmludmFsaWQg +KFRoaWpzIHZhbiBkZXIgSGVpamRlbikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQC2w3PIK5efAeKzi/P5DTvoZ5pajZcqgxXnKWHwa4L/f9Jb//9QBuo4 +4I0wlnMePg3luFaJsLnI1znWiY/iuf53P68w8dNHlX4ZMP6JaPROAoix5bPGRjXY +jeWUtsdOyfmVtALdYfmI/MWUxf3+D7gvMiBwngSYJn9xKDdkK8Pwj1i+CiJ15fUQ +rKQHS3tFTFp8szzYtc8L6E+43/WfLj79YEfDE5vIV+35vgF+GCbVs7C9aw547oaE +zIOmLDRNxjOUMQbESbIM8X/32FfKYGjYJPGWM+1MbSfcfQSFQ/mSephek25Gke/B +W8m+20afeOPNq8n8Et0xvl2eEsydr/0tAgMBAAGjdDByMA4GA1UdDwEB/wQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBTjYFGuSfA7hYlAtujE +7clLgFRoKjAqBgNVHREEIzAhgh9sb2NhbC5kYXRhc3Ryb3BoZS5zY2llbmNlLnV1 +Lm5sMA0GCSqGSIb3DQEBCwUAA4IBgQCTnvdrsD8YUMXuOisyZgCBo9welMyvLTgJ +/A782zv93qpJZ6PDu1R5ivKPOhEivHuzvB0ujza403Urj9w7KXjZQL9jQnFFcdDO +07PD+dCmOjiPsHS7WP8GLHbSgKs6vmosxKwUA7znB8faDrOLmsDnnunOJ+U6u2BM +p9k5/n85b9wp7MiHs+EBTKDqKaooZ9gqibDmSyXv6dNZyWDomLZiFB8U5V02oFVY +sEh7Kl91A89M4fvJe45uMChbgAmbxUOofNlDMAr8tXs+E9Y8W6hV7CFqw6dU1bM+ +HoqawYPvMIJpSOmR8ZBCz9zd9noXybpE2SQdgaB3oLg7cz6FdO2VVnnyGSAE2bfT +vM+a86rAl5BttyolDfgU+jfRqK9j7ZQs5oR5ab0Ck90bUbESzRQfzHfp9I9weYK3 +SNpsbZYMcjagZZPqVsxK8+rh3SRrdcwiJhO2Xm1DktnBJG6TMfN3zZG648iGGbMC +K5BZL8VW6rrgbC9FvM2xq68KAnaZ06M= +-----END CERTIFICATE----- diff --git a/certs/local-key.pem b/certs/local-key.pem new file mode 100644 index 0000000000000000000000000000000000000000..8e69d4d5cf2bfdfb9add399bd5d2836b951860cf --- /dev/null +++ b/certs/local-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2w3PIK5efAeKz +i/P5DTvoZ5pajZcqgxXnKWHwa4L/f9Jb//9QBuo44I0wlnMePg3luFaJsLnI1znW +iY/iuf53P68w8dNHlX4ZMP6JaPROAoix5bPGRjXYjeWUtsdOyfmVtALdYfmI/MWU +xf3+D7gvMiBwngSYJn9xKDdkK8Pwj1i+CiJ15fUQrKQHS3tFTFp8szzYtc8L6E+4 +3/WfLj79YEfDE5vIV+35vgF+GCbVs7C9aw547oaEzIOmLDRNxjOUMQbESbIM8X/3 +2FfKYGjYJPGWM+1MbSfcfQSFQ/mSephek25Gke/BW8m+20afeOPNq8n8Et0xvl2e +Esydr/0tAgMBAAECggEBAImNj13PmV0egS5bHjOLB1TCbQTMXkKryFdj6QeXE1AT +NxLxGIp1ueE2+GziPyA62iDUXaVh8mI7wpc6j6W4ENJVhxiSWU8eL3rsShbHfGAe +Ph2OYYDQJQSov2rvKhCdqRBIHIPckn/MpzRy09hcomY7FvlLpO+SwgY7m3z0B5PM +OWpy2qmHi3WZQ7f5ejdwb9A4kCVA6Max4NgMi637QY/ShQwqEQ20tB9XNDIpKyTV +uwKgpZjAislpDwPmhyi5x9VMD9/qABFcg8JLnZ8MSebncFkteuIIR4eI3osg4bAx +17O5u93juPOxGui2dEhPFf/YZrUWpsik3/RjAnuHPQ0CgYEA5gaeXk/S7zbgHq3g +z5uj/m1lRb4XYVcL7PTkRuqzywXfqSRdOlmYbClevgDWlU2tcZ5N+Kjj4ee7Xxel +PMtpoNDgGnB/Leb/F4pqHuxXNQvfN8KlnXuk0SDHs3VRd4YR54C0PFKcCpCQXMpb +qFxUG0ob/+WmT30wFTz4PqYaktcCgYEAy2adjHpkG3jK9lShY3At8gX4ikDxKHUe +SAGGYMNWe6cjlp5X5PHjALn1sJEnJ5d50y/jwn/pUz/GvbHuV54XqsWUW1Z28M+O +I/IOOM/eFrY5I3+E7YX/gZv9XV+GPS5cDw2U9WMJBtSZFteqclhdK/YMGVexQ/ab +OUEM0S+h85sCgYEA4T0H5BfL11tnmALxCLlBmwgpy2H46OLglZWy1dJKXXmR6cDm +3RUQUJEt6WVOuYIHXSMC/IL8KZ/M9K9lqKMR+lutpZYUorD3hmiNw1vvhMzsNWCO +5SdGW1T61zoAnMYWUBbR5eOKUjn+ci3gFHrcDKDDzA5mjJ1r8M/z0Py7Np8CgYB5 +pU5WRKB4WZ6xAd5flSi1VWLWdI6GDr1kfRz74/dmDojXPK3+a7fCqHTK+5S6NfqT +FlIV8d/+fOcEblTIK/GlnXLjyWtrDAbLcqmUyqTdWnADzfEXCQvNXRiDbmzfTEmc +axgKRgeRATbplWQH7NcUQpvr0ClhJyygakobFWy7PwKBgDH35TEzSrTSc3B++zHU +OIM5MfQho9eOu844udnJq2M6DfymsE1ZFb7MB8nJkcivL+NJANaCExkfDr6a71+O +BuElJlnd45w8bBt0y0Oxv2B6R/vbLwfs1UaA5Bd48exM+irYcivPqq6od0Y/zAC6 +85AtslKKD101x65j6Iy9fp69 +-----END PRIVATE KEY----- diff --git a/libs/shared/data-access/authorization/.babelrc b/libs/shared/data-access/authorization/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..cf7ddd99c615a064ac18eb3109eee4f394ab1faf --- /dev/null +++ b/libs/shared/data-access/authorization/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/libs/shared/data-access/authorization/.eslintrc.json b/libs/shared/data-access/authorization/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..632e9b0e22253922989d1153e06f7ba996c72d38 --- /dev/null +++ b/libs/shared/data-access/authorization/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/shared/data-access/authorization/README.md b/libs/shared/data-access/authorization/README.md new file mode 100644 index 0000000000000000000000000000000000000000..009a2ead17f3f87e2bc4b246ad9ceae1423f9813 --- /dev/null +++ b/libs/shared/data-access/authorization/README.md @@ -0,0 +1,7 @@ +# shared-data-access-authorization + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shared-data-access-authorization` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared/data-access/authorization/jest.config.js b/libs/shared/data-access/authorization/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..6bae255b477a179c550eade1547895b0cd8372d0 --- /dev/null +++ b/libs/shared/data-access/authorization/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'shared-data-access-authorization', + preset: '../../../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '<rootDir>/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: + '../../../../coverage/libs/shared/data-access/authorization', +}; diff --git a/libs/shared/data-access/authorization/project.json b/libs/shared/data-access/authorization/project.json new file mode 100644 index 0000000000000000000000000000000000000000..7225438fd977b6401038fbabfffa797328d1b7c1 --- /dev/null +++ b/libs/shared/data-access/authorization/project.json @@ -0,0 +1,23 @@ +{ + "root": "libs/shared/data-access/authorization", + "sourceRoot": "libs/shared/data-access/authorization/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/shared/data-access/authorization/**/*.ts"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/shared/data-access/authorization"], + "options": { + "jestConfig": "libs/shared/data-access/authorization/jest.config.js", + "passWithNoTests": true + } + } + }, + "tags": [] +} diff --git a/libs/shared/data-access/authorization/src/index.ts b/libs/shared/data-access/authorization/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0309d93b41a3f18d0294efe7999d9038b98f42ee --- /dev/null +++ b/libs/shared/data-access/authorization/src/index.ts @@ -0,0 +1 @@ +export { AuthorizationHandler } from './lib/authorizationHandler'; diff --git a/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts b/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..32adafab72440bdf5c30febfa6dd1c59d5737d3b --- /dev/null +++ b/libs/shared/data-access/authorization/src/lib/authorizationHandler.ts @@ -0,0 +1,193 @@ +import { Cookies } from 'react-cookie'; + +export class AuthorizationHandler { + private static _instance: AuthorizationHandler; + private accessToken = ''; + private authorized = false; + private userID = ''; + private sessionID = ''; + private callback?: () => void = undefined; + + // instance gets the AuthorizationHandler singleton instance + public static instance(): AuthorizationHandler { + if (!AuthorizationHandler._instance) { + AuthorizationHandler._instance = new AuthorizationHandler(); + } + + return AuthorizationHandler._instance; + } + + // SetCallback sets a function that will be called when the auth state changes + // TODO: This should be done with a store or custom hook + public setCallback(callback: () => void) { + this.callback = callback; + } + + // MARK: Authorization code + + /** + * Authorize attempts to authorize using a refresh-token set as a cookie. If the user has been inactive for more than 7 days this cookie will be gone. + * @returns true is authorization was successful, else returns false + */ + public async Authorize(): Promise<boolean> { + // Attempt to log in with a refresh-token + const authResponse = await this.getNewAccessToken(); + + // If the request was a success, we have an accessToken, userID and sessionID + if (authResponse.success) { + // Store them + this.accessToken = authResponse.accessToken ?? ''; + this.userID = authResponse.userID ?? ''; + this.sessionID = authResponse.sessionID ?? ''; + + // Init refresh token + this.initialiseRefreshToken(); + + // Start the automatic refreshing every 10 minutes + setInterval(() => { + this.refreshTokens(); + }, 10 * 60 * 1000); + } + + return new Promise((resolve) => { + resolve(authResponse.success); + }); + } + + /** + * getNewAccessToken gets a new access token using the refresh-token cookie + * @returns an authResponse containing details + */ + private async getNewAccessToken(): Promise<authResponse> { + // If we have an access token already, append it to the url as a query param to keep sessionID the same + let url = 'https://datastrophe.science.uu.nl/auth/refresh'; + if (this.accessToken != '') { + url += '?access_token=' + this.accessToken; + } + + return new Promise<authResponse>((resolve) => { + fetch(url, { + method: 'GET', + credentials: 'include', + }) + .then((response) => { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); + }) + .then((responseJSON) => { + resolve({ + success: true, + accessToken: responseJSON.accessToken, + userID: responseJSON.userID, + sessionID: responseJSON.sessionID, + }); + }) + .catch(() => { + // User is not authorized + resolve({ success: false }); + }); + }); + } + + /** + * refreshTokens refreshes tokens + */ + private async refreshTokens() { + console.log('refreshing tokens'); + // Get a new access + refresh token pair + const authResponse = await this.getNewAccessToken(); + + if (authResponse.success) { + // Set the new access token + this.accessToken = authResponse.accessToken ?? ''; + + // Initialise the new refresh token + this.initialiseRefreshToken(); + } + } + + /** + * initialiseRefreshToken attempts to initialise a refresh token + */ + private async initialiseRefreshToken() { + fetch('https://datastrophe.science.uu.nl/auth/refresh', { + method: 'POST', + credentials: 'include', + }) + .then((response) => { + if (!response.ok) { + throw Error(response.statusText); + } + }) + .catch((error) => { + console.error(error); + }); + } + + // MARK: Getters + + /** + * Authorized returns the current authorization status + * @returns true if authorized + */ + Authorized(): boolean { + return this.authorized; + } + + /** + * AccessToken returns the current access token + * @returns token + */ + AccessToken(): string { + return this.accessToken; + } + + /** + * UserID returns the current user' ID + * @returns id + */ + UserID(): string { + return this.userID; + } + + /** + * SessionID returns the current session' ID + * @returns id + */ + SessionID(): string { + return this.sessionID; + } + + // MARK: Setters + /** + * SetAccessToken sets the current access token (should only be called by the sign-in component) + * @param accessToken + */ + SetAccessToken(accessToken: string) { + this.accessToken = accessToken; + + console.log(this.accessToken); + + // Activate the refresh token + this.initialiseRefreshToken(); + + // Start the automatic refreshing every 10 minutes + setInterval(() => { + this.refreshTokens(); + }, 10 * 60 * 1000); + + // TODO: Change auth state + if (this.callback) { + this.callback(); + } + } +} + +type authResponse = { + success: boolean; + accessToken?: string; + userID?: string; + sessionID?: string; +}; diff --git a/libs/shared/data-access/authorization/tsconfig.json b/libs/shared/data-access/authorization/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..d0953a0f8b8084fd7ca99de4a00f82435f8679cd --- /dev/null +++ b/libs/shared/data-access/authorization/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/libs/shared/data-access/authorization/tsconfig.lib.json b/libs/shared/data-access/authorization/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..2ef844c42b4d526dd97ef3b18591cc5c652781e5 --- /dev/null +++ b/libs/shared/data-access/authorization/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": [] + }, + "include": ["**/*.ts"], + "exclude": ["**/*.spec.ts"] +} diff --git a/libs/shared/data-access/authorization/tsconfig.spec.json b/libs/shared/data-access/authorization/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..315a5b0bbebaca96617a8dd5353901287ebd8e68 --- /dev/null +++ b/libs/shared/data-access/authorization/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/package.json b/package.json index f642de7d3e9e56e5ef1a0e278deffc1dd9fc11bb..226ea71c49a69e10d7ee9d79d3c926d64f36adad 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "graphology": "^0.23.2", "graphology-types": "^0.23.0", "react": "17.0.2", + "react-cookie": "^4.1.1", "react-dom": "17.0.2", "react-flow-renderer": "^9.7.4", "react-grid-layout": "^1.3.3", diff --git a/tsconfig.base.json b/tsconfig.base.json index 4f689f610a680e42f9bc6583c8ec06d80b46ee87..ac6b15170da814b379cec144deafff5b91b2ece1 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,9 @@ "@graphpolaris/schema/schema-usecases": [ "libs/schema/schema-usecases/src/index.ts" ], + "@graphpolaris/shared/data-access/authorization": [ + "libs/shared/data-access/authorization/src/index.ts" + ], "@graphpolaris/shared/data-access/store": [ "libs/shared/data-access/store/src/index.ts" ], diff --git a/workspace.json b/workspace.json index 1369ffa1cca6a284a3d2dc70381d94ff38ddb536..66837864c2722a33d3536b08716bd3ff66c0c93f 100644 --- a/workspace.json +++ b/workspace.json @@ -5,6 +5,7 @@ "shared-data-access-store": "libs/shared/data-access/store", "shared-data-access-theme": "libs/shared/data-access/theme", "web-graphpolaris": "apps/web-graphpolaris", - "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e" + "web-graphpolaris-e2e": "apps/web-graphpolaris-e2e", + "shared-data-access-authorization": "libs/shared/data-access/authorization" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9fba981e220c3108648ca9a4d690e6358dda0311..b072098d4cd3c7ebce9706974f16b1249f69c429 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4142,6 +4142,10 @@ version "3.19.4" resolved "https://registry.yarnpkg.com/@types/cytoscape/-/cytoscape-3.19.4.tgz#f41214103b80ff3d7d8741bacc32265ed90e45b5" integrity sha512-0IozTg1vdZrA3nuAK5o9Pa8nl2INUnTaXwcGwoiALDcsD8/TiVnp0Zi+R1IiPRG6edoy0Ya61/3osFLtfkhhmw== +"@types/cookie@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" + integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== "@types/eslint-scope@^3.7.0": version "3.7.3" @@ -4217,7 +4221,7 @@ dependencies: "@types/unist" "*" -"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": +"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.0.1", "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -6988,6 +6992,11 @@ cookie@0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@^0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + copy-concurrently@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" @@ -13945,6 +13954,15 @@ react-colorful@^5.1.2: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784" integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg== +react-cookie@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-4.1.1.tgz#832e134ad720e0de3e03deaceaab179c4061a19d" + integrity sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A== + dependencies: + "@types/hoist-non-react-statics" "^3.0.1" + hoist-non-react-statics "^3.0.0" + universal-cookie "^4.0.0" + react-docgen-typescript@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c" @@ -16430,6 +16448,14 @@ unist-util-visit@2.0.3, unist-util-visit@^2.0.0: unist-util-is "^4.0.0" unist-util-visit-parents "^3.0.0" +universal-cookie@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.4.tgz#06e8b3625bf9af049569ef97109b4bb226ad798d" + integrity sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw== + dependencies: + "@types/cookie" "^0.3.3" + cookie "^0.4.0" + universalify@^0.1.0, universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"