From b549d6e91dd21c6c567420d25c99ceb81acad4f5 Mon Sep 17 00:00:00 2001 From: slumbering Date: Fri, 11 Jun 2021 12:24:58 +0200 Subject: [PATCH] docs: :sparkles: swizzle doc page to implement github auth access Signed-off-by: slumbering --- website/.env | 2 + website/docusaurus.config.js | 9 +- website/package.json | 2 + website/src/api/github.js | 58 ++++++ website/src/components/DocAuthentication.js | 13 ++ .../components/DocAuthentication.module.css | 13 ++ website/src/components/Spinner.js | 8 + website/src/components/Spinner.module.css | 64 ++++++ website/src/theme/DocPage/index.js | 195 ++++++++++++++++++ website/src/theme/DocPage/styles.module.css | 98 +++++++++ .../img/Dagger_Website_Space_Uranus.png | Bin 0 -> 6732 bytes website/yarn.lock | 31 +++ 12 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 website/.env create mode 100644 website/src/api/github.js create mode 100644 website/src/components/DocAuthentication.js create mode 100644 website/src/components/DocAuthentication.module.css create mode 100644 website/src/components/Spinner.js create mode 100644 website/src/components/Spinner.module.css create mode 100644 website/src/theme/DocPage/index.js create mode 100644 website/src/theme/DocPage/styles.module.css create mode 100644 website/static/img/Dagger_Website_Space_Uranus.png diff --git a/website/.env b/website/.env new file mode 100644 index 00000000..6737a79d --- /dev/null +++ b/website/.env @@ -0,0 +1,2 @@ +REACT_APP_CLIENT_ID=cd8f9be2562bfc8d6cfc +REACT_APP_CLIENT_SECRET=4856ebc1101d1228e21b0c9705e8d08105804a3e \ No newline at end of file diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 3ed9b38a..61866f85 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -12,12 +12,10 @@ module.exports = { organizationName: "Dagger", projectName: "Dagger", stylesheets: [ - "https://fonts.gstatic.com", - "https://fonts.googleapis.com/css2?family=Poppins:wght@700&display=swap", "https://fonts.googleapis.com/css2?family=Karla&family=Poppins:wght@700&display=swap", ], themeConfig: { - sidebarCollapsible: false, + sidebarCollapsible: true, prism: { theme: require("prism-react-renderer/themes/okaidia"), }, @@ -58,5 +56,8 @@ module.exports = { }, ], ], - plugins: ["docusaurus-plugin-sass"], + plugins: [ + "docusaurus-plugin-sass", + "docusaurus2-dotenv" + ], }; diff --git a/website/package.json b/website/package.json index 795dccc3..575a5e80 100644 --- a/website/package.json +++ b/website/package.json @@ -20,9 +20,11 @@ "@svgr/webpack": "^5.5.0", "clsx": "^1.1.1", "docusaurus-plugin-sass": "^0.2.0", + "docusaurus2-dotenv": "^1.4.0", "file-loader": "^6.2.0", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-social-login-buttons": "^3.4.0", "sass": "^1.34.1", "url-loader": "^4.1.1" }, diff --git a/website/src/api/github.js b/website/src/api/github.js new file mode 100644 index 00000000..30ff6bfb --- /dev/null +++ b/website/src/api/github.js @@ -0,0 +1,58 @@ +import axios from 'axios'; + +const AxiosInstance = axios.create({ + headers: { 'Accept': 'application/vnd.github.v3+json' }, +}); + +async function getAccessToken(code) { + + try { + const getAccessToken = await AxiosInstance.get('https://github.com/login/oauth/access_token', { + params: { + code, + client_id: process.env.REACT_APP_CLIENT_ID, + client_secret: process.env.REACT_APP_CLIENT_SECRET, + }, + validateStatus: function (status) { + return status < 500; // Resolve only if the status code is less than 500 + } + }) + + return getAccessToken.data; + } catch (error) { + console.log("error getAccessToken", error.message) + } +} + +export async function getUser(access_token) { + try { + const getUserLogin = await AxiosInstance.get("https://api.github.com/user", { + headers: { Authorization: `token ${access_token}` }, + validateStatus: function (status) { + return status < 500; // Resolve only if the status code is less than 500 + } + }) + + return { + login: getUserLogin.data.login, + status: getUserLogin.status + } + } catch (error) { + console.log("error getUser", error.message) + } +} + +export async function checkUserCollaboratorStatus(code) { + const { access_token } = await getAccessToken(code) + const { login } = await getUser(access_token) + try { + const isUserCollaborator = await AxiosInstance.get(`https://docs-access.dagger.io/u/${login}`) + + return { + status: isUserCollaborator.status, + access_token + } + } catch (error) { + console.log("error checkUserCollaboratorStatus", error.message); + } +} \ No newline at end of file diff --git a/website/src/components/DocAuthentication.js b/website/src/components/DocAuthentication.js new file mode 100644 index 00000000..de2199fa --- /dev/null +++ b/website/src/components/DocAuthentication.js @@ -0,0 +1,13 @@ +import React from "react"; +import { GithubLoginButton } from 'react-social-login-buttons'; +import style from './DocAuthentication.module.css' + +export default function DocAuthentication() { + return ( +
+

