Skip to content

Commit

Permalink
feat: add login info to consent screen
Browse files Browse the repository at this point in the history
  • Loading branch information
its-felix committed Oct 6, 2024
1 parent a48d589 commit 6c9c87a
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 35 deletions.
13 changes: 13 additions & 0 deletions src/components/common/issuer-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { faGithub, faGoogle } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import { Issuer } from '../../lib/api/api.model';
import { Gw2AuthLogo } from './gw2auth-logo';

export function IssuerIcon({ issuer }: { issuer: Issuer }) {
return {
[Issuer.GITHUB]: (<FontAwesomeIcon icon={faGithub} />),
[Issuer.GOOGLE]: (<FontAwesomeIcon icon={faGoogle} />),
[Issuer.COGNITO]: (<Gw2AuthLogo />),
}[issuer];
}
12 changes: 5 additions & 7 deletions src/components/login/login.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {
Alert, Button, ColumnLayout, ExpandableSection, SpaceBetween,
Alert, Button, ColumnLayout, ExpandableSection, SpaceBetween
} from '@cloudscape-design/components';
import { faGithub, faGoogle } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useMemo } from 'react';
import { Issuer } from '../../lib/api/api.model';
import { Gw2AuthLogo } from '../common/gw2auth-logo';
import { IssuerIcon } from '../common/issuer-icon';
import { useI18n } from '../util/context/i18n';
import { usePreviousIssuer } from '../util/state/use-previous-issuer';

