From 0309638909fe5b107366ff8e288ca25352221677 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 13 Feb 2021 00:26:45 +0300 Subject: Adds Basic Auth Functionality Moves all authorization functionality to a new file, and adds a helper to send discord OAuth code to the backend, and set JWT. Adds a library to read and set cookies. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 208 ++++++++++++++++++++++++++++++++++ src/pages/CallbackPage.tsx | 5 +- src/tests/pages/CallbackPage.test.tsx | 15 ++- 3 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 src/api/auth.ts (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..ae20236 --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,208 @@ +import Cookies, { CookieSetOptions } from "universal-cookie"; +import { AxiosResponse } from "axios"; + +import APIClient from "./client"; + +const OAUTH2_CLIENT_ID = process.env.REACT_APP_OAUTH2_CLIENT_ID; +const PRODUCTION = process.env.NODE_ENV !== "development"; + +/** + * Authorization result as returned from the backend. + */ +interface AuthResult { + token: string, + expiry: string +} + +interface JWTResponse { + JWT: string, + Expiry: Date +} + +/** + * Name properties for authorization cookies. + */ +enum CookieNames { + Scopes = "DiscordOAuthScopes", + Token = "FormBackendToken" +} + +/** + * [Reference]{@link https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes} + * + * Commented out enums are locked behind whitelists. + */ +export enum OAuthScopes { + Bot = "bot", + Connections = "connections", + Email = "email", + Identify = "identify", + Guilds = "guilds", + GuildsJoin = "guilds.join", + GDMJoin = "gdm.join", + MessagesRead = "messages.read", + // RPC = "rpc", + // RPC_API = "rpc.api", + // RPCNotifRead = "rpc.notifications.read", + WebhookIncoming = "webhook.incoming", + // AppsBuildsUpload = "applications.builds.upload", + AppsBuildsRead = "applications.builds.read", + AppsStoreUpdate = "applications.store.update", + AppsEntitlements = "applications.entitlements", + // RelationshipsRead = "relationships.read", + // ActivitiesRead = "activities.read", + // ActivitiesWrite = "activities.write", + AppsCommands = "applications.commands", + AppsCommandsUpdate = "applications.commands.update" +} + +/** + * Helper method to ensure the minimum required scopes + * for the application to function exist in a list. + */ +function ensureMinimumScopes(scopes: unknown, expected: OAuthScopes | OAuthScopes[]): OAuthScopes[] { + let result: OAuthScopes[] = []; + if (scopes && Array.isArray(scopes)) { + result = scopes; + } + + if (Array.isArray(expected)) { + expected.forEach(scope => { if (!result.includes(scope)) result.push(scope); }); + } else { + if (!result.includes(expected)) result.push(expected); + } + + return result; +} + +/** + * Return true if the program has the requested scopes or higher. + */ +export function checkScopes(scopes?: OAuthScopes[], path = ""): boolean { + const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); + + // Get Active Scopes And Ensure Type + const cookies = new Cookies().get(CookieNames.Scopes + path); + if (!cookies || !Array.isArray(cookies)) { + return false; + } + + // Check For Scope Existence + for (const scope of cleanedScopes) { + if (!cookies.includes(scope)) { + return false; + } + } + + return true; +} + +/*** + * Request authorization code from the discord api with the provided scopes. + * + * If disable function is passed, the component will be disabled while the login is ongoing. + * + * @returns {code, cleanedScopes} The discord authorization code and the scopes the code is granted for. + * @throws {Error} Indicates that an integrity check failed. + */ +export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (newState: boolean) => void): Promise<{code: string, cleanedScopes: OAuthScopes[]}> { + const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); + const disable = (newState: boolean) => { if (disableFunction) disableFunction(newState); }; + + disable(true); + + // Generate a new user state + const state = crypto.getRandomValues(new Uint32Array(1))[0]; + + const scopeString = encodeURIComponent(cleanedScopes.join(" ")); + const redirectURI = encodeURIComponent(document.location.protocol + "//" + document.location.host + "/callback"); + + // Open login window + const windowRef = window.open( + `https://discord.com/api/oauth2/authorize?client_id=${OAUTH2_CLIENT_ID}&state=${state}&response_type=code&scope=${scopeString}&redirect_uri=${redirectURI}&prompt=none`, + "Discord_OAuth2", + "height=700,width=500,location=no,menubar=no,resizable=no,status=no,titlebar=no,left=300,top=300" + ); + + // Clean up on login + const interval = setInterval(() => { + if (windowRef?.closed) { + clearInterval(interval); + disable(false); + } + }, 500); + + // Handle response + const code = await new Promise(resolve => { + window.onmessage = (message: MessageEvent) => { + if (message.data.source) { + // React DevTools has a habit of sending messages, ignore them. + return; + } + + if (message.isTrusted) { + windowRef?.close(); + + clearInterval(interval); + disable(false); + + // State integrity check + if (message.data.state !== state.toString()) { + // This indicates a lack of integrity + throw Error("Integrity check failed."); + } + + // Remove handler + window.onmessage = null; + resolve(message.data.code); + } + }; + }); + + return {code: code, cleanedScopes: cleanedScopes}; +} + +/** + * Sends a discord code from a given path to the backend, + * and returns the resultant JWT and expiry date. + */ +export async function requestBackendJWT(code: string): Promise { + const result = await APIClient.post("/auth/authorize", {token: code}) + .catch(reason => { throw reason; }) // TODO: Show some sort of authentication failure here + .then((response: AxiosResponse) => { + const _expiry = new Date(); + _expiry.setTime(Date.parse(response.data.expiry)); + + return {JWT: response.data.token, Expiry: _expiry}; + }); + + if (!result.JWT || !result.Expiry) { + throw Error("Could not fetch OAuth code."); + } + + return result; +} + +/** + * Handle a full authorization flow. Sets a token for the specified path with the JWT and scopes. + * + * @param scopes The scopes that should be authorized for the application. + * @param disableFunction An optional function that can disable a component while processing. + * @param path The site path to save the token under. + */ +export default function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): void { + if (!checkScopes(scopes, path)) { + const cookies = new Cookies; + cookies.remove(CookieNames.Token + path); + cookies.remove(CookieNames.Scopes + path); + + getDiscordCode(scopes || [], disableFunction).then(discord_response =>{ + requestBackendJWT(discord_response.code).then(backend_response => { + const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; + + cookies.set(CookieNames.Token + path, backend_response.JWT, options); + cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); + }); + }); + } +} diff --git a/src/pages/CallbackPage.tsx b/src/pages/CallbackPage.tsx index fab2086..00feb76 100644 --- a/src/pages/CallbackPage.tsx +++ b/src/pages/CallbackPage.tsx @@ -7,11 +7,12 @@ export default function CallbackPage(): JSX.Element { const params = new URLSearchParams(location.search); const code = params.get("code"); + const state = params.get("state"); if (!hasSent) { setHasSent(true); - window.opener.postMessage(code); + window.opener.postMessage({code: code, state: state}); } - return

Code is {code}

; + return
; } diff --git a/src/tests/pages/CallbackPage.test.tsx b/src/tests/pages/CallbackPage.test.tsx index 9049ca3..70f2fed 100644 --- a/src/tests/pages/CallbackPage.test.tsx +++ b/src/tests/pages/CallbackPage.test.tsx @@ -3,21 +3,20 @@ import { render } from "@testing-library/react"; import CallbackPage from "../../pages/CallbackPage"; -test("callback page renders provided code", () => { +test("callback page sends provided code", () => { global.opener = { postMessage: jest.fn() }; - const mockLocation = new URL("https://forms.pythondiscord.com/authorize?code=abcdef"); + const mockLocation = new URL("https://forms.pythondiscord.com/authorize?code=abcde_code&state=abcde_state"); Object.defineProperty(global, "location", {value: mockLocation}); - const comp = ; + render(); - const { getByText } = render(comp); - - - const codeText = getByText(/Code is abcdef/); - expect(codeText).toBeInTheDocument(); expect(global.opener.postMessage).toBeCalledTimes(1); + expect(global.opener.postMessage).toBeCalledWith({ + code: "abcde_code", + state: "abcde_state" + }); }); -- cgit v1.2.3 From edcdc9b18dddb30f2726da282efcf77ab5ad3c1a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 13 Feb 2021 00:27:03 +0300 Subject: Cleans Up OAuth Button Removes OAuth button from home page, and redesigns it. Uses new authorization functionality in auth. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/OAuth2Button.tsx | 91 +++++++----------------------- src/pages/LandingPage.tsx | 11 +--- src/tests/components/OAuth2Button.test.tsx | 2 +- 3 files changed, 23 insertions(+), 81 deletions(-) (limited to 'src') diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 4fa3f61..231e560 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -1,88 +1,39 @@ /** @jsx jsx */ import { css, jsx } from "@emotion/react"; +import { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faDiscord } from "@fortawesome/free-brands-svg-icons"; -import colors from "../colors"; -import { useState } from "react"; - -const OAUTH2_CLIENT_ID = process.env.REACT_APP_OAUTH2_CLIENT_ID; +import authenticate, { OAuthScopes } from "../api/auth"; -const buttonStyling = css` -display: flex; -background-color: ${colors.blurple}; -border: none; -color: white; -font-family: "Hind", "Helvetica", "Arial", sans-serif; -border-radius: 5px; -padding-top: 10px; -padding-bottom: 10px; -padding-right: 20px; -padding-left: 20px; -outline: none; -transition: filter 100ms; -font-size: 1.2em; -align-items: center; - -span { - vertical-align: middle; -} -&:hover:enabled { - filter: brightness(110%); - cursor: pointer; +interface OAuth2ButtonProps { + scopes?: OAuthScopes[], + path?: string } -&:disabled { - background-color: ${colors.greyple}; -} +const iconStyles = css` + position: relative; + top: 0.3rem; + padding-left: 0.65rem; + font-size: 1.2em; `; -function doLogin(disableFunction: (newState: boolean) => void) { - disableFunction(true); - - const redirectURI = encodeURIComponent(document.location.protocol + "//" + document.location.host + "/callback"); - - const windowRef = window.open( - `https://discord.com/api/oauth2/authorize?client_id=${OAUTH2_CLIENT_ID}&response_type=code&scope=identify&redirect_uri=${redirectURI}&prompt=none`, - "Discord_OAuth2", - "height=700,width=500,location=no,menubar=no,resizable=no,status=no,titlebar=no,left=300,top=300" - ); - - const interval = setInterval(() => { - if (windowRef?.closed) { - clearInterval(interval); - disableFunction(false); - } - }, 500); - - window.onmessage = (code: MessageEvent) => { - if (code.data.source) { - // React DevTools has a habit of sending messages, ignore them. - return; - } - - if (code.isTrusted) { - windowRef?.close(); - - console.log("Code received:", code.data); - - disableFunction(false); - clearInterval(interval); - - window.onmessage = null; - } - }; -} +const textStyles = css` + display: inline-block; + padding: 0.5rem 0.75rem 0.5rem 0.5rem; +`; -function OAuth2Button(): JSX.Element { +function OAuth2Button(props: OAuth2ButtonProps): JSX.Element { const [disabled, setDisabled] = useState(false); - return ; + return ( + + ); } export default OAuth2Button; diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 06fef46..efb3f05 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from "react"; import HeaderBar from "../components/HeaderBar"; import FormListing from "../components/FormListing"; -import OAuth2Button from "../components/OAuth2Button"; import Loading from "../components/Loading"; import ScrollToTop from "../components/ScrollToTop"; @@ -28,16 +27,8 @@ function LandingPage(): JSX.Element {
-
+

Available Forms

- - - - {forms.map(form => ( ))} diff --git a/src/tests/components/OAuth2Button.test.tsx b/src/tests/components/OAuth2Button.test.tsx index f05159f..a773686 100644 --- a/src/tests/components/OAuth2Button.test.tsx +++ b/src/tests/components/OAuth2Button.test.tsx @@ -4,7 +4,7 @@ import OAuth2Button from "../../components/OAuth2Button"; test("renders oauth2 sign in button text", () => { const { getByText } = render(); - const button = getByText(/Sign in with Discord/i); + const button = getByText(/Discord Login/i); expect(button).toBeInTheDocument(); }); -- cgit v1.2.3 From 2350aa12fae661e37895616e0e7fe3e7ebc62f53 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 13 Feb 2021 01:13:37 +0300 Subject: Makes Authorize Helper Async Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts index ae20236..fa3ac85 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -190,19 +190,21 @@ export async function requestBackendJWT(code: string): Promise { * @param disableFunction An optional function that can disable a component while processing. * @param path The site path to save the token under. */ -export default function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): void { +export default async function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): Promise { if (!checkScopes(scopes, path)) { const cookies = new Cookies; cookies.remove(CookieNames.Token + path); cookies.remove(CookieNames.Scopes + path); - getDiscordCode(scopes || [], disableFunction).then(discord_response =>{ - requestBackendJWT(discord_response.code).then(backend_response => { + await getDiscordCode(scopes || [], disableFunction).then(async discord_response =>{ + await requestBackendJWT(discord_response.code).then(backend_response => { const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; cookies.set(CookieNames.Token + path, backend_response.JWT, options); cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); }); }); + + return new Promise(resolve => resolve()); } } -- cgit v1.2.3 From 451c825c77cd68eafeb262eb1ea5cbfc21dce550 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 13 Feb 2021 01:21:36 +0300 Subject: Dynamically Show Discord OAuth Button Dynamically displays an auth button in place of the submit button if needed, and adds full authorization flow. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/commonStyles.tsx | 32 ++++++++++++++++++++ src/components/OAuth2Button.tsx | 14 +++++++-- src/pages/FormPage.tsx | 65 ++++++++++++++++++++--------------------- 3 files changed, 74 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/commonStyles.tsx b/src/commonStyles.tsx index 89a2746..eb3e319 100644 --- a/src/commonStyles.tsx +++ b/src/commonStyles.tsx @@ -1,4 +1,5 @@ import { css } from "@emotion/react"; +import colors from "./colors"; const selectable = css` -moz-user-select: text; @@ -50,6 +51,36 @@ const textInputs = css` border-radius: 8px; `; +const submitStyles = css` + text-align: right; + + button:disabled { + background-color: ${colors.greyple}; + cursor: default; + } + + button { + cursor: pointer; + + border: none; + border-radius: 8px; + + color: white; + font: inherit; + + background-color: ${colors.blurple}; + transition: background-color 300ms; + } + + button[type="submit"] { + padding: 0.55rem 4.25rem; + } + + button:enabled:hover { + background-color: ${colors.darkerBlurple}; + } +`; + export { selectable, @@ -57,4 +88,5 @@ export { hiddenInput, multiSelectInput, textInputs, + submitStyles }; diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 231e560..90a25fa 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -5,12 +5,13 @@ import { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faDiscord } from "@fortawesome/free-brands-svg-icons"; -import authenticate, { OAuthScopes } from "../api/auth"; +import authenticate, {checkScopes, OAuthScopes} from "../api/auth"; interface OAuth2ButtonProps { scopes?: OAuthScopes[], - path?: string + path?: string, + rerender: () => void } const iconStyles = css` @@ -27,9 +28,16 @@ const textStyles = css` function OAuth2Button(props: OAuth2ButtonProps): JSX.Element { const [disabled, setDisabled] = useState(false); + async function login() { + await authenticate(props.scopes, setDisabled, props.path); + + if (checkScopes(props.scopes, props.path)) { + props.rerender(); + } + } return ( - diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index c49b9fd..4237e86 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -9,10 +9,12 @@ import HeaderBar from "../components/HeaderBar"; import RenderedQuestion from "../components/Question"; import Loading from "../components/Loading"; import ScrollToTop from "../components/ScrollToTop"; +import OAuth2Button from "../components/OAuth2Button"; import { Form, FormFeatures, getForm } from "../api/forms"; +import { OAuthScopes, checkScopes } from "../api/auth"; import colors from "../colors"; -import { unselectable } from "../commonStyles"; +import { submitStyles, unselectable } from "../commonStyles"; interface PathParams { @@ -20,10 +22,13 @@ interface PathParams { } interface NavigationProps { - form_state: boolean // Whether the form is open or not + form_state: boolean, // Whether the form is open or not + scopes: OAuthScopes[] } class Navigation extends React.Component { + PAGE_PATH = "/form" + containerStyles = css` margin: auto; width: 50%; @@ -38,7 +43,7 @@ class Navigation extends React.Component { width: 50%; } - @media (max-width: 850px) { + @media (max-width: 870px) { width: 100%; > div { @@ -62,13 +67,13 @@ class Navigation extends React.Component { height: 0; display: none; - @media (max-width: 850px) { + @media (max-width: 870px) { display: block; } `; returnStyles = css` - padding: 0.5rem 2rem; + padding: 0.55rem 2.2rem; border-radius: 8px; color: white; @@ -83,36 +88,21 @@ class Navigation extends React.Component { } `; - submitStyles = css` - text-align: right; - - button { - padding: 0.5rem 4rem; - cursor: pointer; - - border: none; - border-radius: 8px; - - color: white; - font: inherit; - - background-color: ${colors.blurple}; - transition: background-color 300ms; - } - - button:hover { - background-color: ${colors.darkerBlurple}; - } - `; + constructor(props: NavigationProps) { + super(props); + this.setState({"logged_in": false}); + } render(): JSX.Element { let submit = null; + if (this.props.form_state) { - submit = ( -
- -
- ); + if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes, this.PAGE_PATH)) { + // Render OAuth button if login is required, and the scopes needed are not available + submit = this.setState({"logged_in": true})}/>; + } else { + submit = ; + } } return ( @@ -121,7 +111,7 @@ class Navigation extends React.Component { Return Home

- { submit } +
{ submit }
); } @@ -146,7 +136,7 @@ const closedHeaderStyles = css` font-size: 1.5rem; background-color: ${colors.error}; - + @media (max-width: 500px) { padding: 1rem 1.5rem; } @@ -186,6 +176,13 @@ function FormPage(): JSX.Element { } const open: boolean = form.features.includes(FormFeatures.Open); + const require_auth: boolean = form.features.includes(FormFeatures.RequiresLogin); + + const scopes = []; + if (require_auth) { + scopes.push(OAuthScopes.Identify); + if (form.features.includes(FormFeatures.CollectEmail)) { scopes.push(OAuthScopes.Email); } + } let closed_header = null; if (!open) { @@ -201,7 +198,7 @@ function FormPage(): JSX.Element { { closed_header } { questions } - +
-- cgit v1.2.3 From e8bd7798a0154725ba27295eceae7160156e9889 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 16 Feb 2021 23:28:48 +0300 Subject: Adds Error Handling To Auth Helpers Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 75 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts index fa3ac85..ad97e67 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -27,6 +27,19 @@ enum CookieNames { Token = "FormBackendToken" } +export interface APIErrors { + Message: APIErrorMessages, + Error: any, /* eslint-disable-line @typescript-eslint/no-explicit-any */ +} + +export enum APIErrorMessages { + BackendValidation = "Backend could not authorize with Discord. Please contact the forms team.", + BackendValidationDev = "Backend could not authorize with Discord, possibly due to being on a preview branch. Please contact the forms team.", + BackendUnresponsive = "Unable to reach the backend, please retry, or contact the forms team.", + BadResponse = "The server returned a bad response, please contact the forms team.", + Unknown = "An unknown error occurred, please contact the forms team." +} + /** * [Reference]{@link https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes} * @@ -100,16 +113,11 @@ export function checkScopes(scopes?: OAuthScopes[], path = ""): boolean { /*** * Request authorization code from the discord api with the provided scopes. * - * If disable function is passed, the component will be disabled while the login is ongoing. - * * @returns {code, cleanedScopes} The discord authorization code and the scopes the code is granted for. * @throws {Error} Indicates that an integrity check failed. */ -export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (newState: boolean) => void): Promise<{code: string, cleanedScopes: OAuthScopes[]}> { +export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: string, cleanedScopes: OAuthScopes[]}> { const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); - const disable = (newState: boolean) => { if (disableFunction) disableFunction(newState); }; - - disable(true); // Generate a new user state const state = crypto.getRandomValues(new Uint32Array(1))[0]; @@ -128,7 +136,6 @@ export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (n const interval = setInterval(() => { if (windowRef?.closed) { clearInterval(interval); - disable(false); } }, 500); @@ -144,7 +151,6 @@ export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (n windowRef?.close(); clearInterval(interval); - disable(false); // State integrity check if (message.data.state !== state.toString()) { @@ -165,19 +171,47 @@ export async function getDiscordCode(scopes: OAuthScopes[], disableFunction?: (n /** * Sends a discord code from a given path to the backend, * and returns the resultant JWT and expiry date. + * + * @throws { APIErrors } On error, the APIErrors.Message is set, and an APIErrors object is thrown. */ export async function requestBackendJWT(code: string): Promise { - const result = await APIClient.post("/auth/authorize", {token: code}) - .catch(reason => { throw reason; }) // TODO: Show some sort of authentication failure here - .then((response: AxiosResponse) => { - const _expiry = new Date(); - _expiry.setTime(Date.parse(response.data.expiry)); + const reason: APIErrors = { Message: APIErrorMessages.Unknown, Error: null }; + let result; + + try { + result = await APIClient.post("/auth/authorize", {token: code}) + .catch(error => { + reason.Error = error; + + if (error.response) { + // API Responded with a non-2xx Response + if (error.response.status === 400) { + reason.Message = process.env.CONTEXT === "deploy-preview" ? APIErrorMessages.BackendValidationDev : APIErrorMessages.BackendValidation; + } + } else if (error.request) { + // API did not respond + reason.Message = APIErrorMessages.BackendUnresponsive; + } - return {JWT: response.data.token, Expiry: _expiry}; - }); + throw error; + + }).then((response: AxiosResponse) => { + const expiry = new Date(); + expiry.setTime(Date.parse(response.data.expiry)); + + return {JWT: response.data.token, Expiry: expiry}; + }); + } catch (e) { + if (reason.Error === null) { + reason.Error = e; + } + + throw reason; + } if (!result.JWT || !result.Expiry) { - throw Error("Could not fetch OAuth code."); + reason.Message = APIErrorMessages.BadResponse; + throw reason; } return result; @@ -189,20 +223,25 @@ export async function requestBackendJWT(code: string): Promise { * @param scopes The scopes that should be authorized for the application. * @param disableFunction An optional function that can disable a component while processing. * @param path The site path to save the token under. + * + * @throws { APIErrors } See documentation on { requestBackendJWT }. */ -export default async function authorize(scopes?: OAuthScopes[], disableFunction?: (newState: boolean) => void, path = "/"): Promise { +export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void, path = "/"): Promise { if (!checkScopes(scopes, path)) { const cookies = new Cookies; cookies.remove(CookieNames.Token + path); cookies.remove(CookieNames.Scopes + path); - await getDiscordCode(scopes || [], disableFunction).then(async discord_response =>{ + if (disableFunction) { disableFunction(true); } + await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; cookies.set(CookieNames.Token + path, backend_response.JWT, options); cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); }); + }).finally(() => { + if (disableFunction) { disableFunction(false); } }); return new Promise(resolve => resolve()); -- cgit v1.2.3 From 2229ba931b6eadafc0c999d306fce9e6e3dc7339 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 16 Feb 2021 23:29:24 +0300 Subject: Adds Error Handler For OAuth Button Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/OAuth2Button.tsx | 64 ++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 90a25fa..61bcd99 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -1,11 +1,12 @@ /** @jsx jsx */ import { css, jsx } from "@emotion/react"; -import { useState } from "react"; +import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faDiscord } from "@fortawesome/free-brands-svg-icons"; -import authenticate, {checkScopes, OAuthScopes} from "../api/auth"; +import authenticate, { APIErrors, checkScopes, OAuthScopes } from "../api/auth"; +import { selectable } from "../commonStyles"; interface OAuth2ButtonProps { @@ -26,22 +27,63 @@ const textStyles = css` padding: 0.5rem 0.75rem 0.5rem 0.5rem; `; -function OAuth2Button(props: OAuth2ButtonProps): JSX.Element { - const [disabled, setDisabled] = useState(false); - async function login() { - await authenticate(props.scopes, setDisabled, props.path); +const errorStyles = css` + position: absolute; + visibility: hidden; + width: 100%; + left: 0; + + text-align: center; + white-space: normal; + box-sizing: border-box; + padding: 0 5rem; + + color: red; + margin-top: 2.5rem; +`; + +async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject, setDisabled: (newState: boolean) => void) { + await authenticate(props.scopes, setDisabled, props.path).catch((reason: APIErrors) => { + // Display Error Message + if (errorDialog.current) { + errorDialog.current.style.visibility = "visible"; + errorDialog.current.textContent = reason.Message; + errorDialog.current.scrollIntoView({behavior: "smooth"}); + } + + // Propagate to sentry + const error = reason.Error.toJSON(); + error["Custom Error Message"] = reason.Message; + + // Filter Discord code + if (error?.config?.data) { + const data = JSON.parse(error.config.data); + if (data["token"]) { + data["token"] = "[FILTERED]"; + } - if (checkScopes(props.scopes, props.path)) { - props.rerender(); + error.config.data = data; } + + throw error; + }); + + if (checkScopes(props.scopes, props.path)) { + props.rerender(); } +} + +function OAuth2Button(props: OAuth2ButtonProps): JSX.Element { + const [disabled, setDisabled] = useState(false); + const errorDialog: React.RefObject = React.useRef(null); - return ( - - ); +
+ ; } export default OAuth2Button; -- cgit v1.2.3 From 020b794ef1c1b3ba67711e19b0baee7687c92c96 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Tue, 16 Feb 2021 23:48:25 +0300 Subject: Cleans Up Error Display Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/OAuth2Button.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 61bcd99..b084571 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -36,7 +36,11 @@ const errorStyles = css` text-align: center; white-space: normal; box-sizing: border-box; - padding: 0 5rem; + + padding: 0 15rem; + @media (max-width: 750px) { + padding: 0 5rem; + } color: red; margin-top: 2.5rem; @@ -52,7 +56,7 @@ async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject Date: Wed, 17 Feb 2021 00:03:01 +0300 Subject: Cleans Up OAuth Error Logging Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/OAuth2Button.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index b084571..1ee456c 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -56,20 +56,8 @@ async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject Date: Wed, 17 Feb 2021 09:30:47 +0300 Subject: Removes Path From Auth Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 23 +++++++++++------------ src/components/OAuth2Button.tsx | 5 ++--- src/pages/FormPage.tsx | 6 ++---- 3 files changed, 15 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts index ad97e67..cfaa563 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -91,11 +91,11 @@ function ensureMinimumScopes(scopes: unknown, expected: OAuthScopes | OAuthScope /** * Return true if the program has the requested scopes or higher. */ -export function checkScopes(scopes?: OAuthScopes[], path = ""): boolean { +export function checkScopes(scopes?: OAuthScopes[]): boolean { const cleanedScopes = ensureMinimumScopes(scopes, OAuthScopes.Identify); // Get Active Scopes And Ensure Type - const cookies = new Cookies().get(CookieNames.Scopes + path); + const cookies = new Cookies().get(CookieNames.Scopes); if (!cookies || !Array.isArray(cookies)) { return false; } @@ -169,7 +169,7 @@ export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: stri } /** - * Sends a discord code from a given path to the backend, + * Sends a discord code to the backend, * and returns the resultant JWT and expiry date. * * @throws { APIErrors } On error, the APIErrors.Message is set, and an APIErrors object is thrown. @@ -218,27 +218,26 @@ export async function requestBackendJWT(code: string): Promise { } /** - * Handle a full authorization flow. Sets a token for the specified path with the JWT and scopes. + * Handle a full authorization flow. Sets a cookie with the JWT and scopes. * * @param scopes The scopes that should be authorized for the application. * @param disableFunction An optional function that can disable a component while processing. - * @param path The site path to save the token under. * * @throws { APIErrors } See documentation on { requestBackendJWT }. */ -export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void, path = "/"): Promise { - if (!checkScopes(scopes, path)) { +export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void): Promise { + if (!checkScopes(scopes)) { const cookies = new Cookies; - cookies.remove(CookieNames.Token + path); - cookies.remove(CookieNames.Scopes + path); + cookies.remove(CookieNames.Token); + cookies.remove(CookieNames.Scopes); if (disableFunction) { disableFunction(true); } await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION, path: path}; + const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION}; - cookies.set(CookieNames.Token + path, backend_response.JWT, options); - cookies.set(CookieNames.Scopes + path, discord_response.cleanedScopes, options); + cookies.set(CookieNames.Token, backend_response.JWT, options); + cookies.set(CookieNames.Scopes, discord_response.cleanedScopes, options); }); }).finally(() => { if (disableFunction) { disableFunction(false); } diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 1ee456c..25c5871 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -11,7 +11,6 @@ import { selectable } from "../commonStyles"; interface OAuth2ButtonProps { scopes?: OAuthScopes[], - path?: string, rerender: () => void } @@ -47,7 +46,7 @@ const errorStyles = css` `; async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject, setDisabled: (newState: boolean) => void) { - await authenticate(props.scopes, setDisabled, props.path).catch((reason: APIErrors) => { + await authenticate(props.scopes, setDisabled).catch((reason: APIErrors) => { // Display Error Message if (errorDialog.current) { errorDialog.current.style.visibility = "visible"; @@ -60,7 +59,7 @@ async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject { - PAGE_PATH = "/form" - containerStyles = css` margin: auto; width: 50%; @@ -97,9 +95,9 @@ class Navigation extends React.Component { let submit = null; if (this.props.form_state) { - if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes, this.PAGE_PATH)) { + if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes)) { // Render OAuth button if login is required, and the scopes needed are not available - submit = this.setState({"logged_in": true})}/>; + submit = this.setState({"logged_in": true})}/>; } else { submit = ; } -- cgit v1.2.3 From 604620f5f98a76461741212f9a78ea2468bce814 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 20 Feb 2021 03:55:27 +0300 Subject: Adds Token Refresh Adds automatic token refresh, and removes manual setting of JWT. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 84 +++++++++++++++++++++++++++++++++++-------------------- src/api/client.ts | 3 +- 2 files changed, 55 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts index cfaa563..1aba307 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -10,21 +10,16 @@ const PRODUCTION = process.env.NODE_ENV !== "development"; * Authorization result as returned from the backend. */ interface AuthResult { - token: string, + username: string, expiry: string } -interface JWTResponse { - JWT: string, - Expiry: Date -} - /** * Name properties for authorization cookies. */ enum CookieNames { Scopes = "DiscordOAuthScopes", - Token = "FormBackendToken" + Username = "DiscordUsername" } export interface APIErrors { @@ -169,12 +164,12 @@ export async function getDiscordCode(scopes: OAuthScopes[]): Promise<{code: stri } /** - * Sends a discord code to the backend, - * and returns the resultant JWT and expiry date. + * Sends a discord code to the backend, which sets an authentication JWT + * and returns the Discord username. * * @throws { APIErrors } On error, the APIErrors.Message is set, and an APIErrors object is thrown. */ -export async function requestBackendJWT(code: string): Promise { +export async function requestBackendJWT(code: string): Promise<{username: string, maxAge: number}> { const reason: APIErrors = { Message: APIErrorMessages.Unknown, Error: null }; let result; @@ -196,10 +191,8 @@ export async function requestBackendJWT(code: string): Promise { throw error; }).then((response: AxiosResponse) => { - const expiry = new Date(); - expiry.setTime(Date.parse(response.data.expiry)); - - return {JWT: response.data.token, Expiry: expiry}; + const expiry = Date.parse(response.data.expiry); + return {username: response.data.username, maxAge: (expiry - Date.now()) / 1000}; }); } catch (e) { if (reason.Error === null) { @@ -209,7 +202,7 @@ export async function requestBackendJWT(code: string): Promise { throw reason; } - if (!result.JWT || !result.Expiry) { + if (!result || !result.username || !result.maxAge) { reason.Message = APIErrorMessages.BadResponse; throw reason; } @@ -217,32 +210,61 @@ export async function requestBackendJWT(code: string): Promise { return result; } +/** + * Refresh the backend authentication JWT. Returns the success of the operation, and silently handles denied requests. + */ +export async function refreshBackendJWT(): Promise { + const cookies = new Cookies(); + + let pass = true; + APIClient.post("/auth/refresh").then((response: AxiosResponse) => { + cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION}); + + const expiry = Date.parse(response.data.expiry); + setTimeout(refreshBackendJWT, (expiry * 0.9)); + }).catch(() => { + pass = false; + cookies.remove(CookieNames.Scopes); + }); + + return new Promise(resolve => resolve(pass)); +} + /** * Handle a full authorization flow. Sets a cookie with the JWT and scopes. * * @param scopes The scopes that should be authorized for the application. * @param disableFunction An optional function that can disable a component while processing. + * @param refresh If true, the token refresh will be scehduled automatically * * @throws { APIErrors } See documentation on { requestBackendJWT }. */ -export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void): Promise { - if (!checkScopes(scopes)) { - const cookies = new Cookies; - cookies.remove(CookieNames.Token); - cookies.remove(CookieNames.Scopes); +export default async function authorize(scopes: OAuthScopes[] = [], disableFunction?: (newState: boolean) => void, refresh = true): Promise { + if (checkScopes(scopes)) { + return; + } - if (disableFunction) { disableFunction(true); } - await getDiscordCode(scopes).then(async discord_response =>{ - await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", expires: backend_response.Expiry, secure: PRODUCTION}; + const cookies = new Cookies; + cookies.remove(CookieNames.Scopes); - cookies.set(CookieNames.Token, backend_response.JWT, options); - cookies.set(CookieNames.Scopes, discord_response.cleanedScopes, options); - }); - }).finally(() => { - if (disableFunction) { disableFunction(false); } + if (disableFunction) { disableFunction(true); } + await getDiscordCode(scopes).then(async discord_response =>{ + await requestBackendJWT(discord_response.code).then(backend_response => { + const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION}; + cookies.set(CookieNames.Username, backend_response.username, options); + + options.maxAge = backend_response.maxAge; + cookies.set(CookieNames.Scopes, discord_response.cleanedScopes, options); + + if (refresh) { + // Schedule refresh after 90% of it's age + setTimeout(refreshBackendJWT, (backend_response.maxAge * 0.9) * 1000); + } }); + }).finally(() => { + if (disableFunction) { disableFunction(false); } + }); - return new Promise(resolve => resolve()); - } + + return new Promise(resolve => resolve()); } diff --git a/src/api/client.ts b/src/api/client.ts index b534938..a9499cc 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -2,5 +2,6 @@ import axios from "axios"; export default axios.create({ - baseURL: process.env.BACKEND_URL + baseURL: process.env.BACKEND_URL, + withCredentials: true }); -- cgit v1.2.3 From 54fd22c22ef1213ab5a8096d15f6f4cc79ac0998 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 21 Feb 2021 01:10:34 +0300 Subject: Saves Username & Scopes On `/` Path Explicitly sets the path attribute of username and scope cookies, to ensure they work correctly across page transitions, and to match the actual authorization cookie. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts index 1aba307..7bdf2bb 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -218,7 +218,7 @@ export async function refreshBackendJWT(): Promise { let pass = true; APIClient.post("/auth/refresh").then((response: AxiosResponse) => { - cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION}); + cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION, path: "/"}); const expiry = Date.parse(response.data.expiry); setTimeout(refreshBackendJWT, (expiry * 0.9)); @@ -250,7 +250,7 @@ export default async function authorize(scopes: OAuthScopes[] = [], disableFunct if (disableFunction) { disableFunction(true); } await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION}; + const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION, path: "/"}; cookies.set(CookieNames.Username, backend_response.username, options); options.maxAge = backend_response.maxAge; -- cgit v1.2.3 From b915ce2ba225d1c1209f7499618a67524450739f Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sat, 6 Mar 2021 23:17:54 +0300 Subject: Set Username Expiry To Permanent Extends username expiry to a very far date, to prevent it from expiring on session. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/api/auth.ts b/src/api/auth.ts index 7bdf2bb..f118108 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -218,7 +218,7 @@ export async function refreshBackendJWT(): Promise { let pass = true; APIClient.post("/auth/refresh").then((response: AxiosResponse) => { - cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION, path: "/"}); + cookies.set(CookieNames.Username, response.data.username, {sameSite: "strict", secure: PRODUCTION, path: "/", expires: new Date(3000, 1)}); const expiry = Date.parse(response.data.expiry); setTimeout(refreshBackendJWT, (expiry * 0.9)); @@ -250,7 +250,7 @@ export default async function authorize(scopes: OAuthScopes[] = [], disableFunct if (disableFunction) { disableFunction(true); } await getDiscordCode(scopes).then(async discord_response =>{ await requestBackendJWT(discord_response.code).then(backend_response => { - const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION, path: "/"}; + const options: CookieSetOptions = {sameSite: "strict", secure: PRODUCTION, path: "/", expires: new Date(3000, 1)}; cookies.set(CookieNames.Username, backend_response.username, options); options.maxAge = backend_response.maxAge; -- cgit v1.2.3 From 9f04d5f5effd07534c9eb5b9f8886c8f970d0dc6 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 7 Mar 2021 00:46:47 +0300 Subject: Fixes Return Home Button Centering Fixes the centering of the return home button on closed forms, by removing the style wrapper. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/OAuth2Button.tsx | 6 +++--- src/pages/FormPage.tsx | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx index 25c5871..885c080 100644 --- a/src/components/OAuth2Button.tsx +++ b/src/components/OAuth2Button.tsx @@ -31,16 +31,16 @@ const errorStyles = css` visibility: hidden; width: 100%; left: 0; - + text-align: center; white-space: normal; box-sizing: border-box; - + padding: 0 15rem; @media (max-width: 750px) { padding: 0 5rem; } - + color: red; margin-top: 2.5rem; `; diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index fa63282..8852ac5 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -96,12 +96,14 @@ class Navigation extends React.Component { let submit = null; if (this.props.form_state) { + let inner_submit; if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes)) { // Render OAuth button if login is required, and the scopes needed are not available - submit = this.setState({"logged_in": true})}/>; + inner_submit = this.setState({"logged_in": true})}/>; } else { - submit = ; + inner_submit = ; } + submit =
{ inner_submit }
; } return ( @@ -110,7 +112,7 @@ class Navigation extends React.Component { Return Home

-
{ submit }
+ { submit }
); } -- cgit v1.2.3 From df350092c32af5500e3d949987bc16dd9aaa5225 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 7 Mar 2021 03:17:33 +0300 Subject: Fixes Radio Button Rounding Makes radio buttons spheres instead of ovals. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/InputTypes/Radio.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/components/InputTypes/Radio.tsx b/src/components/InputTypes/Radio.tsx index a857964..d95dcdd 100644 --- a/src/components/InputTypes/Radio.tsx +++ b/src/components/InputTypes/Radio.tsx @@ -14,7 +14,7 @@ interface RadioProps { const styles = css` div { width: 0.7em; - height: 0.75em; + height: 0.7em; top: 0.18rem; border-radius: 50%; -- cgit v1.2.3 From b92baa668465c61170ca9cc8630c4f4b8bac7b71 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Sun, 7 Mar 2021 16:05:47 +0300 Subject: Makes Code Inputs TextAreas Changes the display of code to textareas until a proper solution is implemented. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/InputTypes/index.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'src') diff --git a/src/components/InputTypes/index.tsx b/src/components/InputTypes/index.tsx index bc65248..c6a83f1 100644 --- a/src/components/InputTypes/index.tsx +++ b/src/components/InputTypes/index.tsx @@ -1,5 +1,4 @@ import Checkbox from "./Checkbox"; -import Code from "./Code"; import Radio from "./Radio"; import Range from "./Range"; import Select from "./Select"; @@ -38,6 +37,7 @@ export default function create_input({ question, public_state }: QuestionProp, h /* eslint-disable react/react-in-jsx-scope */ switch (question.type) { + case QuestionType.Code: // TODO: Implement case QuestionType.TextArea: result =