From b7e2d3e0a7d96770991a849f193ac6464aaa879c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 Aug 2022 19:38:09 -0500 Subject: [PATCH 1/8] Add inert oauth consumer buttons --- .../auth_login/components/consumer-button.tsx | 46 +++++++++++++++++++ .../auth_login/components/login_form.tsx | 19 ++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/features/auth_login/components/consumer-button.tsx diff --git a/app/soapbox/features/auth_login/components/consumer-button.tsx b/app/soapbox/features/auth_login/components/consumer-button.tsx new file mode 100644 index 000000000..63ca9d3df --- /dev/null +++ b/app/soapbox/features/auth_login/components/consumer-button.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { useIntl, defineMessages } from 'react-intl'; + +import { IconButton, Tooltip } from 'soapbox/components/ui'; + +const messages = defineMessages({ + tooltip: { id: 'oauth_consumer.tooltip', defaultMessage: 'Sign in with {provider}' }, +}); + +/** Map between OAuth providers and brand icons. */ +const BRAND_ICONS: Record = { + twitter: require('@tabler/icons/brand-twitter.svg'), + facebook: require('@tabler/icons/brand-facebook.svg'), + google: require('@tabler/icons/brand-google.svg'), + microsoft: require('@tabler/icons/brand-windows.svg'), + slack: require('@tabler/icons/brand-slack.svg'), + github: require('@tabler/icons/brand-github.svg'), +}; + +/** Capitalize the first letter of a string. */ +// https://stackoverflow.com/a/1026087 +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +interface IConsumerButton { + provider: string, +} + +/** OAuth consumer button for logging in with a third-party service. */ +const ConsumerButton: React.FC = ({ provider }) => { + const intl = useIntl(); + const icon = BRAND_ICONS[provider] || require('@tabler/icons/key.svg'); + + return ( + + + + ); +}; + +export default ConsumerButton; diff --git a/app/soapbox/features/auth_login/components/login_form.tsx b/app/soapbox/features/auth_login/components/login_form.tsx index 9e8b1dc35..44b8e6df5 100644 --- a/app/soapbox/features/auth_login/components/login_form.tsx +++ b/app/soapbox/features/auth_login/components/login_form.tsx @@ -1,8 +1,12 @@ +import { List as ImmutableList } from 'immutable'; import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; -import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; +import { Button, Form, FormActions, FormGroup, HStack, Input, Stack } from 'soapbox/components/ui'; +import { useAppSelector } from 'soapbox/hooks'; + +import ConsumerButton from './consumer-button'; const messages = defineMessages({ username: { @@ -22,6 +26,7 @@ interface ILoginForm { const LoginForm: React.FC = ({ isLoading, handleSubmit }) => { const intl = useIntl(); + const providers = useAppSelector(state => ImmutableList(state.instance.pleroma.get('oauth_consumer_strategies'))); return (
@@ -29,7 +34,7 @@ const LoginForm: React.FC = ({ isLoading, handleSubmit }) => {

-
+
= ({ isLoading, handleSubmit }) => { -
+ + {(providers.size > 0) && ( + + {providers.map(provider => ( + + ))} + + )} + ); }; From 304e9aa880b640bdfe9ba20dd77e459f42a3ec2d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 Aug 2022 19:46:56 -0500 Subject: [PATCH 2/8] Move oauth ConsumersList into its own component --- .../auth_login/components/consumers-list.tsx | 29 +++++++++++++++++++ .../auth_login/components/login_form.tsx | 15 ++-------- 2 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 app/soapbox/features/auth_login/components/consumers-list.tsx diff --git a/app/soapbox/features/auth_login/components/consumers-list.tsx b/app/soapbox/features/auth_login/components/consumers-list.tsx new file mode 100644 index 000000000..368bc63f4 --- /dev/null +++ b/app/soapbox/features/auth_login/components/consumers-list.tsx @@ -0,0 +1,29 @@ +import { List as ImmutableList } from 'immutable'; +import React from 'react'; + +import { HStack } from 'soapbox/components/ui'; +import { useAppSelector } from 'soapbox/hooks'; + +import ConsumerButton from './consumer-button'; + +interface IConsumersList { +} + +/** Displays OAuth consumers to log in with. */ +const ConsumersList: React.FC = () => { + const providers = useAppSelector(state => ImmutableList(state.instance.pleroma.get('oauth_consumer_strategies'))); + + if (providers.size > 0) { + return ( + + {providers.map(provider => ( + + ))} + + ); + } else { + return null; + } +}; + +export default ConsumersList; diff --git a/app/soapbox/features/auth_login/components/login_form.tsx b/app/soapbox/features/auth_login/components/login_form.tsx index 44b8e6df5..24e4e3740 100644 --- a/app/soapbox/features/auth_login/components/login_form.tsx +++ b/app/soapbox/features/auth_login/components/login_form.tsx @@ -1,12 +1,10 @@ -import { List as ImmutableList } from 'immutable'; import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; -import { Button, Form, FormActions, FormGroup, HStack, Input, Stack } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; +import { Button, Form, FormActions, FormGroup, Input, Stack } from 'soapbox/components/ui'; -import ConsumerButton from './consumer-button'; +import ConsumersList from './consumers-list'; const messages = defineMessages({ username: { @@ -26,7 +24,6 @@ interface ILoginForm { const LoginForm: React.FC = ({ isLoading, handleSubmit }) => { const intl = useIntl(); - const providers = useAppSelector(state => ImmutableList(state.instance.pleroma.get('oauth_consumer_strategies'))); return (
@@ -82,13 +79,7 @@ const LoginForm: React.FC = ({ isLoading, handleSubmit }) => { - {(providers.size > 0) && ( - - {providers.map(provider => ( - - ))} - - )} +
); From 609eb543ba991827107913b75845024fb05e5e01 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 Aug 2022 19:56:48 -0500 Subject: [PATCH 3/8] Style ConsumersList --- .../auth_login/components/consumers-list.tsx | 18 ++++++++++++------ .../auth_login/components/login_form.tsx | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/soapbox/features/auth_login/components/consumers-list.tsx b/app/soapbox/features/auth_login/components/consumers-list.tsx index 368bc63f4..84f65d900 100644 --- a/app/soapbox/features/auth_login/components/consumers-list.tsx +++ b/app/soapbox/features/auth_login/components/consumers-list.tsx @@ -1,7 +1,8 @@ import { List as ImmutableList } from 'immutable'; import React from 'react'; +import { FormattedMessage } from 'react-intl'; -import { HStack } from 'soapbox/components/ui'; +import { Card, HStack, Text } from 'soapbox/components/ui'; import { useAppSelector } from 'soapbox/hooks'; import ConsumerButton from './consumer-button'; @@ -15,11 +16,16 @@ const ConsumersList: React.FC = () => { if (providers.size > 0) { return ( - - {providers.map(provider => ( - - ))} - + + + + + + {providers.map(provider => ( + + ))} + + ); } else { return null; diff --git a/app/soapbox/features/auth_login/components/login_form.tsx b/app/soapbox/features/auth_login/components/login_form.tsx index 24e4e3740..5b1f6f733 100644 --- a/app/soapbox/features/auth_login/components/login_form.tsx +++ b/app/soapbox/features/auth_login/components/login_form.tsx @@ -31,7 +31,7 @@ const LoginForm: React.FC = ({ isLoading, handleSubmit }) => {

- +
Date: Thu, 11 Aug 2022 21:50:08 -0500 Subject: [PATCH 4/8] Make consumer strategy buttons work --- app/soapbox/actions/consumer-auth.ts | 55 +++++++++++++++++++ .../auth_login/components/consumer-button.tsx | 9 +++ 2 files changed, 64 insertions(+) create mode 100644 app/soapbox/actions/consumer-auth.ts diff --git a/app/soapbox/actions/consumer-auth.ts b/app/soapbox/actions/consumer-auth.ts new file mode 100644 index 000000000..b669c6393 --- /dev/null +++ b/app/soapbox/actions/consumer-auth.ts @@ -0,0 +1,55 @@ +import axios from 'axios'; + +import * as BuildConfig from 'soapbox/build_config'; +import { isURL } from 'soapbox/utils/auth'; +import sourceCode from 'soapbox/utils/code'; +import { getFeatures } from 'soapbox/utils/features'; + +import { createApp } from './apps'; + +import type { AppDispatch, RootState } from 'soapbox/store'; + +const createProviderApp = () => { + return async(dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const { scopes } = getFeatures(state.instance); + + const params = { + client_name: sourceCode.displayName, + redirect_uris: `${window.location.origin}/login/external`, + website: sourceCode.homepage, + scopes, + }; + + return dispatch(createApp(params)); + }; +}; + +export const prepareRequest = (provider: string) => { + return async(dispatch: AppDispatch, getState: () => RootState) => { + const baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : ''; + + const state = getState(); + const { scopes } = getFeatures(state.instance); + const app = await dispatch(createProviderApp()); + const { client_id, redirect_uri } = app; + + localStorage.setItem('soapbox:external:app', JSON.stringify(app)); + localStorage.setItem('soapbox:external:baseurl', baseURL); + localStorage.setItem('soapbox:external:scopes', scopes); + + const params = { + provider, + authorization: { + client_id, + redirect_uri, + scope: scopes, + }, + }; + + const formdata = axios.toFormData(params); + const query = new URLSearchParams(formdata as any); + + location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`; + }; +}; diff --git a/app/soapbox/features/auth_login/components/consumer-button.tsx b/app/soapbox/features/auth_login/components/consumer-button.tsx index 63ca9d3df..8ce78e799 100644 --- a/app/soapbox/features/auth_login/components/consumer-button.tsx +++ b/app/soapbox/features/auth_login/components/consumer-button.tsx @@ -1,7 +1,9 @@ import React from 'react'; import { useIntl, defineMessages } from 'react-intl'; +import { prepareRequest } from 'soapbox/actions/consumer-auth'; import { IconButton, Tooltip } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; const messages = defineMessages({ tooltip: { id: 'oauth_consumer.tooltip', defaultMessage: 'Sign in with {provider}' }, @@ -30,14 +32,21 @@ interface IConsumerButton { /** OAuth consumer button for logging in with a third-party service. */ const ConsumerButton: React.FC = ({ provider }) => { const intl = useIntl(); + const dispatch = useAppDispatch(); + const icon = BRAND_ICONS[provider] || require('@tabler/icons/key.svg'); + const handleClick = () => { + dispatch(prepareRequest(provider)); + }; + return ( ); From f2fc3698773e5e40a49ebdf22e02050c1b1538c1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 Aug 2022 22:06:38 -0500 Subject: [PATCH 5/8] Allow configuring authProvider in place of registrations --- .../auth_login/components/consumer-button.tsx | 7 +--- app/soapbox/features/landing_page/index.tsx | 36 +++++++++++++++++-- .../normalizers/soapbox/soapbox_config.ts | 1 + app/soapbox/utils/strings.ts | 7 ++++ 4 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 app/soapbox/utils/strings.ts diff --git a/app/soapbox/features/auth_login/components/consumer-button.tsx b/app/soapbox/features/auth_login/components/consumer-button.tsx index 8ce78e799..0f003e98a 100644 --- a/app/soapbox/features/auth_login/components/consumer-button.tsx +++ b/app/soapbox/features/auth_login/components/consumer-button.tsx @@ -4,6 +4,7 @@ import { useIntl, defineMessages } from 'react-intl'; import { prepareRequest } from 'soapbox/actions/consumer-auth'; import { IconButton, Tooltip } from 'soapbox/components/ui'; import { useAppDispatch } from 'soapbox/hooks'; +import { capitalize } from 'soapbox/utils/strings'; const messages = defineMessages({ tooltip: { id: 'oauth_consumer.tooltip', defaultMessage: 'Sign in with {provider}' }, @@ -19,12 +20,6 @@ const BRAND_ICONS: Record = { github: require('@tabler/icons/brand-github.svg'), }; -/** Capitalize the first letter of a string. */ -// https://stackoverflow.com/a/1026087 -function capitalize(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - interface IConsumerButton { provider: string, } diff --git a/app/soapbox/features/landing_page/index.tsx b/app/soapbox/features/landing_page/index.tsx index dcb9afd36..a5b0e58e8 100644 --- a/app/soapbox/features/landing_page/index.tsx +++ b/app/soapbox/features/landing_page/index.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import { prepareRequest } from 'soapbox/actions/consumer-auth'; import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification_badge'; import RegistrationForm from 'soapbox/features/auth_login/components/registration_form'; -import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; +import { capitalize } from 'soapbox/utils/strings'; const LandingPage = () => { + const dispatch = useAppDispatch(); const features = useFeatures(); const soapboxConfig = useSoapboxConfig(); const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true; @@ -40,6 +43,29 @@ const LandingPage = () => { return ; }; + /** Display login button for external provider. */ + const renderProvider = () => { + const { authProvider } = soapboxConfig; + + return ( + + + + + + + + + + ); + }; + /** Pepe API registrations are open */ const renderPepe = () => { return ( @@ -47,7 +73,9 @@ const LandingPage = () => { - Let's get started! + + + Social Media Without Discrimination @@ -58,7 +86,9 @@ const LandingPage = () => { // Render registration flow depending on features const renderBody = () => { - if (pepeEnabled && pepeOpen) { + if (soapboxConfig.authProvider) { + return renderProvider(); + } else if (pepeEnabled && pepeOpen) { return renderPepe(); } else if (features.accountCreation && instance.registrations) { return renderOpen(); diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts index 6e9a2c745..a471401c5 100644 --- a/app/soapbox/normalizers/soapbox/soapbox_config.ts +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -71,6 +71,7 @@ export const CryptoAddressRecord = ImmutableRecord({ export const SoapboxConfigRecord = ImmutableRecord({ ads: ImmutableList(), appleAppId: null, + authProvider: '', logo: '', logoDarkMode: null, banner: '', diff --git a/app/soapbox/utils/strings.ts b/app/soapbox/utils/strings.ts new file mode 100644 index 000000000..c1c8e08bc --- /dev/null +++ b/app/soapbox/utils/strings.ts @@ -0,0 +1,7 @@ +/** Capitalize the first letter of a string. */ +// https://stackoverflow.com/a/1026087 +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export { capitalize }; From 386af1ea2ce4aba0cddf05b162d098b43fc047bf Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 Aug 2022 22:10:16 -0500 Subject: [PATCH 6/8] i18n pepe locales --- app/soapbox/features/landing_page/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/landing_page/index.tsx b/app/soapbox/features/landing_page/index.tsx index a5b0e58e8..1e3b01f80 100644 --- a/app/soapbox/features/landing_page/index.tsx +++ b/app/soapbox/features/landing_page/index.tsx @@ -76,10 +76,14 @@ const LandingPage = () => { - Social Media Without Discrimination + + + - + ); }; From bc72739dda9ea43461f2667730878279218db46a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 Aug 2022 22:31:53 -0500 Subject: [PATCH 7/8] Normalize instance in login tests --- .../auth_login/components/__tests__/login_form.test.tsx | 7 ++++--- .../auth_login/components/__tests__/login_page.test.tsx | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/auth_login/components/__tests__/login_form.test.tsx b/app/soapbox/features/auth_login/components/__tests__/login_form.test.tsx index b46acf31a..8c388601f 100644 --- a/app/soapbox/features/auth_login/components/__tests__/login_form.test.tsx +++ b/app/soapbox/features/auth_login/components/__tests__/login_form.test.tsx @@ -1,6 +1,7 @@ -import { Map as ImmutableMap } from 'immutable'; import React from 'react'; +import { normalizeInstance } from 'soapbox/normalizers'; + import { fireEvent, render, screen } from '../../../../jest/test-helpers'; import LoginForm from '../login_form'; @@ -8,7 +9,7 @@ describe('', () => { it('renders for Pleroma', () => { const mockFn = jest.fn(); const store = { - instance: ImmutableMap({ + instance: normalizeInstance({ version: '2.7.2 (compatible; Pleroma 2.3.0)', }), }; @@ -21,7 +22,7 @@ describe('', () => { it('renders for Mastodon', () => { const mockFn = jest.fn(); const store = { - instance: ImmutableMap({ + instance: normalizeInstance({ version: '3.0.0', }), }; diff --git a/app/soapbox/features/auth_login/components/__tests__/login_page.test.tsx b/app/soapbox/features/auth_login/components/__tests__/login_page.test.tsx index 50f94b285..70a0f3b95 100644 --- a/app/soapbox/features/auth_login/components/__tests__/login_page.test.tsx +++ b/app/soapbox/features/auth_login/components/__tests__/login_page.test.tsx @@ -1,13 +1,14 @@ -import { Map as ImmutableMap } from 'immutable'; import React from 'react'; +import { normalizeInstance } from 'soapbox/normalizers'; + import { render, screen } from '../../../../jest/test-helpers'; import LoginPage from '../login_page'; describe('', () => { it('renders correctly on load', () => { const store = { - instance: ImmutableMap({ + instance: normalizeInstance({ version: '2.7.2 (compatible; Pleroma 2.3.0)', }), }; From 30df3808e7cf505a681d76c2c679f6cd22a6fd32 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 13 Aug 2022 09:40:51 -0500 Subject: [PATCH 8/8] ConsumerButton: use 'outlined' theme --- app/soapbox/features/auth_login/components/consumer-button.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/auth_login/components/consumer-button.tsx b/app/soapbox/features/auth_login/components/consumer-button.tsx index 0f003e98a..6be1088af 100644 --- a/app/soapbox/features/auth_login/components/consumer-button.tsx +++ b/app/soapbox/features/auth_login/components/consumer-button.tsx @@ -38,7 +38,8 @@ const ConsumerButton: React.FC = ({ provider }) => { return (