mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Implemented desktop application type
This commit is contained in:
@@ -3,7 +3,7 @@ import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import ApplicationsIndex from './ApplicationsIndex';
|
||||
import { TYPE_APPLICATION } from 'app/components/dev/apps';
|
||||
import { TYPE_WEB_APPLICATION } from 'app/components/dev/apps';
|
||||
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import rootStyles from 'app/pages/root/root.scss';
|
||||
@@ -18,8 +18,10 @@ export const DevLayout: ComponentType = ({ children }) => (
|
||||
export const sampleApp: OauthAppResponse = {
|
||||
clientId: 'my-application',
|
||||
clientSecret: 'cL1eNtS3cRE7xNJqfWQdqrMRKURfW1ssP4kiX6JDW0_szM-n-q',
|
||||
type: TYPE_APPLICATION,
|
||||
type: TYPE_WEB_APPLICATION,
|
||||
name: 'My Application',
|
||||
description: '',
|
||||
redirectUri: '',
|
||||
websiteUrl: '',
|
||||
createdAt: 0,
|
||||
countUsers: 0,
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import React, { FC, ReactElement } from 'react';
|
||||
import { defineMessages, FormattedMessage as Message } from 'react-intl';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { LinkButton } from 'app/components/ui/form';
|
||||
import { COLOR_GREEN, COLOR_BLUE } from 'app/components/ui';
|
||||
import { ContactLink } from 'app/components/contact';
|
||||
@@ -27,77 +28,83 @@ type Props = {
|
||||
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
|
||||
};
|
||||
|
||||
export default class ApplicationsIndex extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.welcomeContainer}>
|
||||
<Message key="accountsForDevelopers" defaultMessage="Ely.by Accounts for developers">
|
||||
{(pageTitle: string) => (
|
||||
<h2 className={styles.welcomeTitle}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h2>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.welcomeTitleDelimiter} />
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
key="accountsAllowsYouYoUseOauth2"
|
||||
defaultMessage="Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}."
|
||||
values={{
|
||||
ourDocumentation: (
|
||||
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
|
||||
<Message key="ourDocumentation" defaultMessage="our documentation" />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
key="ifYouHaveAnyTroubles"
|
||||
defaultMessage="If you are experiencing difficulties, you can always use {feedback}. We'll surely help you."
|
||||
values={{
|
||||
feedback: (
|
||||
<ContactLink>
|
||||
<Message key="feedback" defaultMessage="feedback" />
|
||||
</ContactLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
const ApplicationsIndex: FC<Props> = ({
|
||||
displayForGuest,
|
||||
applications,
|
||||
isLoading,
|
||||
resetApp,
|
||||
deleteApp,
|
||||
clientId,
|
||||
resetClientId,
|
||||
}) => {
|
||||
let content: ReactElement;
|
||||
|
||||
{this.getContent()}
|
||||
</div>
|
||||
if (applications.length > 0) {
|
||||
content = (
|
||||
<ApplicationsList
|
||||
applications={applications}
|
||||
resetApp={resetApp}
|
||||
deleteApp={deleteApp}
|
||||
clientId={clientId}
|
||||
resetClientId={resetClientId}
|
||||
/>
|
||||
);
|
||||
} else if (displayForGuest) {
|
||||
content = <Guest />;
|
||||
} else {
|
||||
content = <Loader noApps={!isLoading} />;
|
||||
}
|
||||
|
||||
getContent() {
|
||||
const { displayForGuest, applications, isLoading, resetApp, deleteApp, clientId, resetClientId } = this.props;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.welcomeContainer}>
|
||||
<Message key="accountsForDevelopers" defaultMessage="Ely.by Accounts for developers">
|
||||
{(pageTitle: string) => (
|
||||
<h2 className={styles.welcomeTitle}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h2>
|
||||
)}
|
||||
</Message>
|
||||
<div className={styles.welcomeTitleDelimiter} />
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
key="accountsAllowsYouYoUseOauth2"
|
||||
defaultMessage="Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}."
|
||||
values={{
|
||||
ourDocumentation: (
|
||||
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
|
||||
<Message key="ourDocumentation" defaultMessage="our documentation" />
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.welcomeParagraph}>
|
||||
<Message
|
||||
key="ifYouHaveAnyTroubles"
|
||||
defaultMessage="If you are experiencing difficulties, you can always use {feedback}. We'll surely help you."
|
||||
values={{
|
||||
feedback: (
|
||||
<ContactLink>
|
||||
<Message key="feedback" defaultMessage="feedback" />
|
||||
</ContactLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
if (applications.length > 0) {
|
||||
return (
|
||||
<ApplicationsList
|
||||
applications={applications}
|
||||
resetApp={resetApp}
|
||||
deleteApp={deleteApp}
|
||||
clientId={clientId}
|
||||
resetClientId={resetClientId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (displayForGuest) {
|
||||
return <Guest />;
|
||||
}
|
||||
|
||||
return <Loader noApps={!isLoading} />;
|
||||
}
|
||||
interface LoaderProps {
|
||||
noApps: boolean;
|
||||
}
|
||||
|
||||
function Loader({ noApps }: { noApps: boolean }) {
|
||||
const Loader: FC<LoaderProps> = ({ noApps }) => {
|
||||
return (
|
||||
<div className={styles.emptyState} data-e2e={noApps ? 'noApps' : 'loading'}>
|
||||
<img src={noApps ? cubeIcon : loadingCubeIcon} className={styles.emptyStateIcon} />
|
||||
@@ -130,9 +137,9 @@ function Loader({ noApps }: { noApps: boolean }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function Guest() {
|
||||
const Guest: FC = () => {
|
||||
return (
|
||||
<div className={styles.emptyState}>
|
||||
<img src={toolsIcon} className={styles.emptyStateIcon} />
|
||||
@@ -150,4 +157,6 @@ function Guest() {
|
||||
</LinkButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ApplicationsIndex;
|
||||
|
@@ -4,14 +4,19 @@ import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import { FormModel } from 'app/components/ui/form';
|
||||
import { ApplicationType, TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'app/components/dev/apps';
|
||||
import {
|
||||
ApplicationType,
|
||||
TYPE_WEB_APPLICATION,
|
||||
TYPE_DESKTOP_APPLICATION,
|
||||
TYPE_MINECRAFT_SERVER,
|
||||
} from 'app/components/dev/apps';
|
||||
import ApplicationForm from './ApplicationForm';
|
||||
import { DevLayout } from '../ApplicationsIndex.story';
|
||||
|
||||
const blankApp: OauthAppResponse = {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
type: TYPE_APPLICATION,
|
||||
type: TYPE_WEB_APPLICATION,
|
||||
name: '',
|
||||
websiteUrl: '',
|
||||
createdAt: 0,
|
||||
@@ -47,7 +52,17 @@ storiesOf('Components/Dev/Apps/ApplicationForm', module)
|
||||
form={new FormModel()}
|
||||
onSubmit={onSubmit}
|
||||
displayTypeSwitcher
|
||||
type={TYPE_APPLICATION}
|
||||
type={TYPE_WEB_APPLICATION}
|
||||
setType={action('setType')}
|
||||
app={blankApp}
|
||||
/>
|
||||
))
|
||||
.add('Create desktop application', () => (
|
||||
<ApplicationForm
|
||||
form={new FormModel()}
|
||||
onSubmit={onSubmit}
|
||||
displayTypeSwitcher
|
||||
type={TYPE_DESKTOP_APPLICATION}
|
||||
setType={action('setType')}
|
||||
app={blankApp}
|
||||
/>
|
||||
@@ -69,7 +84,7 @@ storiesOf('Components/Dev/Apps/ApplicationForm', module)
|
||||
<ApplicationForm
|
||||
form={new FormModel()}
|
||||
onSubmit={onSubmit}
|
||||
type={TYPE_APPLICATION}
|
||||
type={TYPE_WEB_APPLICATION}
|
||||
app={{
|
||||
...blankApp,
|
||||
clientId: 'already-registered',
|
||||
|
@@ -1,22 +1,28 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage as Message, defineMessages } from 'react-intl';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { MessageDescriptor, FormattedMessage as Message, defineMessages } from 'react-intl';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { MessageDescriptor } from 'react-intl';
|
||||
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import { ApplicationType } from 'app/components/dev/apps';
|
||||
import {
|
||||
ApplicationType,
|
||||
TYPE_WEB_APPLICATION,
|
||||
TYPE_DESKTOP_APPLICATION,
|
||||
TYPE_MINECRAFT_SERVER,
|
||||
} from 'app/components/dev/apps';
|
||||
import { Form, FormModel, Button } from 'app/components/ui/form';
|
||||
import { BackButton } from 'app/components/profile/ProfileForm';
|
||||
import { COLOR_GREEN } from 'app/components/ui';
|
||||
import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'app/components/dev/apps';
|
||||
import styles from 'app/components/profile/profileForm.scss';
|
||||
import logger from 'app/services/logger';
|
||||
|
||||
import ApplicationTypeSwitcher from './ApplicationTypeSwitcher';
|
||||
import WebsiteType from './WebsiteType';
|
||||
import DesktopApplicationType from './DesktopApplicationType';
|
||||
import MinecraftServerType from './MinecraftServerType';
|
||||
|
||||
const messages = defineMessages({
|
||||
website: 'Web site',
|
||||
desktopApplication: 'Desktop application',
|
||||
minecraftServer: 'Minecraft server',
|
||||
|
||||
creatingApplication: 'Creating an application',
|
||||
@@ -32,10 +38,14 @@ type TypeToForm = Record<
|
||||
>;
|
||||
|
||||
const typeToForm: TypeToForm = {
|
||||
[TYPE_APPLICATION]: {
|
||||
[TYPE_WEB_APPLICATION]: {
|
||||
label: messages.website,
|
||||
component: WebsiteType,
|
||||
},
|
||||
[TYPE_DESKTOP_APPLICATION]: {
|
||||
label: messages.desktopApplication,
|
||||
component: DesktopApplicationType,
|
||||
},
|
||||
[TYPE_MINECRAFT_SERVER]: {
|
||||
label: messages.minecraftServer,
|
||||
component: MinecraftServerType,
|
||||
@@ -53,83 +63,22 @@ const typeToLabel: TypeToLabel = (Object.keys(typeToForm) as unknown as Array<Ap
|
||||
{} as TypeToLabel,
|
||||
);
|
||||
|
||||
export default class ApplicationForm extends React.Component<{
|
||||
interface Props {
|
||||
app: OauthAppResponse;
|
||||
form: FormModel;
|
||||
displayTypeSwitcher?: boolean;
|
||||
type: ApplicationType | null;
|
||||
setType: (type: ApplicationType) => void;
|
||||
onSubmit: (form: FormModel) => Promise<void>;
|
||||
}> {
|
||||
static defaultProps = {
|
||||
setType: () => {},
|
||||
};
|
||||
setType?: (type: ApplicationType) => void;
|
||||
onSubmit?: (form: FormModel) => Promise<void>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { type, setType, form, displayTypeSwitcher, app } = this.props;
|
||||
const { component: FormComponent } = (type && typeToForm[type]) || {};
|
||||
const isUpdate = app.clientId !== '';
|
||||
|
||||
return (
|
||||
<Form form={form} onSubmit={this.onFormSubmit}>
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<BackButton to="/dev/applications" />
|
||||
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...(isUpdate ? messages.updatingApplication : messages.creatingApplication)}>
|
||||
{(pageTitle: string) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
|
||||
{displayTypeSwitcher && (
|
||||
<div className={styles.formRow}>
|
||||
<ApplicationTypeSwitcher
|
||||
selectedType={type}
|
||||
setType={setType}
|
||||
appTypes={typeToLabel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{FormComponent ? (
|
||||
<FormComponent form={form} app={app} />
|
||||
) : (
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message
|
||||
key="toDisplayRegistrationFormChooseType"
|
||||
defaultMessage="To display registration form for a new application choose necessary type."
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!FormComponent && (
|
||||
<Button color={COLOR_GREEN} block type="submit">
|
||||
{isUpdate ? (
|
||||
<Message key="updateApplication" defaultMessage="Update application" />
|
||||
) : (
|
||||
<Message key="createApplication" defaultMessage="Create application" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
onFormSubmit = async () => {
|
||||
const { form } = this.props;
|
||||
const ApplicationForm: FC<Props> = ({ app, form, displayTypeSwitcher, type, setType, onSubmit }) => {
|
||||
const isUpdate = app.clientId !== '';
|
||||
const { component: FormComponent } = (type && typeToForm[type]) || {};
|
||||
|
||||
const onFormSubmit = useCallback(async () => {
|
||||
try {
|
||||
await this.props.onSubmit(form);
|
||||
await onSubmit?.(form);
|
||||
} catch (resp) {
|
||||
if (resp.errors) {
|
||||
form.setErrors(resp.errors);
|
||||
@@ -139,5 +88,57 @@ export default class ApplicationForm extends React.Component<{
|
||||
|
||||
logger.unexpected(new Error('Error submitting application form'), resp);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [form, onSubmit]);
|
||||
|
||||
return (
|
||||
<Form form={form} onSubmit={onFormSubmit}>
|
||||
<div className={styles.contentWithBackButton}>
|
||||
<BackButton to="/dev/applications" />
|
||||
|
||||
<div className={styles.form}>
|
||||
<div className={styles.formBody}>
|
||||
<Message {...(isUpdate ? messages.updatingApplication : messages.creatingApplication)}>
|
||||
{(pageTitle: string) => (
|
||||
<h3 className={styles.title}>
|
||||
<Helmet title={pageTitle} />
|
||||
{pageTitle}
|
||||
</h3>
|
||||
)}
|
||||
</Message>
|
||||
|
||||
{displayTypeSwitcher && (
|
||||
<div className={styles.formRow}>
|
||||
<ApplicationTypeSwitcher selectedType={type} setType={setType} appTypes={typeToLabel} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{FormComponent ? (
|
||||
<FormComponent form={form} app={app} />
|
||||
) : (
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message
|
||||
key="toDisplayRegistrationFormChooseType"
|
||||
defaultMessage="To display registration form for a new application choose necessary type."
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!!FormComponent && (
|
||||
<Button color={COLOR_GREEN} block type="submit">
|
||||
{isUpdate ? (
|
||||
<Message key="updateApplication" defaultMessage="Update application" />
|
||||
) : (
|
||||
<Message key="createApplication" defaultMessage="Create application" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApplicationForm;
|
||||
|
@@ -9,7 +9,7 @@ import styles from './applicationTypeSwitcher.scss';
|
||||
interface Props {
|
||||
appTypes: Record<ApplicationType, MessageDescriptor>;
|
||||
selectedType: ApplicationType | null;
|
||||
setType: (type: ApplicationType) => void;
|
||||
setType?: (type: ApplicationType) => void;
|
||||
}
|
||||
|
||||
const ApplicationTypeSwitcher: ComponentType<Props> = ({ appTypes, selectedType, setType }) => (
|
||||
@@ -17,7 +17,7 @@ const ApplicationTypeSwitcher: ComponentType<Props> = ({ appTypes, selectedType,
|
||||
{(Object.keys(appTypes) as unknown as Array<ApplicationType>).map((type) => (
|
||||
<div className={styles.radioContainer} key={type}>
|
||||
<Radio
|
||||
onChange={() => setType(type)}
|
||||
onChange={() => setType?.(type)}
|
||||
skin={SKIN_LIGHT}
|
||||
label={appTypes[type]}
|
||||
value={type}
|
||||
|
@@ -0,0 +1,59 @@
|
||||
import React, { FC } from 'react';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { Input, TextArea, FormModel } from 'app/components/ui/form';
|
||||
import { OauthDesktopAppResponse } from 'app/services/api/oauth';
|
||||
import { SKIN_LIGHT } from 'app/components/ui';
|
||||
import styles from 'app/components/profile/profileForm.scss';
|
||||
|
||||
import commonMessages from './commonMessages';
|
||||
|
||||
interface Props {
|
||||
form: FormModel;
|
||||
app: OauthDesktopAppResponse;
|
||||
}
|
||||
|
||||
const DesktopApplicationType: FC<Props> = ({ form, app }) => (
|
||||
<div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('name')}
|
||||
label={commonMessages.applicationName}
|
||||
defaultValue={app.name}
|
||||
required
|
||||
skin={SKIN_LIGHT}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...commonMessages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<TextArea
|
||||
{...form.bindField('description')}
|
||||
label={commonMessages.description}
|
||||
defaultValue={app.description}
|
||||
skin={SKIN_LIGHT}
|
||||
minRows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...commonMessages.websiteLinkWillBeUsedAsAdditionalId} />
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('websiteUrl')}
|
||||
label={commonMessages.websiteLink}
|
||||
defaultValue={app.websiteUrl}
|
||||
skin={SKIN_LIGHT}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DesktopApplicationType;
|
@@ -1,22 +1,24 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
import { FormattedMessage as Message, defineMessages } from 'react-intl';
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
|
||||
import { OauthMinecraftServerResponse } from 'app/services/api/oauth';
|
||||
import { Input, FormModel } from 'app/components/ui/form';
|
||||
import { SKIN_LIGHT } from 'app/components/ui';
|
||||
import styles from 'app/components/profile/profileForm.scss';
|
||||
|
||||
import commonMessages from './commonMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
serverName: 'Server name:',
|
||||
ipAddressIsOptionButPreferable:
|
||||
'IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=',
|
||||
serverIp: 'Server IP:',
|
||||
youCanAlsoSpecifyServerSite: "You also can specify either server's site URL or its community in a social network.",
|
||||
websiteLink: 'Website link:',
|
||||
});
|
||||
|
||||
interface Props {
|
||||
form: FormModel;
|
||||
app: OauthAppResponse;
|
||||
app: OauthMinecraftServerResponse;
|
||||
}
|
||||
|
||||
const MinecraftServerType: ComponentType<Props> = ({ form, app }) => (
|
||||
@@ -53,7 +55,7 @@ const MinecraftServerType: ComponentType<Props> = ({ form, app }) => (
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('websiteUrl')}
|
||||
label={messages.websiteLink}
|
||||
label={commonMessages.websiteLink}
|
||||
defaultValue={app.websiteUrl}
|
||||
skin={SKIN_LIGHT}
|
||||
/>
|
||||
|
@@ -1,18 +1,13 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
import { defineMessages, FormattedMessage as Message } from 'react-intl';
|
||||
import { Input, TextArea, FormModel } from 'app/components/ui/form';
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
import { OauthWebAppResponse } from 'app/services/api/oauth';
|
||||
import { SKIN_LIGHT } from 'app/components/ui';
|
||||
import styles from 'app/components/profile/profileForm.scss';
|
||||
|
||||
import commonMessages from './commonMessages';
|
||||
|
||||
const messages = defineMessages({
|
||||
applicationName: 'Application name:',
|
||||
appDescriptionWillBeAlsoVisibleOnOauthPage:
|
||||
"Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
|
||||
description: 'Description:',
|
||||
websiteLinkWillBeUsedAsAdditionalId:
|
||||
"Site's link is optional, but it can be used as an additional identifier of the application.",
|
||||
websiteLink: 'Website link:',
|
||||
redirectUriLimitsAllowableBaseAddress:
|
||||
"Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
|
||||
redirectUri: 'Redirect URI:',
|
||||
@@ -20,7 +15,7 @@ const messages = defineMessages({
|
||||
|
||||
interface Props {
|
||||
form: FormModel;
|
||||
app: OauthAppResponse;
|
||||
app: OauthWebAppResponse;
|
||||
}
|
||||
|
||||
const WebsiteType: ComponentType<Props> = ({ form, app }) => (
|
||||
@@ -28,7 +23,7 @@ const WebsiteType: ComponentType<Props> = ({ form, app }) => (
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('name')}
|
||||
label={messages.applicationName}
|
||||
label={commonMessages.applicationName}
|
||||
defaultValue={app.name}
|
||||
required
|
||||
skin={SKIN_LIGHT}
|
||||
@@ -37,13 +32,13 @@ const WebsiteType: ComponentType<Props> = ({ form, app }) => (
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
|
||||
<Message {...commonMessages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<TextArea
|
||||
{...form.bindField('description')}
|
||||
label={messages.description}
|
||||
label={commonMessages.description}
|
||||
defaultValue={app.description}
|
||||
skin={SKIN_LIGHT}
|
||||
minRows={3}
|
||||
@@ -52,13 +47,13 @@ const WebsiteType: ComponentType<Props> = ({ form, app }) => (
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<p className={styles.description}>
|
||||
<Message {...messages.websiteLinkWillBeUsedAsAdditionalId} />
|
||||
<Message {...commonMessages.websiteLinkWillBeUsedAsAdditionalId} />
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.formRow}>
|
||||
<Input
|
||||
{...form.bindField('websiteUrl')}
|
||||
label={messages.websiteLink}
|
||||
label={commonMessages.websiteLink}
|
||||
defaultValue={app.websiteUrl}
|
||||
skin={SKIN_LIGHT}
|
||||
/>
|
||||
|
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
export default defineMessages({
|
||||
applicationName: 'Application name:',
|
||||
appDescriptionWillBeAlsoVisibleOnOauthPage:
|
||||
"Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
|
||||
description: 'Description:',
|
||||
websiteLinkWillBeUsedAsAdditionalId:
|
||||
"Site's link is optional, but it can be used as an additional identifier of the application.",
|
||||
websiteLink: 'Website link:',
|
||||
});
|
@@ -1,3 +1,8 @@
|
||||
export type ApplicationType = 'application' | 'minecraft-server';
|
||||
export const TYPE_APPLICATION = 'application' as const;
|
||||
export const TYPE_WEB_APPLICATION = 'application' as const;
|
||||
export const TYPE_DESKTOP_APPLICATION = 'desktop-application' as const;
|
||||
export const TYPE_MINECRAFT_SERVER = 'minecraft-server' as const;
|
||||
|
||||
export type ApplicationType =
|
||||
| typeof TYPE_WEB_APPLICATION
|
||||
| typeof TYPE_DESKTOP_APPLICATION
|
||||
| typeof TYPE_MINECRAFT_SERVER;
|
||||
|
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { SKIN_LIGHT, COLOR_BLACK, COLOR_RED } from 'app/components/ui';
|
||||
import { Input, Button } from 'app/components/ui/form';
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
@@ -13,20 +14,6 @@ import messages from '../list.intl';
|
||||
const ACTION_REVOKE_TOKENS = 'revoke-tokens';
|
||||
const ACTION_RESET_SECRET = 'reset-secret';
|
||||
const ACTION_DELETE = 'delete';
|
||||
const actionButtons = [
|
||||
{
|
||||
type: ACTION_REVOKE_TOKENS,
|
||||
label: <Message {...messages.revokeAllTokens} />,
|
||||
},
|
||||
{
|
||||
type: ACTION_RESET_SECRET,
|
||||
label: <Message {...messages.resetClientSecret} />,
|
||||
},
|
||||
{
|
||||
type: ACTION_DELETE,
|
||||
label: <Message {...messages.delete} />,
|
||||
},
|
||||
];
|
||||
|
||||
interface State {
|
||||
selectedAction: string | null;
|
||||
@@ -70,8 +57,9 @@ export default class ApplicationItem extends React.Component<
|
||||
<div className={styles.appTileTitle}>
|
||||
<div className={styles.appName}>{app.name}</div>
|
||||
<div className={styles.appStats}>
|
||||
{/* TODO: it must change items order for RTL languages, try to use FormattedList */}
|
||||
Client ID: {app.clientId}
|
||||
{typeof app.countUsers !== 'undefined' && (
|
||||
{'countUsers' in app && (
|
||||
<span>
|
||||
{' | '}
|
||||
<Message
|
||||
@@ -103,34 +91,72 @@ export default class ApplicationItem extends React.Component<
|
||||
<Input label="Client ID:" skin={SKIN_LIGHT} disabled value={app.clientId} copy />
|
||||
</div>
|
||||
|
||||
<div className={styles.appDetailsInfoField}>
|
||||
<Input
|
||||
label="Client Secret:"
|
||||
skin={SKIN_LIGHT}
|
||||
disabled
|
||||
value={app.clientSecret}
|
||||
data-testid="client-secret"
|
||||
copy
|
||||
/>
|
||||
</div>
|
||||
{'clientSecret' in app ? (
|
||||
<>
|
||||
<div className={styles.appDetailsInfoField}>
|
||||
<Input
|
||||
label="Client Secret:"
|
||||
skin={SKIN_LIGHT}
|
||||
disabled
|
||||
value={app.clientSecret}
|
||||
data-testid="client-secret"
|
||||
copy
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.appDetailsDescription}>
|
||||
<Message {...messages.ifYouSuspectingThatSecretHasBeenCompromised} />
|
||||
</div>
|
||||
<div className={styles.appDetailsDescription}>
|
||||
<Message {...messages.ifYouSuspectingThatSecretHasBeenCompromised} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.appDetailsDescription}>
|
||||
<Message
|
||||
key="publickOauthApplicationDescription"
|
||||
defaultMessage="This is a public client and it can't have any secrets by design. To secure authorization flow, use the PKCE challenge code. <link>Read more</link>."
|
||||
values={{
|
||||
// @ts-expect-error those typings seems to be invalid in the current version of react-intl, but might be fixed later
|
||||
link: (nodes) => (
|
||||
<a href="https://www.oauth.com/oauth2-servers/pkce/" target="_blank">
|
||||
{nodes}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.appActionsButtons}>
|
||||
{actionButtons.map(({ type, label }) => (
|
||||
<Button
|
||||
color={COLOR_BLACK}
|
||||
className={styles.appActionButton}
|
||||
disabled={!!selectedAction && selectedAction !== ACTION_REVOKE_TOKENS}
|
||||
onClick={this.onActionButtonClick(ACTION_REVOKE_TOKENS)}
|
||||
small
|
||||
>
|
||||
<Message {...messages.revokeAllTokens} />
|
||||
</Button>
|
||||
|
||||
{'clientSecret' in app && (
|
||||
<Button
|
||||
key={type}
|
||||
color={COLOR_BLACK}
|
||||
className={styles.appActionButton}
|
||||
disabled={!!selectedAction && selectedAction !== type}
|
||||
onClick={this.onActionButtonClick(type)}
|
||||
disabled={!!selectedAction && selectedAction !== ACTION_RESET_SECRET}
|
||||
onClick={this.onActionButtonClick(ACTION_RESET_SECRET)}
|
||||
small
|
||||
>
|
||||
{label}
|
||||
<Message {...messages.resetClientSecret} />
|
||||
</Button>
|
||||
))}
|
||||
)}
|
||||
|
||||
<Button
|
||||
color={COLOR_BLACK}
|
||||
className={styles.appActionButton}
|
||||
disabled={!!selectedAction && selectedAction !== ACTION_DELETE}
|
||||
onClick={this.onActionButtonClick(ACTION_DELETE)}
|
||||
small
|
||||
>
|
||||
<Message {...messages.delete} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { restoreScroll } from 'app/components/ui/scroll/scroll';
|
||||
import { FormattedMessage as Message } from 'react-intl';
|
||||
|
||||
import { restoreScroll } from 'app/components/ui/scroll/scroll';
|
||||
import { LinkButton } from 'app/components/ui/form';
|
||||
import { COLOR_GREEN } from 'app/components/ui';
|
||||
import { OauthAppResponse } from 'app/services/api/oauth';
|
||||
@@ -9,24 +10,24 @@ import messages from '../list.intl';
|
||||
import styles from '../applicationsIndex.scss';
|
||||
import ApplicationItem from './ApplicationItem';
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
applications: OauthAppResponse[];
|
||||
deleteApp: (clientId: string) => Promise<any>;
|
||||
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
|
||||
resetClientId: () => void;
|
||||
clientId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
type State = {
|
||||
interface State {
|
||||
expandedApp: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export default class ApplicationsList extends React.Component<Props, State> {
|
||||
state = {
|
||||
expandedApp: null,
|
||||
};
|
||||
|
||||
appsRefs: { [key: string]: HTMLDivElement | null } = {};
|
||||
appsRefs: Record<string, HTMLDivElement | null> = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.checkForActiveApp();
|
||||
|
@@ -9,21 +9,32 @@ export interface Client {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface OauthAppResponse {
|
||||
interface OauthCommonResponse {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
type: ApplicationType;
|
||||
name: string;
|
||||
websiteUrl: string;
|
||||
createdAt: number;
|
||||
// fields for 'application' type
|
||||
countUsers?: number;
|
||||
description?: string;
|
||||
redirectUri?: string;
|
||||
// fields for 'minecraft-server' type
|
||||
minecraftServerIp?: string;
|
||||
}
|
||||
|
||||
export interface OauthWebAppResponse extends OauthCommonResponse {
|
||||
clientSecret: string;
|
||||
countUsers: number;
|
||||
description: string;
|
||||
redirectUri: string;
|
||||
}
|
||||
|
||||
export interface OauthDesktopAppResponse extends OauthCommonResponse {
|
||||
countUsers: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface OauthMinecraftServerResponse extends OauthCommonResponse {
|
||||
minecraftServerIp: string;
|
||||
}
|
||||
|
||||
export type OauthAppResponse = OauthWebAppResponse | OauthDesktopAppResponse | OauthMinecraftServerResponse;
|
||||
|
||||
interface AuthCodeFlowRequestData {
|
||||
client_id: string;
|
||||
redirect_uri: string;
|
||||
|
Reference in New Issue
Block a user