webauthn: update login flow to support other 2fa methods
This commit is contained in:
parent
42a3e740ba
commit
31c2ef5279
13 changed files with 255 additions and 41 deletions
|
@ -90,11 +90,10 @@ abstract class AbstractLoginController extends Controller
|
||||||
$this->auth->guard()->login($user, true);
|
$this->auth->guard()->login($user, true);
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'data' => [
|
|
||||||
'complete' => true,
|
'complete' => true,
|
||||||
|
'methods' => [],
|
||||||
'intended' => $this->redirectPath(),
|
'intended' => $this->redirectPath(),
|
||||||
'user' => $user->toReactObject(),
|
'user' => $user->toReactObject(),
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ namespace Pterodactyl\Http\Controllers\Auth;
|
||||||
|
|
||||||
use Pterodactyl\Models\User;
|
use Pterodactyl\Models\User;
|
||||||
use Illuminate\Auth\AuthManager;
|
use Illuminate\Auth\AuthManager;
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use PragmaRX\Google2FA\Google2FA;
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
use Illuminate\Contracts\Config\Repository;
|
use Illuminate\Contracts\Config\Repository;
|
||||||
use Illuminate\Contracts\Encryption\Encrypter;
|
use Illuminate\Contracts\Encryption\Encrypter;
|
||||||
|
@ -48,7 +47,7 @@ class LoginCheckpointController extends AbstractLoginController
|
||||||
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
*/
|
*/
|
||||||
public function __invoke(LoginCheckpointRequest $request): JsonResponse
|
public function __invoke(LoginCheckpointRequest $request)
|
||||||
{
|
{
|
||||||
if ($this->hasTooManyLoginAttempts($request)) {
|
if ($this->hasTooManyLoginAttempts($request)) {
|
||||||
$this->sendLockoutResponse($request);
|
$this->sendLockoutResponse($request);
|
||||||
|
|
|
@ -17,11 +17,11 @@ use Pterodactyl\Exceptions\Repository\RecordNotFoundException;
|
||||||
|
|
||||||
class LoginController extends AbstractLoginController
|
class LoginController extends AbstractLoginController
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private const SESSION_PUBLICKEY_REQUEST = 'webauthn.publicKeyRequest';
|
private const SESSION_PUBLICKEY_REQUEST = 'webauthn.publicKeyRequest';
|
||||||
|
|
||||||
|
private const METHOD_TOTP = 'totp';
|
||||||
|
private const METHOD_WEBAUTHN = 'webauthn';
|
||||||
|
|
||||||
private CacheRepository $cache;
|
private CacheRepository $cache;
|
||||||
private UserRepositoryInterface $repository;
|
private UserRepositoryInterface $repository;
|
||||||
private ViewFactory $view;
|
private ViewFactory $view;
|
||||||
|
@ -61,7 +61,7 @@ class LoginController extends AbstractLoginController
|
||||||
* @throws \Pterodactyl\Exceptions\DisplayException
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
*/
|
*/
|
||||||
public function login(Request $request): JsonResponse
|
public function login(Request $request)
|
||||||
{
|
{
|
||||||
$username = $request->input('user');
|
$username = $request->input('user');
|
||||||
$useColumn = $this->getField($username);
|
$useColumn = $this->getField($username);
|
||||||
|
@ -99,9 +99,9 @@ class LoginController extends AbstractLoginController
|
||||||
$request->session()->put(self::SESSION_PUBLICKEY_REQUEST, $publicKey);
|
$request->session()->put(self::SESSION_PUBLICKEY_REQUEST, $publicKey);
|
||||||
$request->session()->save();
|
$request->session()->save();
|
||||||
|
|
||||||
$methods = ['webauthn'];
|
$methods = [ self::METHOD_WEBAUTHN ];
|
||||||
if ($user->use_totp) {
|
if ($user->use_totp) {
|
||||||
$methods[] = 'totp';
|
$methods[] = self::METHOD_TOTP;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
|
@ -118,7 +118,7 @@ class LoginController extends AbstractLoginController
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'complete' => false,
|
'complete' => false,
|
||||||
'methods' => ['totp'],
|
'methods' => [ self::METHOD_TOTP ],
|
||||||
'confirmation_token' => $token,
|
'confirmation_token' => $token,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
9
resources/scripts/api/account/webauthn/deleteKey.ts
Normal file
9
resources/scripts/api/account/webauthn/deleteKey.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import http from '@/api/http';
|
||||||
|
|
||||||
|
export default (id: number): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.delete(`/api/client/account/webauthn/${id}`)
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
23
resources/scripts/api/account/webauthn/getWebauthn.ts
Normal file
23
resources/scripts/api/account/webauthn/getWebauthn.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import http from '@/api/http';
|
||||||
|
|
||||||
|
export interface Key {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
createdAt: Date;
|
||||||
|
lastUsedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rawDataToKey = (data: any): Key => ({
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
createdAt: new Date(data.created_at),
|
||||||
|
lastUsedAt: new Date(data.last_used_at) || new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default (): Promise<Key[]> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.get('/api/client/account/webauthn')
|
||||||
|
.then(({ data }) => resolve((data.data || []).map((d: any) => rawDataToKey(d.attributes))))
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
51
resources/scripts/api/account/webauthn/login.ts
Normal file
51
resources/scripts/api/account/webauthn/login.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { LoginResponse } from '@/api/auth/login';
|
||||||
|
import { base64Decode, bufferDecode, bufferEncode, decodeCredentials } from '@/api/account/webauthn/registerKey';
|
||||||
|
|
||||||
|
export default (token: string, publicKey: PublicKeyCredentialRequestOptions): Promise<LoginResponse> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log(token);
|
||||||
|
console.log(publicKey);
|
||||||
|
const publicKeyCredential = Object.assign({}, publicKey);
|
||||||
|
|
||||||
|
publicKeyCredential.challenge = bufferDecode(base64Decode(publicKey.challenge.toString()));
|
||||||
|
if (publicKey.allowCredentials) {
|
||||||
|
publicKeyCredential.allowCredentials = decodeCredentials(publicKey.allowCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.credentials.get({
|
||||||
|
publicKey: publicKeyCredential,
|
||||||
|
}).then((c) => {
|
||||||
|
if (c === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const credential = c as PublicKeyCredential;
|
||||||
|
const response = credential.response as AuthenticatorAssertionResponse;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
confirmation_token: token,
|
||||||
|
|
||||||
|
data: JSON.stringify({
|
||||||
|
id: credential.id,
|
||||||
|
type: credential.type,
|
||||||
|
rawId: bufferEncode(credential.rawId),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
authenticatorData: bufferEncode(response.authenticatorData),
|
||||||
|
clientDataJSON: bufferEncode(response.clientDataJSON),
|
||||||
|
signature: bufferEncode(response.signature),
|
||||||
|
userHandle: response.userHandle ? bufferEncode(response.userHandle) : null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
http.post('/auth/login/checkpoint/key', data).then(response => {
|
||||||
|
return resolve({
|
||||||
|
complete: response.data.complete,
|
||||||
|
intended: response.data.data?.intended || undefined,
|
||||||
|
});
|
||||||
|
}).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
};
|
73
resources/scripts/api/account/webauthn/registerKey.ts
Normal file
73
resources/scripts/api/account/webauthn/registerKey.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { Key, rawDataToKey } from '@/api/account/webauthn/getWebauthn';
|
||||||
|
|
||||||
|
export const base64Decode = (input: string): string => {
|
||||||
|
input = input.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
|
const pad = input.length % 4;
|
||||||
|
if (pad) {
|
||||||
|
if (pad === 1) {
|
||||||
|
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
|
||||||
|
}
|
||||||
|
input += new Array(5 - pad).join('=');
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bufferDecode = (value: string): ArrayBuffer => {
|
||||||
|
return Uint8Array.from(window.atob(value), c => c.charCodeAt(0));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bufferEncode = (value: ArrayBuffer): string => {
|
||||||
|
// @ts-ignore
|
||||||
|
return window.btoa(String.fromCharCode.apply(null, new Uint8Array(value)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeCredentials = (credentials: PublicKeyCredentialDescriptor[]) => {
|
||||||
|
return credentials.map(c => {
|
||||||
|
return {
|
||||||
|
id: bufferDecode(base64Decode(c.id.toString())),
|
||||||
|
type: c.type,
|
||||||
|
transports: c.transports,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (name: string): Promise<Key> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.get('/api/client/account/webauthn/register').then((res) => {
|
||||||
|
const publicKey = res.data.public_key;
|
||||||
|
const publicKeyCredential = Object.assign({}, publicKey);
|
||||||
|
|
||||||
|
publicKeyCredential.user.id = bufferDecode(publicKey.user.id);
|
||||||
|
publicKeyCredential.challenge = bufferDecode(base64Decode(publicKey.challenge));
|
||||||
|
if (publicKey.excludeCredentials) {
|
||||||
|
publicKeyCredential.excludeCredentials = decodeCredentials(publicKey.excludeCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigator.credentials.create({
|
||||||
|
publicKey: publicKeyCredential,
|
||||||
|
});
|
||||||
|
}).then((c) => {
|
||||||
|
if (c === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const credential = c as PublicKeyCredential;
|
||||||
|
const response = credential.response as AuthenticatorAttestationResponse;
|
||||||
|
|
||||||
|
http.post('/api/client/account/webauthn/register', {
|
||||||
|
name: name,
|
||||||
|
|
||||||
|
register: JSON.stringify({
|
||||||
|
id: credential.id,
|
||||||
|
type: credential.type,
|
||||||
|
rawId: bufferEncode(credential.rawId),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
attestationObject: bufferEncode(response.attestationObject),
|
||||||
|
clientDataJSON: bufferEncode(response.clientDataJSON),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).then(({ data }) => resolve(rawDataToKey(data.attributes))).catch(reject);
|
||||||
|
}).catch(reject);
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,9 +1,11 @@
|
||||||
import http from '@/api/http';
|
import http from '@/api/http';
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
|
methods?: string[];
|
||||||
complete: boolean;
|
complete: boolean;
|
||||||
intended?: string;
|
intended?: string;
|
||||||
confirmationToken?: string;
|
confirmationToken?: string;
|
||||||
|
publicKey?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoginData {
|
export interface LoginData {
|
||||||
|
@ -19,15 +21,18 @@ export default ({ username, password, recaptchaData }: LoginData): Promise<Login
|
||||||
password,
|
password,
|
||||||
'g-recaptcha-response': recaptchaData,
|
'g-recaptcha-response': recaptchaData,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(({ data }) => {
|
||||||
if (!(response.data instanceof Object)) {
|
if (!(data instanceof Object)) {
|
||||||
return reject(new Error('An error occurred while processing the login request.'));
|
return reject(new Error('An error occurred while processing the login request.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve({
|
return resolve({
|
||||||
complete: response.data.data.complete,
|
methods: data.methods,
|
||||||
intended: response.data.data.intended || undefined,
|
complete: data.complete,
|
||||||
confirmationToken: response.data.data.confirmation_token || undefined,
|
intended: data.intended || undefined,
|
||||||
|
confirmationToken: data.confirmation_token || undefined,
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
publicKey: data.webauthn?.public_key || undefined,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
|
|
|
@ -9,8 +9,8 @@ export default (token: string, code: string, recoveryToken?: string): Promise<Lo
|
||||||
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined,
|
||||||
})
|
})
|
||||||
.then(response => resolve({
|
.then(response => resolve({
|
||||||
complete: response.data.data.complete,
|
complete: response.data.complete,
|
||||||
intended: response.data.data.intended || undefined,
|
intended: response.data.intended || undefined,
|
||||||
}))
|
}))
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
});
|
});
|
||||||
|
|
|
@ -39,19 +39,37 @@ const LoginContainer = ({ history }: RouteComponentProps) => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
clearAndAddHttpError({ error });
|
clearAndAddHttpError({ error });
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
login({ ...values, recaptchaData: token })
|
login({ ...values, recaptchaData: token })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
console.log('wow!');
|
||||||
|
console.log(response);
|
||||||
if (response.complete) {
|
if (response.complete) {
|
||||||
|
console.log(`Redirecting to: ${response.intended || '/'}`);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location = response.intended || '/';
|
window.location = response.intended || '/';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.methods?.includes('webauthn')) {
|
||||||
|
console.log('Redirecting to: /auth/login/key');
|
||||||
|
history.replace('/auth/login/key', {
|
||||||
|
token: response.confirmationToken,
|
||||||
|
publicKey: response.publicKey,
|
||||||
|
hasTotp: response.methods.includes('totp'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.methods?.includes('totp')) {
|
||||||
|
console.log('/auth/login/checkpoint');
|
||||||
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
history.replace('/auth/login/checkpoint', { token: response.confirmationToken });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('huh?');
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
@ -5,11 +5,7 @@ import { breakpoint } from '@/theme';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
type Props = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
const Wrapper = styled.div`
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
${breakpoint('sm')`
|
${breakpoint('sm')`
|
||||||
${tw`w-4/5 mx-auto`}
|
${tw`w-4/5 mx-auto`}
|
||||||
`};
|
`};
|
||||||
|
@ -28,24 +24,26 @@ const Container = styled.div`
|
||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) => (
|
const Inner = ({ children }: { children: React.ReactNode }) => (
|
||||||
<Container>
|
<div css={tw`md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1`}>
|
||||||
|
<div css={tw`flex-none select-none mb-6 md:mb-0 self-center`}>
|
||||||
|
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`}/>
|
||||||
|
</div>
|
||||||
|
<div css={tw`flex-1`}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Container = ({ title, children }: { title?: string, children: React.ReactNode }) => (
|
||||||
|
<Wrapper>
|
||||||
{title &&
|
{title &&
|
||||||
<h2 css={tw`text-3xl text-center text-neutral-100 font-medium py-4`}>
|
<h2 css={tw`text-3xl text-center text-neutral-100 font-medium py-4`}>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
}
|
}
|
||||||
<FlashMessageRender css={tw`mb-2 px-1`}/>
|
<FlashMessageRender css={tw`mb-2 px-1`}/>
|
||||||
<Form {...props} ref={ref}>
|
{children}
|
||||||
<div css={tw`md:flex w-full bg-white shadow-lg rounded-lg p-6 md:pl-0 mx-1`}>
|
|
||||||
<div css={tw`flex-none select-none mb-6 md:mb-0 self-center`}>
|
|
||||||
<img src={'/assets/svgs/pterodactyl.svg'} css={tw`block w-48 md:w-64 mx-auto`}/>
|
|
||||||
</div>
|
|
||||||
<div css={tw`flex-1`}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
<p css={tw`text-center text-neutral-500 text-xs mt-4`}>
|
<p css={tw`text-center text-neutral-500 text-xs mt-4`}>
|
||||||
© 2015 - {(new Date()).getFullYear()}
|
© 2015 - {(new Date()).getFullYear()}
|
||||||
<a
|
<a
|
||||||
|
@ -57,5 +55,31 @@ export default forwardRef<HTMLFormElement, Props>(({ title, ...props }, ref) =>
|
||||||
Pterodactyl Software
|
Pterodactyl Software
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
type FormContainerProps = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormContainer = forwardRef<HTMLFormElement, FormContainerProps>(({ title, ...props }, ref) => (
|
||||||
|
<Container title={title}>
|
||||||
|
<Form {...props} ref={ref}>
|
||||||
|
<Inner>{props.children}</Inner>
|
||||||
|
</Form>
|
||||||
</Container>
|
</Container>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
type DivContainerProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DivContainer = ({ title, ...props }: DivContainerProps) => (
|
||||||
|
<Container title={title}>
|
||||||
|
<div {...props}>
|
||||||
|
<Inner>{props.children}</Inner>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FormContainer;
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import { DivContainer as LoginFormContainer } from '@/components/auth/LoginFormContainer';
|
||||||
|
|
||||||
|
const LoginKeyCheckpointContainer = () => {
|
||||||
|
return (
|
||||||
|
<LoginFormContainer title={'Login to Continue'} css={tw`w-full flex`} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginKeyCheckpointContainer;
|
|
@ -1,9 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||||
import LoginContainer from '@/components/auth/LoginContainer';
|
import LoginContainer from '@/components/auth/LoginContainer';
|
||||||
|
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
||||||
|
import LoginKeyCheckpointContainer from '@/components/auth/LoginKeyCheckpointContainer';
|
||||||
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
||||||
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
||||||
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
|
||||||
import { NotFound } from '@/components/elements/ScreenBlock';
|
import { NotFound } from '@/components/elements/ScreenBlock';
|
||||||
|
|
||||||
export default ({ location, history, match }: RouteComponentProps) => (
|
export default ({ location, history, match }: RouteComponentProps) => (
|
||||||
|
@ -11,6 +12,7 @@ export default ({ location, history, match }: RouteComponentProps) => (
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
|
<Route path={`${match.path}/login`} component={LoginContainer} exact/>
|
||||||
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
<Route path={`${match.path}/login/checkpoint`} component={LoginCheckpointContainer}/>
|
||||||
|
<Route path={`${match.path}/login/key`} component={LoginKeyCheckpointContainer}/>
|
||||||
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
<Route path={`${match.path}/password`} component={ForgotPasswordContainer} exact/>
|
||||||
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
<Route path={`${match.path}/password/reset/:token`} component={ResetPasswordContainer}/>
|
||||||
<Route path={`${match.path}/checkpoint`}/>
|
<Route path={`${match.path}/checkpoint`}/>
|
||||||
|
|
Loading…
Reference in a new issue