Welcome on Dagger documentation

+

Please Sign in to Github in order to get access to the doc

+ window.location.href = `//github.com/login/oauth/authorize?client_id=${process.env.REACT_APP_CLIENT_ID}&scope=user&allow_signup=false`} /> +
+ ) +} \ No newline at end of file diff --git a/website/src/components/DocAuthentication.module.css b/website/src/components/DocAuthentication.module.css new file mode 100644 index 00000000..5fd0ada2 --- /dev/null +++ b/website/src/components/DocAuthentication.module.css @@ -0,0 +1,13 @@ +.container { + background: url("/img/Dagger_Website_Space_Uranus.png") no-repeat; + background-size: cover; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.btn__github { + width: 240px !important; +} \ No newline at end of file diff --git a/website/src/components/Spinner.js b/website/src/components/Spinner.js new file mode 100644 index 00000000..1cdb3161 --- /dev/null +++ b/website/src/components/Spinner.js @@ -0,0 +1,8 @@ +import React from 'react'; +import styles from './Spinner.module.css'; + +export default function Spinner() { + return ( +
+ ) +} \ No newline at end of file diff --git a/website/src/components/Spinner.module.css b/website/src/components/Spinner.module.css new file mode 100644 index 00000000..15f5d9de --- /dev/null +++ b/website/src/components/Spinner.module.css @@ -0,0 +1,64 @@ +.ellipsis { + display: inline-block; + position: absolute; + top: 50vh; + z-index: 99999; + left: 50vw; + transform: translate(-50%, -50%); + width: 80px; + height: 80px; +} +.ellipsis div { + position: absolute; + top: 33px; + width: 13px; + height: 13px; + border-radius: 50%; + background: var(--ifm-color-primary-dark); + animation-timing-function: cubic-bezier(0, 1, 1, 0); +} + +html[data-theme="dark"] .ellipsis div { + background: var(--ifm-color-primary-light); +} + +.ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; +} +.ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; +} +.ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; +} +.ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; +} +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } +} +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + } +} +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(24px, 0); + } +} diff --git a/website/src/theme/DocPage/index.js b/website/src/theme/DocPage/index.js new file mode 100644 index 00000000..b5d67846 --- /dev/null +++ b/website/src/theme/DocPage/index.js @@ -0,0 +1,195 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import React, { useState, useEffect, useCallback } from 'react'; +import { MDXProvider } from '@mdx-js/react'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import renderRoutes from '@docusaurus/renderRoutes'; +import Layout from '@theme/Layout'; +import DocSidebar from '@theme/DocSidebar'; +import MDXComponents from '@theme/MDXComponents'; +import NotFound from '@theme/NotFound'; +import IconArrow from '@theme/IconArrow'; +import { matchPath } from '@docusaurus/router'; +import { translate } from '@docusaurus/Translate'; +import clsx from 'clsx'; +import styles from './styles.module.css'; +import { ThemeClassNames, docVersionSearchTag } from '@docusaurus/theme-common'; +import { Redirect } from "react-router"; +import qs from 'querystringify'; +import isEmpty from 'lodash/isEmpty'; +import { checkUserCollaboratorStatus, getUser } from '../../api/github' +import { GithubLoginButton } from 'react-social-login-buttons'; +import Spinner from '../../components/Spinner'; +import DocAuthentication from '../../components/DocAuthentication'; + +function DocPageContent({ currentDocRoute, versionMetadata, children }) { + const { siteConfig, isClient } = useDocusaurusContext(); + const { pluginId, permalinkToSidebar, docsSidebars, version } = versionMetadata; + const sidebarName = permalinkToSidebar[currentDocRoute.path]; + const sidebar = docsSidebars[sidebarName]; + const [hiddenSidebarContainer, setHiddenSidebarContainer] = useState(false); + const [hiddenSidebar, setHiddenSidebar] = useState(false); + const toggleSidebar = useCallback(() => { + if (hiddenSidebar) { + setHiddenSidebar(false); + } + + setHiddenSidebarContainer(!hiddenSidebarContainer); + }, [hiddenSidebar]); + return ( + +
+ {sidebar && ( +
{ + if ( + !e.currentTarget.classList.contains(styles.docSidebarContainer) + ) { + return; + } + + if (hiddenSidebarContainer) { + setHiddenSidebar(true); + } + }} + role="complementary"> + + + {hiddenSidebar && ( +
+ +
+ )} +
+ )} +
+
+ {children} +
+
+
+
+ ); +} + +function DocPage(props) { + const { + route: { routes: docRoutes }, + versionMetadata, + location, + } = props; + const currentDocRoute = docRoutes.find((docRoute) => + matchPath(location.pathname, docRoute), + ); + + // CUSTOM DOCPAGE + const [isUserAuthorized, setIsUserAuthorized] = useState() + const [isLoading, setIsLoading] = useState(true) + const [redirectState, setRedirectState] = useState() + const authQuery = qs.parse(location.search); + const [userAccessToken, setUserAccessToken] = useState((() => { + if (typeof window !== "undefined") return window.localStorage.getItem('user-github-key') + })()) + + useEffect(async () => { + if (userAccessToken) { + const user = await getUser(userAccessToken) + setIsUserAuthorized(user) + } else { + if (!isEmpty(authQuery)) { //callback after successful auth with github) + const isUserCollaborator = await checkUserCollaboratorStatus(authQuery.code); + if (isUserCollaborator?.status === 200) { + setUserAccessToken(isUserCollaborator.access_token) + if (typeof window !== "undefined") window.localStorage.setItem('user-github-key', isUserCollaborator.access_token); + } else { + setIsUserAuthorized({ status: 401 }) + } + } + } + setIsLoading(false) + }, [userAccessToken]) + + + if (isLoading) return + + if ((isUserAuthorized?.status && isUserAuthorized?.status === 401)) { + return

Redirection vers dagger.io...

+ } + + if (!isUserAuthorized) { + return ( + + ) + } + + // END CUSTOM DOCPAGE + + if (!currentDocRoute) { + return ; + } + + return ( + + {renderRoutes(docRoutes)} + + ); +} + +export default DocPage; diff --git a/website/src/theme/DocPage/styles.module.css b/website/src/theme/DocPage/styles.module.css new file mode 100644 index 00000000..19bfa5fc --- /dev/null +++ b/website/src/theme/DocPage/styles.module.css @@ -0,0 +1,98 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +:root { + --doc-sidebar-width: 300px; +} + +:global(.docs-wrapper) { + display: flex; +} + +.docPage, +.docMainContainer { + display: flex; + width: 100%; +} + +@media (min-width: 997px) { + .docMainContainer { + flex-grow: 1; + max-width: calc(100% - var(--doc-sidebar-width)); + } + + .docMainContainerEnhanced { + max-width: none; + } + + .docSidebarContainer { + width: var(--doc-sidebar-width); + margin-top: calc(-1 * var(--ifm-navbar-height)); + border-right: 1px solid var(--ifm-toc-border-color); + will-change: width; + transition: width var(--ifm-transition-fast) ease; + clip-path: inset(0); + } + + .docSidebarContainerHidden { + width: 30px; + cursor: pointer; + } + + .collapsedDocSidebar { + position: sticky; + top: 0; + height: 100%; + max-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + transition: background-color var(--ifm-transition-fast) ease; + } + + .collapsedDocSidebar:hover, + .collapsedDocSidebar:focus { + background-color: var(--ifm-color-emphasis-200); + } + + .expandSidebarButtonIcon { + transform: rotate(0); + } + html[dir='rtl'] .expandSidebarButtonIcon { + transform: rotate(180deg); + } + + html[data-theme='dark'] .collapsedDocSidebar:hover, + html[data-theme='dark'] .collapsedDocSidebar:focus { + background-color: var(--collapse-button-bg-color-dark); + } + + .docItemWrapperEnhanced { + max-width: calc(var(--ifm-container-width) + var(--doc-sidebar-width)); + } +} + +@media (max-width: 996px) { + .docSidebarContainer { + margin-top: 0; + } +} + +@media (min-width: 997px) and (max-width: 1320px) { + .docItemWrapper { + max-width: calc( + var(--ifm-container-width) - var(--doc-sidebar-width) - + var(--ifm-spacing-horizontal) * 2 + ); + } + + .docItemWrapperEnhanced { + max-width: calc( + var(--ifm-container-width) - var(--ifm-spacing-horizontal) * 2 + ); + } +} diff --git a/website/static/img/Dagger_Website_Space_Uranus.png b/website/static/img/Dagger_Website_Space_Uranus.png new file mode 100644 index 0000000000000000000000000000000000000000..b1b3f4bf2f773aafc5be48b8e3d9143cf3aff981 GIT binary patch literal 6732 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 vz_lWA7~*T)P