Expand All @@ -14,9 +12,9 @@ export default function Login() {
const [previousIssuer] = usePreviousIssuer();
const [alert, loginSelection] = useMemo(() => {
const buttonByIssuer = {
[Issuer.GITHUB]: (<Button iconSvg={<FontAwesomeIcon icon={faGithub} />} variant={'primary'} fullWidth={true} href={'/auth/oauth2/authorization/github'}>{i18n.components.login.loginWith(Issuer.GITHUB)}</Button>),
[Issuer.GOOGLE]: (<Button iconSvg={<FontAwesomeIcon icon={faGoogle} />} variant={'primary'} fullWidth={true} href={'/auth/oauth2/authorization/google'}>{i18n.components.login.loginWith(Issuer.GOOGLE)}</Button>),
[Issuer.COGNITO]: (<Button iconSvg={<Gw2AuthLogo />} variant={'primary'} fullWidth={true} href={'/auth/oauth2/authorization/cognito'}>{i18n.components.login.loginWith(Issuer.COGNITO)}</Button>),
[Issuer.GITHUB]: (<Button iconSvg={<IssuerIcon issuer={Issuer.GITHUB} /> } variant={'primary'} fullWidth={true} href={'/auth/oauth2/authorization/github'}>{i18n.components.login.loginWith(Issuer.GITHUB)}</Button>),
[Issuer.GOOGLE]: (<Button iconSvg={<IssuerIcon issuer={Issuer.GOOGLE} />} variant={'primary'} fullWidth={true} href={'/auth/oauth2/authorization/google'}>{i18n.components.login.loginWith(Issuer.GOOGLE)}</Button>),
[Issuer.COGNITO]: (<Button iconSvg={<IssuerIcon issuer={Issuer.COGNITO} />} variant={'primary'} fullWidth={true} href={'/auth/oauth2/authorization/cognito'}>{i18n.components.login.loginWith(Issuer.COGNITO)}</Button>),
};

if (previousIssuer === null) {
Expand Down
1 change: 1 addition & 0 deletions src/components/util/context/auth-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function useAuthInfo() {
const MustAuthInfoContext = createContext<AuthInfo>({
sessionId: '',
sessionCreationTime: new Date().toISOString(),
accountCreationTime: new Date().toISOString(),
issuer: Issuer.COGNITO,
idAtIssuer: '',
});
Expand Down
2 changes: 2 additions & 0 deletions src/lib/api/api.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export enum Issuer {
export interface AuthInfo {
sessionId: string;
sessionCreationTime: string;
accountCreationTime: string;
issuer: Issuer;
idAtIssuer: string;
}
Expand Down Expand Up @@ -232,6 +233,7 @@ export interface OAuth2ConsentInfo {
previouslyConsentedGw2AccountIds: ReadonlyArray<string>;
containsAnyGw2AccountRelatedScopes: boolean;
redirectUri: string;
requestUri: string;
}

export interface MinimalApiToken {
Expand Down
1 change: 1 addition & 0 deletions src/lib/i18n/i18n.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type I18nRoute = string | I18nRouteExplicit;
export interface I18nFormats {
locale: EffectiveLocale,
general: {
issuerName: (v: Issuer) => string,
date: (d: Date) => string,
time: (d: Date) => string,
dateTime: (d: Date) => string,
Expand Down
1 change: 1 addition & 0 deletions src/lib/i18n/i18n_de.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const COMMON = {
export const I18N_DE = ({
locale: Locale.DE,
general: {
issuerName: COMMON.issuerName,
date: (d) => d.toLocaleDateString('de-DE'),
time: (d) => d.toLocaleTimeString('de-DE'),
dateTime: (d) => d.toLocaleString('de-DE'),
Expand Down
1 change: 1 addition & 0 deletions src/lib/i18n/i18n_en.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const COMMON = {
export const I18N_EN = ({
locale: Locale.EN,
general: {
issuerName: COMMON.issuerName,
date: (d) => d.toLocaleDateString('en-US'),
time: (d) => d.toLocaleTimeString('en-US'),
dateTime: (d) => d.toLocaleString('en-US'),
Expand Down
136 changes: 108 additions & 28 deletions src/pages/oauth2-consent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Alert,
Box,
Button,
Container,
Expand All @@ -9,16 +10,19 @@ import {
Header,
Multiselect,
MultiselectProps, SpaceBetween,
Spinner, TextContent,
Spinner, TextContent
} from '@cloudscape-design/components';
import React, {
useCallback, useEffect, useMemo, useState,
} from 'react';
import { useSearchParams } from 'react-router-dom';
import { AddApiTokenWizard } from '../components/add-api-token/add-api-token';
import { IssuerIcon } from '../components/common/issuer-icon';
import { Scopes } from '../components/scopes/scopes';
import { catchNotify, useAppControls } from '../components/util/context/app-controls';
import { useMustAuthInfo } from '../components/util/context/auth-info';
import { useHttpClient } from '../components/util/context/http-client';
import { useI18n } from '../components/util/context/i18n';
import { useDateFormat } from '../components/util/state/use-dateformat';
import { VerificationSelection, VerificationWizard } from '../components/verification/verification-wizard';
import { expectSuccess } from '../lib/api/api';
Expand Down Expand Up @@ -63,45 +67,86 @@ export function OAuth2Consent() {
reloadConsentInfo();
}, [reloadConsentInfo]);

if (activeWindow === 'form' || activeWindow === 'verification-select') {
let content: React.ReactNode;
if (isLoading) {
content = <Spinner size={'large'} />;
} else if (consentInfo === undefined) {
content = <Box>Failed to load</Box>;
} else if (activeWindow === 'verification-select') {
content = (
<Container header={<Header variant={'h1'}>Guild Wars 2 Account Verification</Header>}>
<VerificationSelection onCancel={() => setActiveWindow('form')} onContinue={() => setActiveWindow('verification-new')} />
</Container>
);
} else {
content = <ConsentForm consentInfo={consentInfo} setActiveWindow={setActiveWindow} />;
}

if (isLoading) {
return (
<ContentLayout>
<Grid gridDefinition={[{ colspan: { default: 12, xs: 10, s: 8 }, offset: { default: 0, xs: 1, s: 2 } }]}>
{content}
</Grid>
</ContentLayout>
);
<StandardLayout>
<Spinner size={'large'} />
</StandardLayout>
)
}

if (activeWindow === 'add-api-token') {
if (!consentInfo) {
return (
<StandardLayout>
<Box>Failed to load</Box>
</StandardLayout>
)
}

let content: React.ReactNode;
if (activeWindow === 'form') {
content = <ConsentForm consentInfo={consentInfo} setActiveWindow={setActiveWindow} />;
} else if (activeWindow === 'verification-select') {
content = (
<Container header={<Header variant={'h1'}>Guild Wars 2 Account Verification</Header>}>
<VerificationSelection onCancel={() => setActiveWindow('form')} onContinue={() => setActiveWindow('verification-new')} />
</Container>
);
} else if (activeWindow === 'add-api-token') {
content = (
<AddApiTokenWizard onDismiss={() => {
reloadConsentInfo();
setActiveWindow('form');
}} />
);
} else if (activeWindow === 'verification-new') {
content = (
<VerificationWizard onDismiss={() => {
reloadConsentInfo();
setActiveWindow('form');
}} />
);
} else {
content = <Box>Unknown window</Box>;
}

if (activeWindow === 'add-api-token' || activeWindow === 'verification-new') {
// wizard: dont wrap in ContentLayout
return (
<SpaceBetween size={'l'} direction={'vertical'}>
<StandardGrid>
<LoginInfo requestUri={consentInfo.requestUri} />
</StandardGrid>
{content}
</SpaceBetween>
);
}

return (
<VerificationWizard onDismiss={() => {
reloadConsentInfo();
setActiveWindow('form');
}} />
<StandardLayout>
<SpaceBetween size={'l'} direction={'vertical'}>
<LoginInfo requestUri={consentInfo.requestUri} />
{content}
</SpaceBetween>
</StandardLayout>
);
}

function StandardLayout({ children }: React.PropsWithChildren) {
return (
<ContentLayout>
<StandardGrid>
{children}
</StandardGrid>
</ContentLayout>
);
}

function StandardGrid({ children }: React.PropsWithChildren) {
return (
<Grid gridDefinition={[{ colspan: { default: 12, xs: 10, s: 8 }, offset: { default: 0, xs: 1, s: 2 } }]}>
{children}
</Grid>
);
}

Expand Down Expand Up @@ -218,6 +263,41 @@ function ConsentForm({ consentInfo, setActiveWindow }: { consentInfo: OAuth2Cons
);
}

function LoginInfo({ requestUri }: { requestUri: string }) {
const i18n = useI18n();
const { apiClient } = useHttpClient();
const { notification } = useAppControls();
const { formatDate } = useDateFormat();
const authInfo = useMustAuthInfo();

const [isLoading, setLoading] = useState(false);

function logout() {
setLoading(true);
(async () => {
const resp = await apiClient.logout();
if (resp.status >= 500) {
expectSuccess(resp);
return;
}

window.location.href = requestUri;
})()
.catch(catchNotify(notification))
.finally(() => setLoading(false));
}

return (
<Alert
type={'info'}
action={<Button variant={'inline-link'} loading={isLoading} onClick={logout}>Not you? Sign out</Button>}
>
<Box>Signed in using <Box variant={'strong'}><IssuerIcon issuer={authInfo.issuer} /> {i18n.general.issuerName(authInfo.issuer)}</Box></Box>
<Box>Account created: <Box variant={'strong'}>{formatDate(authInfo.accountCreationTime)}</Box></Box>
</Alert>
)
}

function buildOptions(consentInfo: OAuth2ConsentInfo) {
const [options, selectedOptions] = useMemo(() => {
const requestedVerifiedScope = consentInfo.requestedScopes.includes('gw2auth:verified');
Expand Down

0 comments on commit 6c9c87a

Please sign in to comment.