[CORL-621] Auth Fixes (#2569)

* fix: resolve error with redirects

- fixes #2529

* fix: apply validations to username for oidc

* fix: converted components to function components

* fix: snapshots
This commit is contained in:
Wyatt Johnson
2019-09-18 18:01:06 +00:00
committed by Kim Gardner
parent 64f102e6d4
commit 921461008e
28 changed files with 567 additions and 555 deletions
@@ -53,13 +53,13 @@ const OIDCConfig: FunctionComponent<Props> = ({
}) => {
return (
<ConfigBoxWithToggleField
data-testid={`configure-auth-oidc-container`}
data-testid="configure-auth-oidc-container"
title={
<Localized id="configure-auth-oidc-loginWith">
<span>Login with OIDC</span>
</Localized>
}
name={`auth.integrations.oidc.enabled`}
name="auth.integrations.oidc.enabled"
disabled={disabled}
>
{disabledInside => (
@@ -84,7 +84,7 @@ const OIDCConfig: FunctionComponent<Props> = ({
</InputDescription>
</Localized>
<Field
name={`auth.integrations.oidc.name`}
name="auth.integrations.oidc.name"
validate={composeValidatorsWhen(isEnabled, required)}
parse={identity}
>
@@ -107,12 +107,12 @@ const OIDCConfig: FunctionComponent<Props> = ({
</FormField>
<ClientIDField
validate={composeValidatorsWhen(isEnabled, required)}
name={`auth.integrations.oidc.clientID`}
name="auth.integrations.oidc.clientID"
disabled={disabledInside}
/>
<ClientSecretField
validate={composeValidatorsWhen(isEnabled, required)}
name={`auth.integrations.oidc.clientSecret`}
name="auth.integrations.oidc.clientSecret"
disabled={disabledInside}
/>
<FormField>
@@ -127,7 +127,7 @@ const OIDCConfig: FunctionComponent<Props> = ({
</InputDescription>
</Localized>
<Field
name={`auth.integrations.oidc.issuer`}
name="auth.integrations.oidc.issuer"
validate={composeValidatorsWhen(isEnabled, required, validateURL)}
parse={identity}
>
@@ -165,7 +165,7 @@ const OIDCConfig: FunctionComponent<Props> = ({
<InputLabel>authorizationURL</InputLabel>
</Localized>
<Field
name={`auth.integrations.oidc.authorizationURL`}
name="auth.integrations.oidc.authorizationURL"
validate={composeValidatorsWhen(isEnabled, required, validateURL)}
parse={identity}
>
@@ -191,7 +191,7 @@ const OIDCConfig: FunctionComponent<Props> = ({
<InputLabel>tokenURL</InputLabel>
</Localized>
<Field
name={`auth.integrations.oidc.tokenURL`}
name="auth.integrations.oidc.tokenURL"
validate={composeValidatorsWhen(isEnabled, required, validateURL)}
parse={identity}
>
@@ -217,7 +217,7 @@ const OIDCConfig: FunctionComponent<Props> = ({
<InputLabel>jwksURI</InputLabel>
</Localized>
<Field
name={`auth.integrations.oidc.jwksURI`}
name="auth.integrations.oidc.jwksURI"
validate={composeValidatorsWhen(isEnabled, required, validateURL)}
parse={identity}
>
@@ -244,11 +244,11 @@ const OIDCConfig: FunctionComponent<Props> = ({
<span>Use OIDC login on</span>
</Localized>
}
name={`auth.integrations.oidc.targetFilter`}
name="auth.integrations.oidc.targetFilter"
disabled={disabledInside}
/>
<RegistrationField
name={`auth.integrations.oidc.allowRegistration`}
name="auth.integrations.oidc.allowRegistration"
disabled={disabledInside}
/>
</HorizontalGutter>
@@ -42,6 +42,9 @@ class OIDCConfigContainer extends React.Component<Props, State> {
issuer: form.getState().values.auth.integrations.oidc.issuer,
});
if (config) {
if (config.issuer) {
form.change("auth.integrations.oidc.issuer", config.issuer);
}
form.change(
"auth.integrations.oidc.authorizationURL",
config.authorizationURL
+1 -6
View File
@@ -1,9 +1,7 @@
import React, { FunctionComponent } from "react";
import { useResizeObserver } from "coral-framework/hooks";
import { PropTypesOf } from "coral-framework/types";
import resizePopup from "../dom/resizePopup";
import AddEmailAddress from "../views/AddEmailAddress";
import CreatePassword from "../views/CreatePassword";
import CreateUsername from "../views/CreateUsername";
@@ -50,11 +48,8 @@ const render = ({ view, auth, viewer }: AppProps) => {
};
const App: FunctionComponent<AppProps> = props => {
const ref = useResizeObserver(entry => {
resizePopup();
});
return (
<div ref={ref}>
<div>
{process.env.NODE_ENV !== "test" && <ViewRouter />}
<div>{render(props)}</div>
</div>
@@ -3,7 +3,7 @@
exports[`renders sign in 1`] = `
<div>
<div>
<withContext(withMutation(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(SignInContainer)))))))))
<withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(SignInContainer)))))
auth={Object {}}
/>
</div>
+1 -4
View File
@@ -1,9 +1,6 @@
function resizePopup() {
const innerHeight = window.document.body.offsetHeight;
window.resizeTo(
window.outerWidth,
innerHeight + window.outerHeight - window.innerHeight
);
window.resizeTo(350, innerHeight + window.outerHeight - window.innerHeight);
}
let resizedAlready = false;
@@ -0,0 +1,50 @@
import { useCallback, useEffect, useState } from "react";
import { useResizeObserver } from "coral-framework/hooks";
import resizePopup from "../dom/resizePopup";
export default function useResizePopup() {
const [polling, setPolling] = useState(true);
const [pollTimeout, setPollTimeout] = useState<NodeJS.Timer | null>(null);
const pollPopupHeight = useCallback(
(interval: number = 200) => {
if (!polling) {
return;
}
// Save the reference to the browser timeout we create.
setPollTimeout(
// Create the timeout to fire after the interval.
setTimeout(() => {
// Using requestAnimationFrame, resize the popup, and reschedule the
// resize timeout again in another interval.
window.requestAnimationFrame(() => {
resizePopup();
pollPopupHeight(interval);
});
}, interval)
);
},
[pollTimeout, setPollTimeout, polling]
);
useEffect(() => {
// Poll for popup height changes.
pollPopupHeight();
return () => {
if (pollTimeout) {
clearTimeout(pollTimeout);
setPollTimeout(null);
setPolling(false);
}
};
}, [setPollTimeout, setPolling]);
const ref = useResizeObserver(() => {
resizePopup();
});
return ref;
}
-23
View File
@@ -4,31 +4,12 @@ import ReactDOM from "react-dom";
import { createManaged } from "coral-framework/lib/bootstrap";
import App from "./App";
import resizePopup from "./dom/resizePopup";
import { initLocalState } from "./local";
import localesData from "./locales";
// Import css variables.
import "coral-ui/theme/variables.css";
/**
* Adapt popup height to current content every 200ms.
*
* The goal is to smooth out height inconsistensies e.g. when fonts
* are switched out or other resources being loaded that React has no influence
* over.
*
* This works in addition to the ResizeObserver in App.tsx
*/
function pollPopupHeight(interval: number = 200) {
setTimeout(() => {
window.requestAnimationFrame(() => {
resizePopup();
pollPopupHeight(interval);
});
}, interval);
}
async function main() {
const ManagedCoralContextProvider = await createManaged({
initLocalState,
@@ -42,10 +23,6 @@ async function main() {
);
ReactDOM.render(<Index />, document.getElementById("app"));
// Set width.
window.resizeTo(350, window.outerHeight);
// Poll height.
pollPopupHeight();
}
main();
@@ -1,13 +1,15 @@
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { Component } from "react";
import React, { FunctionComponent, useCallback } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "coral-auth/components//Header";
import ConfirmEmailField from "coral-auth/components/ConfirmEmailField";
import EmailField from "coral-auth/components/EmailField";
import Main from "coral-auth/components/Main";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { OnSubmit } from "coral-framework/lib/form";
import { useMutation } from "coral-framework/lib/relay";
import {
Button,
CallOut,
@@ -16,101 +18,98 @@ import {
Typography,
} from "coral-ui/components";
import { SetEmailMutation, withSetEmailMutation } from "./SetEmailMutation";
import SetEmailMutation from "./SetEmailMutation";
import { ListItem, UnorderedList } from "./UnorderedList";
interface FormProps {
email: string;
}
interface Props {
setEmail: SetEmailMutation;
}
const AddEmailAddressContainer: FunctionComponent = () => {
const setEmail = useMutation(SetEmailMutation);
const onSubmit: OnSubmit<FormProps> = useCallback(
async (input, form) => {
try {
await setEmail({ email: input.email });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
},
[setEmail]
);
const ref = useResizePopup();
class AddEmailAddressContainer extends Component<Props> {
private handleSubmit: OnSubmit<FormProps> = async (input, form) => {
try {
await this.props.setEmail({ email: input.email });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
return (
<div ref={ref} data-testid="addEmailAddress-container">
<Bar>
<Localized id="addEmailAddress-addEmailAddressHeader">
<Title>Add Email Address</Title>
</Localized>
</Bar>
<Main data-testid="addEmailAddress-main">
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="oneAndAHalf">
<Localized id="addEmailAddress-whatItIs">
<Typography variant="bodyCopy">
For your added security, we require users to add an email
address to their accounts. Your email address will be used
to:
</Typography>
</Localized>
<UnorderedList>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-receiveUpdates">
<Typography container="div">
Receive updates regarding any changes to your account
(email address, username, password, etc.)
</Typography>
</Localized>
</ListItem>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-allowDownload">
<Typography container="div">
Allow you to download your comments.
</Typography>
</Localized>
</ListItem>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-sendNotifications">
<Typography container="div">
Send comment notifications that you have chosen to
receive.
</Typography>
</Localized>
</ListItem>
</UnorderedList>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<EmailField disabled={submitting} />
<ConfirmEmailField disabled={submitting} />
<Localized id="addEmailAddress-addEmailAddressButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Add Email Address
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
public render() {
// tslint:disable-next-line:no-empty
return (
<div data-testid="addEmailAddress-container">
<Bar>
<Localized id="addEmailAddress-addEmailAddressHeader">
<Title>Add Email Address</Title>
</Localized>
</Bar>
<Main data-testid="addEmailAddress-main">
<Form onSubmit={this.handleSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="oneAndAHalf">
<Localized id="addEmailAddress-whatItIs">
<Typography variant="bodyCopy">
For your added security, we require users to add an email
address to their accounts. Your email address will be used
to:
</Typography>
</Localized>
<UnorderedList>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-receiveUpdates">
<Typography container="div">
Receive updates regarding any changes to your account
(email address, username, password, etc.)
</Typography>
</Localized>
</ListItem>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-allowDownload">
<Typography container="div">
Allow you to download your comments.
</Typography>
</Localized>
</ListItem>
<ListItem icon={<Icon>done</Icon>}>
<Localized id="addEmailAddress-sendNotifications">
<Typography container="div">
Send comment notifications that you have chosen to
receive.
</Typography>
</Localized>
</ListItem>
</UnorderedList>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<EmailField disabled={submitting} />
<ConfirmEmailField disabled={submitting} />
<Localized id="addEmailAddress-addEmailAddressButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Add Email Address
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
}
}
const enhanced = withSetEmailMutation(AddEmailAddressContainer);
export default enhanced;
export default AddEmailAddressContainer;
@@ -3,7 +3,7 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
createMutation,
} from "coral-framework/lib/relay";
import { Omit } from "coral-framework/types";
@@ -39,8 +39,6 @@ function commit(environment: Environment, input: SetEmailInput) {
});
}
export const withSetEmailMutation = createMutationContainer("setEmail", commit);
const SetEmailMutation = createMutation("setEmail", commit);
export type SetEmailMutation = (
input: SetEmailInput
) => Promise<MutationTypes["response"]["setEmail"]>;
export default SetEmailMutation;
@@ -1,11 +1,12 @@
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { Component } from "react";
import React, { FunctionComponent, useCallback } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "coral-auth/components//Header";
import Main from "coral-auth/components/Main";
import SetPasswordField from "coral-auth/components/SetPasswordField";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { OnSubmit } from "coral-framework/lib/form";
import {
Button,
@@ -14,75 +15,71 @@ import {
Typography,
} from "coral-ui/components";
import {
SetPasswordMutation,
withSetPasswordMutation,
} from "./SetPasswordMutation";
import { useMutation } from "coral-framework/lib/relay";
import SetPasswordMutation from "./SetPasswordMutation";
interface FormProps {
password: string;
}
interface Props {
setPassword: SetPasswordMutation;
}
const CreatePasswordContainer: FunctionComponent = () => {
const setPassword = useMutation(SetPasswordMutation);
const onSubmit: OnSubmit<FormProps> = useCallback(
async (input, form) => {
try {
await setPassword({ password: input.password });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
},
[setPassword]
);
const ref = useResizePopup();
class CreatePasswordContainer extends Component<Props> {
private handleSubmit: OnSubmit<FormProps> = async (input, form) => {
try {
await this.props.setPassword({ password: input.password });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
return (
<div ref={ref} data-testid="createPassword-container">
<Bar>
<Localized id="createPassword-createPasswordHeader">
<Title>Create Password</Title>
</Localized>
</Bar>
<Main data-testid="createPassword-main">
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="oneAndAHalf">
<Localized id="createPassword-whatItIs">
<Typography variant="bodyCopy">
To protect against unauthorized changes to your account, we
require users to create a password.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<SetPasswordField disabled={submitting} />
<Localized id="createPassword-createPasswordButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Create Password
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
public render() {
return (
<div data-testid="createPassword-container">
<Bar>
<Localized id="createPassword-createPasswordHeader">
<Title>Create Password</Title>
</Localized>
</Bar>
<Main data-testid="createPassword-main">
<Form onSubmit={this.handleSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="oneAndAHalf">
<Localized id="createPassword-whatItIs">
<Typography variant="bodyCopy">
To protect against unauthorized changes to your account,
we require users to create a password.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<SetPasswordField disabled={submitting} />
<Localized id="createPassword-createPasswordButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Create Password
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
}
}
const enhanced = withSetPasswordMutation(CreatePasswordContainer);
export default enhanced;
export default CreatePasswordContainer;
@@ -3,7 +3,7 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
createMutation,
} from "coral-framework/lib/relay";
import { Omit } from "coral-framework/types";
@@ -41,11 +41,6 @@ function commit(environment: Environment, input: SetPasswordInput) {
});
}
export const withSetPasswordMutation = createMutationContainer(
"setPassword",
commit
);
const SetPasswordMutation = createMutation("setPassword", commit);
export type SetPasswordMutation = (
input: SetPasswordInput
) => Promise<MutationTypes["response"]["setPassword"]>;
export default SetPasswordMutation;
@@ -1,12 +1,14 @@
import { FORM_ERROR } from "final-form";
import { Localized } from "fluent-react/compat";
import React, { Component } from "react";
import React, { FunctionComponent, useCallback } from "react";
import { Form } from "react-final-form";
import { Bar, Title } from "coral-auth/components//Header";
import Main from "coral-auth/components/Main";
import UsernameField from "coral-auth/components/UsernameField";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { OnSubmit } from "coral-framework/lib/form";
import { useMutation } from "coral-framework/lib/relay";
import {
Button,
CallOut,
@@ -14,76 +16,70 @@ import {
Typography,
} from "coral-ui/components";
import {
SetUsernameMutation,
withSetUsernameMutation,
} from "./SetUsernameMutation";
import SetUsernameMutation from "./SetUsernameMutation";
interface FormProps {
username: string;
}
interface Props {
setUsername: SetUsernameMutation;
}
const CreateUsernameContainer: FunctionComponent = () => {
const setUsername = useMutation(SetUsernameMutation);
const onSubmit: OnSubmit<FormProps> = useCallback(
async (input, form) => {
try {
await setUsername({ username: input.username });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
},
[setUsername]
);
const ref = useResizePopup();
class CreateUsernameContainer extends Component<Props> {
private handleSubmit: OnSubmit<FormProps> = async (input, form) => {
try {
await this.props.setUsername({ username: input.username });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
return (
<div ref={ref} data-testid="createUsername-container">
<Bar>
<Localized id="createUsername-createUsernameHeader">
<Title>Create Username</Title>
</Localized>
</Bar>
<Main data-testid="createUsername-main">
<Form onSubmit={onSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="oneAndAHalf">
<Localized id="createUsername-whatItIs">
<Typography variant="bodyCopy">
Your username is an identifier that will appear on all of
your comments.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<UsernameField disabled={submitting} />
<Localized id="createUsername-createUsernameButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Create Username
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
};
public render() {
// tslint:disable-next-line:no-empty
return (
<div data-testid="createUsername-container">
<Bar>
<Localized id="createUsername-createUsernameHeader">
<Title>Create Username</Title>
</Localized>
</Bar>
<Main data-testid="createUsername-main">
<Form onSubmit={this.handleSubmit}>
{({ handleSubmit, submitting, submitError }) => (
<form autoComplete="off" onSubmit={handleSubmit}>
<HorizontalGutter size="oneAndAHalf">
<Localized id="createUsername-whatItIs">
<Typography variant="bodyCopy">
Your username is an identifier that will appear on all of
your comments.
</Typography>
</Localized>
{submitError && (
<CallOut color="error" fullWidth>
{submitError}
</CallOut>
)}
<UsernameField disabled={submitting} />
<Localized id="createUsername-createUsernameButton">
<Button
variant="filled"
color="primary"
size="large"
type="submit"
fullWidth
disabled={submitting}
>
Create Username
</Button>
</Localized>
</HorizontalGutter>
</form>
)}
</Form>
</Main>
</div>
);
}
}
const enhanced = withSetUsernameMutation(CreateUsernameContainer);
export default enhanced;
export default CreateUsernameContainer;
@@ -3,7 +3,7 @@ import { Environment } from "relay-runtime";
import {
commitMutationPromiseNormalized,
createMutationContainer,
createMutation,
} from "coral-framework/lib/relay";
import { Omit } from "coral-framework/types";
@@ -39,11 +39,6 @@ function commit(environment: Environment, input: SetUsernameInput) {
});
}
export const withSetUsernameMutation = createMutationContainer(
"setUsername",
commit
);
const SetUsernameMutation = createMutation("setUsername", commit);
export type SetUsernameMutation = (
input: SetUsernameInput
) => Promise<MutationTypes["response"]["setUsername"]>;
export default SetUsernameMutation;
@@ -3,6 +3,7 @@ import React, { FunctionComponent, useCallback } from "react";
import { Bar, Title } from "coral-auth/components/Header";
import Main from "coral-auth/components/Main";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { Button, HorizontalGutter, Typography } from "coral-ui/components";
interface Props {
@@ -10,12 +11,13 @@ interface Props {
}
const CheckEmail: FunctionComponent<Props> = ({ email }) => {
const ref = useResizePopup();
const closeWindow = useCallback(() => {
window.close();
}, []);
const UserEmail = () => <strong>{email}</strong>;
return (
<div data-testid="forgotPassword-checkEmail-container">
<div ref={ref} data-testid="forgotPassword-checkEmail-container">
<Bar>
<Localized id="forgotPassword-checkEmail-checkEmailHeader">
<Title>Check Your Email</Title>
@@ -6,6 +6,7 @@ import { Field, Form } from "react-final-form";
import { Bar, SubBar, Title } from "coral-auth/components/Header";
import Main from "coral-auth/components/Main";
import { getViewURL } from "coral-auth/helpers";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { SetViewMutation } from "coral-auth/mutations";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { colorFromMeta, ValidationMessage } from "coral-framework/lib/form";
@@ -42,6 +43,7 @@ const ForgotPasswordForm: FunctionComponent<Props> = ({
email,
onCheckEmail,
}) => {
const ref = useResizePopup();
const signInHref = getViewURL("SIGN_IN");
const forgotPassword = useMutation(ForgotPasswordMutation);
const setView = useMutation(SetViewMutation);
@@ -71,7 +73,7 @@ const ForgotPasswordForm: FunctionComponent<Props> = ({
);
return (
<div data-testid="forgotPassword-container">
<div ref={ref} data-testid="forgotPassword-container">
<Bar>
<Localized id="forgotPassword-forgotPasswordHeader">
<Title>Forgot Password?</Title>
+3 -1
View File
@@ -4,6 +4,7 @@ import React, { FunctionComponent } from "react";
import { Bar, SubBar, Subtitle, Title } from "coral-auth/components/Header";
import Main from "coral-auth/components/Main";
import OrSeparator from "coral-auth/components/OrSeparator";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { PropTypesOf } from "coral-framework/types";
import {
CallOut,
@@ -41,10 +42,11 @@ const SignIn: FunctionComponent<SignInForm> = ({
auth,
error,
}) => {
const ref = useResizePopup();
const oneClickIntegrationEnabled =
facebookEnabled || googleEnabled || oidcEnabled;
return (
<div data-testid="signIn-container">
<div ref={ref} data-testid="signIn-container">
<Localized
id="signIn-signInToJoinHeader"
title={<Title />}
@@ -1,15 +1,15 @@
import React, { Component } from "react";
import React, { FunctionComponent, useCallback, useEffect } from "react";
import { SignInContainer_auth as AuthData } from "coral-auth/__generated__/SignInContainer_auth.graphql";
import { SignInContainerLocal as LocalData } from "coral-auth/__generated__/SignInContainerLocal.graphql";
import { getViewURL } from "coral-auth/helpers";
import { SetViewMutation } from "coral-auth/mutations";
import { redirectOAuth2 } from "coral-framework/helpers";
import {
graphql,
MutationProp,
useMutation,
withFragmentContainer,
withLocalStateContainer,
withMutation,
} from "coral-framework/lib/relay";
import {
@@ -17,101 +17,129 @@ import {
withClearErrorMutation,
} from "./ClearErrorMutation";
import SignIn from "./SignIn";
import { SignInMutation, withSignInMutation } from "./SignInMutation";
interface Props {
local: LocalData;
auth: AuthData;
signIn: SignInMutation;
setView: MutationProp<typeof SetViewMutation>;
clearError: ClearErrorMutation;
}
class SignInContainer extends Component<Props> {
private goToSignUp = (e: React.MouseEvent) => {
this.props.setView({ view: "SIGN_UP", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
};
const SignInContainer: FunctionComponent<Props> = ({
auth,
local,
clearError,
}) => {
const setView = useMutation(SetViewMutation);
const goToSignUp = useCallback(
(e: React.MouseEvent) => {
setView({ view: "SIGN_UP", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
},
[setView]
);
public componentWillUnmount() {
this.props.clearError();
}
useEffect(() => {
return () => {
// Clear the error when we unmount.
clearError();
};
}, [clearError, auth]);
public render() {
const integrations = this.props.auth.integrations;
return (
<SignIn
error={this.props.local.error}
auth={this.props.auth}
onGotoSignUp={this.goToSignUp}
emailEnabled={
integrations.local.enabled && integrations.local.targetFilter.stream
}
facebookEnabled={
integrations.facebook.enabled &&
integrations.facebook.targetFilter.stream
}
googleEnabled={
integrations.google.enabled && integrations.google.targetFilter.stream
}
oidcEnabled={
integrations.oidc.enabled && integrations.oidc.targetFilter.stream
}
signUpHref={getViewURL("SIGN_UP")}
/>
// If there's only one enabled auth integration, we should just perform
// the redirect now.
if (
!auth.integrations.local.enabled ||
!auth.integrations.local.targetFilter.stream
) {
// Local isn't enabled, so we can look into the rest of the integrations
// now.
const { facebook, google, oidc } = auth.integrations;
const enabledIntegrations = [facebook, google, oidc].filter(
({ enabled, targetFilter: { stream } }) => enabled && stream
);
}
}
if (
enabledIntegrations.length === 1 &&
enabledIntegrations[0].redirectURL
) {
redirectOAuth2(enabledIntegrations[0].redirectURL);
const enhanced = withMutation(SetViewMutation)(
withClearErrorMutation(
withSignInMutation(
withLocalStateContainer(
graphql`
fragment SignInContainerLocal on Local {
error
}
`
)(
withFragmentContainer<Props>({
auth: graphql`
fragment SignInContainer_auth on Auth {
...SignInWithOIDCContainer_auth
...SignInWithGoogleContainer_auth
...SignInWithFacebookContainer_auth
integrations {
local {
enabled
targetFilter {
stream
}
}
facebook {
enabled
targetFilter {
stream
}
}
google {
enabled
targetFilter {
stream
}
}
oidc {
enabled
targetFilter {
stream
}
}
return null;
}
}
const integrations = auth.integrations;
return (
<SignIn
error={local.error}
auth={auth}
onGotoSignUp={goToSignUp}
emailEnabled={
integrations.local.enabled && integrations.local.targetFilter.stream
}
facebookEnabled={
integrations.facebook.enabled &&
integrations.facebook.targetFilter.stream
}
googleEnabled={
integrations.google.enabled && integrations.google.targetFilter.stream
}
oidcEnabled={
integrations.oidc.enabled && integrations.oidc.targetFilter.stream
}
signUpHref={getViewURL("SIGN_UP")}
/>
);
};
const enhanced = withClearErrorMutation(
withLocalStateContainer(
graphql`
fragment SignInContainerLocal on Local {
error
}
`
)(
withFragmentContainer<Props>({
auth: graphql`
fragment SignInContainer_auth on Auth {
...SignInWithOIDCContainer_auth
...SignInWithGoogleContainer_auth
...SignInWithFacebookContainer_auth
integrations {
local {
enabled
targetFilter {
stream
}
}
`,
})(SignInContainer)
)
)
facebook {
enabled
redirectURL
targetFilter {
stream
}
}
google {
enabled
redirectURL
targetFilter {
stream
}
}
oidc {
enabled
redirectURL
targetFilter {
stream
}
}
}
}
`,
})(SignInContainer)
)
);
export default enhanced;
@@ -2,7 +2,7 @@ import { pick } from "lodash";
import { Environment } from "relay-runtime";
import { CoralContext } from "coral-framework/lib/bootstrap";
import { createMutationContainer } from "coral-framework/lib/relay";
import { createMutation } from "coral-framework/lib/relay";
import { signIn, SignInInput } from "coral-framework/rest";
export type SignInMutation = (input: SignInInput) => Promise<void>;
@@ -16,4 +16,6 @@ export async function commit(
await clearSession(result.token);
}
export const withSignInMutation = createMutationContainer("signIn", commit);
const SignInMutation = createMutation("signIn", commit);
export default SignInMutation;
@@ -1,46 +1,45 @@
import { FORM_ERROR } from "final-form";
import React, { Component } from "react";
import React, { FunctionComponent, useCallback } from "react";
import { SetViewMutation } from "coral-auth/mutations";
import { MutationProp, withMutation } from "coral-framework/lib/relay";
import { useMutation } from "coral-framework/lib/relay";
import { getViewURL } from "coral-auth/helpers";
import { SignInMutation, withSignInMutation } from "./SignInMutation";
import SignInMutation from "./SignInMutation";
import SignInWithEmail, { SignInWithEmailForm } from "./SignInWithEmail";
interface SignInContainerProps {
signIn: SignInMutation;
setView: MutationProp<typeof SetViewMutation>;
}
const SignInContainer: FunctionComponent = () => {
const signIn = useMutation(SignInMutation);
const setView = useMutation(SetViewMutation);
const onSubmit: SignInWithEmailForm["onSubmit"] = useCallback(
async (input, form) => {
try {
await signIn({ email: input.email, password: input.password });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
},
[signIn]
);
const goToForgotPassword = useCallback(
(e: React.MouseEvent) => {
setView({ view: "FORGOT_PASSWORD", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
},
[setView]
);
class SignInContainer extends Component<SignInContainerProps> {
private onSubmit: SignInWithEmailForm["onSubmit"] = async (input, form) => {
try {
await this.props.signIn({ email: input.email, password: input.password });
return form.reset();
} catch (error) {
return { [FORM_ERROR]: error.message };
}
};
private goToForgotPassword = (e: React.MouseEvent) => {
this.props.setView({ view: "FORGOT_PASSWORD", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
};
public render() {
return (
<SignInWithEmail
onSubmit={this.onSubmit}
onGotoForgotPassword={this.goToForgotPassword}
forgotPasswordHref={getViewURL("FORGOT_PASSWORD")}
/>
);
}
}
return (
<SignInWithEmail
onSubmit={onSubmit}
onGotoForgotPassword={goToForgotPassword}
forgotPasswordHref={getViewURL("FORGOT_PASSWORD")}
/>
);
};
const enhanced = withMutation(SetViewMutation)(
withSignInMutation(SignInContainer)
);
export default enhanced;
export default SignInContainer;
@@ -42,7 +42,7 @@ exports[`renders correctly 1`] = `
<ForwardRef(forwardRef)
size="oneAndAHalf"
>
<withContext(withMutation(withContext(createMutationContainer(SignInContainer)))) />
<SignInContainer />
<OrSeparator />
<ForwardRef(forwardRef)>
<Relay(SignInWithFacebookContainer)
@@ -108,7 +108,7 @@ exports[`renders error 1`] = `
>
Server Error
</withPropsOnChange(CallOut)>
<withContext(withMutation(withContext(createMutationContainer(SignInContainer)))) />
<SignInContainer />
<OrSeparator />
<ForwardRef(forwardRef)>
<Relay(SignInWithFacebookContainer)
+3 -1
View File
@@ -4,6 +4,7 @@ import React, { FunctionComponent } from "react";
import { Bar, SubBar, Subtitle, Title } from "coral-auth/components//Header";
import Main from "coral-auth/components/Main";
import OrSeparator from "coral-auth/components/OrSeparator";
import useResizePopup from "coral-auth/hooks/useResizePopup";
import { PropTypesOf } from "coral-framework/types";
import {
Flex,
@@ -38,10 +39,11 @@ const SignUp: FunctionComponent<Props> = ({
signInHref,
auth,
}) => {
const ref = useResizePopup();
const oneClickUptegrationEnabled =
facebookEnabled || googleEnabled || oidcEnabled;
return (
<div data-testid="signUp-container">
<div ref={ref} data-testid="signUp-container">
<Localized
id="signUp-signUpToJoinHeader"
title={<Title />}
@@ -1,100 +1,100 @@
import React, { Component } from "react";
import React, { FunctionComponent, useCallback } from "react";
import { SignUpContainer_auth as AuthData } from "coral-auth/__generated__/SignUpContainer_auth.graphql";
import { getViewURL } from "coral-auth/helpers";
import { SetViewMutation } from "coral-auth/mutations";
import {
graphql,
MutationProp,
useMutation,
withFragmentContainer,
withMutation,
} from "coral-framework/lib/relay";
import { getViewURL } from "coral-auth/helpers";
import SignUp from "./SignUp";
interface Props {
auth: AuthData;
setView: MutationProp<typeof SetViewMutation>;
}
class SignUpContainer extends Component<Props> {
private goToSignIn = (e: React.MouseEvent) => {
this.props.setView({ view: "SIGN_IN", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
};
public render() {
const integrations = this.props.auth.integrations;
return (
<SignUp
signInHref={getViewURL("SIGN_IN")}
auth={this.props.auth}
onGotoSignIn={this.goToSignIn}
emailEnabled={
integrations.local.enabled &&
integrations.local.targetFilter.stream &&
integrations.local.allowRegistration
}
facebookEnabled={
integrations.facebook.enabled &&
integrations.facebook.targetFilter.stream &&
integrations.facebook.allowRegistration
}
googleEnabled={
integrations.google.enabled &&
integrations.google.targetFilter.stream &&
integrations.google.allowRegistration
}
oidcEnabled={
integrations.oidc.enabled &&
integrations.oidc.targetFilter.stream &&
integrations.oidc.allowRegistration
}
/>
);
}
}
const SignUpContainer: FunctionComponent<Props> = ({ auth }) => {
const setView = useMutation(SetViewMutation);
const goToSignIn = useCallback(
(e: React.MouseEvent) => {
setView({ view: "SIGN_IN", history: "push" });
if (e.preventDefault) {
e.preventDefault();
}
},
[setView]
);
const enhanced = withMutation(SetViewMutation)(
withFragmentContainer<Props>({
auth: graphql`
fragment SignUpContainer_auth on Auth {
...SignUpWithOIDCContainer_auth
...SignUpWithGoogleContainer_auth
...SignUpWithFacebookContainer_auth
integrations {
local {
enabled
targetFilter {
stream
}
allowRegistration
const integrations = auth.integrations;
return (
<SignUp
signInHref={getViewURL("SIGN_IN")}
auth={auth}
onGotoSignIn={goToSignIn}
emailEnabled={
integrations.local.enabled &&
integrations.local.targetFilter.stream &&
integrations.local.allowRegistration
}
facebookEnabled={
integrations.facebook.enabled &&
integrations.facebook.targetFilter.stream &&
integrations.facebook.allowRegistration
}
googleEnabled={
integrations.google.enabled &&
integrations.google.targetFilter.stream &&
integrations.google.allowRegistration
}
oidcEnabled={
integrations.oidc.enabled &&
integrations.oidc.targetFilter.stream &&
integrations.oidc.allowRegistration
}
/>
);
};
const enhanced = withFragmentContainer<Props>({
auth: graphql`
fragment SignUpContainer_auth on Auth {
...SignUpWithOIDCContainer_auth
...SignUpWithGoogleContainer_auth
...SignUpWithFacebookContainer_auth
integrations {
local {
enabled
targetFilter {
stream
}
facebook {
enabled
targetFilter {
stream
}
allowRegistration
allowRegistration
}
facebook {
enabled
targetFilter {
stream
}
google {
enabled
targetFilter {
stream
}
allowRegistration
allowRegistration
}
google {
enabled
targetFilter {
stream
}
oidc {
enabled
targetFilter {
stream
}
allowRegistration
allowRegistration
}
oidc {
enabled
targetFilter {
stream
}
allowRegistration
}
}
`,
})(SignUpContainer)
);
}
`,
})(SignUpContainer);
export default enhanced;
@@ -28,7 +28,6 @@ it("renders fully", () => {
facebook: {
enabled: true,
allowRegistration: true,
redirectURL: "http://localhost/facebook",
targetFilter: {
stream: true,
},
@@ -36,7 +35,6 @@ it("renders fully", () => {
google: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/google",
targetFilter: {
stream: true,
},
@@ -44,7 +42,6 @@ it("renders fully", () => {
oidc: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/oidc",
targetFilter: {
stream: true,
},
@@ -90,7 +87,6 @@ it("renders without logout button", () => {
facebook: {
enabled: true,
allowRegistration: true,
redirectURL: "http://localhost/facebook",
targetFilter: {
stream: true,
},
@@ -98,7 +94,6 @@ it("renders without logout button", () => {
google: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/google",
targetFilter: {
stream: true,
},
@@ -106,7 +101,6 @@ it("renders without logout button", () => {
oidc: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/oidc",
targetFilter: {
stream: true,
},
@@ -152,7 +146,6 @@ it("renders sso only", () => {
facebook: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/facebook",
targetFilter: {
stream: true,
},
@@ -160,7 +153,6 @@ it("renders sso only", () => {
google: {
enabled: true,
allowRegistration: true,
redirectURL: "http://localhost/google",
targetFilter: {
stream: false,
},
@@ -168,7 +160,6 @@ it("renders sso only", () => {
oidc: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/oidc",
targetFilter: {
stream: true,
},
@@ -214,7 +205,6 @@ it("renders sso only without logout button", () => {
facebook: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/facebook",
targetFilter: {
stream: true,
},
@@ -222,7 +212,6 @@ it("renders sso only without logout button", () => {
google: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/google",
targetFilter: {
stream: true,
},
@@ -230,7 +219,6 @@ it("renders sso only without logout button", () => {
oidc: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/oidc",
targetFilter: {
stream: true,
},
@@ -276,7 +264,6 @@ it("renders without register button", () => {
facebook: {
enabled: true,
allowRegistration: false,
redirectURL: "http://localhost/facebook",
targetFilter: {
stream: true,
},
@@ -284,7 +271,6 @@ it("renders without register button", () => {
google: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/google",
targetFilter: {
stream: false,
},
@@ -292,7 +278,6 @@ it("renders without register button", () => {
oidc: {
enabled: false,
allowRegistration: true,
redirectURL: "http://localhost/oidc",
targetFilter: {
stream: true,
},
@@ -71,39 +71,10 @@ export class UserBoxContainer extends Component<Props> {
].some(i => i.enabled && i.targetFilter.stream);
}
private get authUrl(): string {
const {
facebook,
google,
local,
oidc,
} = this.props.settings.auth.integrations;
const defaultAuthUrl = `${urls.embed.auth}?view=${
this.props.local.authPopup.view
}`;
if (local.enabled && local.targetFilter.stream) {
return defaultAuthUrl;
}
// For each of these integrations, if only one is enabled for the stream,
// then return the redirectURL for that one only.
const integrations = [facebook, google, oidc];
const enabled = integrations.filter(
integration => integration.enabled && integration.targetFilter.stream
);
if (enabled.length === 1 && enabled[0].redirectURL) {
return enabled[0].redirectURL;
}
return defaultAuthUrl;
}
public render() {
const {
local: {
authPopup: { open, focus },
authPopup: { open, focus, view },
},
viewer,
} = this.props;
@@ -125,13 +96,14 @@ export class UserBoxContainer extends Component<Props> {
return (
<>
<Popup
href={this.authUrl}
href={`${urls.embed.auth}?view=${view}`}
title="Coral Auth"
open={open}
focus={focus}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onClose={this.handleClose}
features={{ width: 350, innerWidth: 350 }}
/>
<UserBoxUnauthenticated
onSignIn={this.handleSignIn}
@@ -182,7 +154,6 @@ const enhanced = withSignOutMutation(
oidc {
enabled
allowRegistration
redirectURL
targetFilter {
stream
}
@@ -190,7 +161,6 @@ const enhanced = withSignOutMutation(
google {
enabled
allowRegistration
redirectURL
targetFilter {
stream
}
@@ -198,7 +168,6 @@ const enhanced = withSignOutMutation(
facebook {
enabled
allowRegistration
redirectURL
targetFilter {
stream
}
@@ -3,6 +3,12 @@
exports[`renders fully 1`] = `
<React.Fragment>
<Popup
features={
Object {
"innerWidth": 350,
"width": 350,
}
}
focus={false}
href="/embed/auth?view=SIGN_IN"
onBlur={[Function]}
@@ -26,6 +32,12 @@ exports[`renders sso only without logout button 1`] = `null`;
exports[`renders without logout button 1`] = `
<React.Fragment>
<Popup
features={
Object {
"innerWidth": 350,
"width": 350,
}
}
focus={false}
href="/embed/auth?view=SIGN_IN"
onBlur={[Function]}
@@ -45,6 +57,12 @@ exports[`renders without logout button 1`] = `
exports[`renders without register button 1`] = `
<React.Fragment>
<Popup
features={
Object {
"innerWidth": 350,
"width": 350,
}
}
focus={false}
href="/embed/auth?view=SIGN_IN"
onBlur={[Function]}
@@ -6,6 +6,7 @@ interface WindowFeatures {
width: number;
height: number;
centered: boolean;
innerWidth?: number;
}
interface PopupProps {
@@ -22,6 +22,7 @@ import { AsymmetricSigningAlgorithm } from "coral-server/services/jwt";
import TenantCache from "coral-server/services/tenant/cache";
import { TenantCacheAdapter } from "coral-server/services/tenant/cache/adapter";
import { insert } from "coral-server/services/users";
import { validateUsername } from "coral-server/services/users/helpers";
import { Request } from "coral-server/types/express";
export interface Params {
@@ -170,7 +171,14 @@ export async function findOrCreateOIDCUser(
// FIXME: implement rules.
// Try to extract the username from the following chain:
const username = preferred_username || nickname || name;
let username = preferred_username || nickname || name;
if (username) {
try {
validateUsername(username);
} catch (err) {
username = undefined;
}
}
// Create the new user, as one didn't exist before!
user = await insert(
@@ -422,7 +422,6 @@ type LocalAuthIntegration {
integration should be displayed in all targets.
"""
targetFilter: AuthenticationTargetFilter!
}
##########################
@@ -443,13 +442,6 @@ type SSOAuthIntegration {
"""
allowRegistration: Boolean!
"""
redirectURL is the URL that the user should be redirected to in order to start
an authentication flow with the given integration. This field is not stored,
and is instead computed from the Tenant.
"""
redirectURL: String
"""
targetFilter will restrict where the authentication integration should be
displayed. If the value of targetFilter is null, then the authentication