Add base logic to configure two factor on account
This commit is contained in:
parent
edf27a5542
commit
eb39826f46
15 changed files with 389 additions and 54 deletions
|
@ -1,16 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Pterodactyl - Panel
|
|
||||||
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
|
|
||||||
*
|
|
||||||
* This software is licensed under the terms of the MIT license.
|
|
||||||
* https://opensource.org/licenses/MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Pterodactyl\Exceptions\Service\User;
|
namespace Pterodactyl\Exceptions\Service\User;
|
||||||
|
|
||||||
use Exception;
|
use Pterodactyl\Exceptions\DisplayException;
|
||||||
|
|
||||||
class TwoFactorAuthenticationTokenInvalid extends Exception
|
class TwoFactorAuthenticationTokenInvalid extends DisplayException
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
106
app/Http/Controllers/Api/Client/TwoFactorController.php
Normal file
106
app/Http/Controllers/Api/Client/TwoFactorController.php
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Controllers\Api\Client;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Contracts\Validation\Factory;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Pterodactyl\Services\Users\TwoFactorSetupService;
|
||||||
|
use Pterodactyl\Services\Users\ToggleTwoFactorService;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
|
||||||
|
class TwoFactorController extends ClientApiController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Services\Users\TwoFactorSetupService
|
||||||
|
*/
|
||||||
|
private $setupService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Illuminate\Contracts\Validation\Factory
|
||||||
|
*/
|
||||||
|
private $validation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Services\Users\ToggleTwoFactorService
|
||||||
|
*/
|
||||||
|
private $toggleTwoFactorService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TwoFactorController constructor.
|
||||||
|
*
|
||||||
|
* @param \Pterodactyl\Services\Users\ToggleTwoFactorService $toggleTwoFactorService
|
||||||
|
* @param \Pterodactyl\Services\Users\TwoFactorSetupService $setupService
|
||||||
|
* @param \Illuminate\Contracts\Validation\Factory $validation
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
ToggleTwoFactorService $toggleTwoFactorService,
|
||||||
|
TwoFactorSetupService $setupService,
|
||||||
|
Factory $validation
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->setupService = $setupService;
|
||||||
|
$this->validation = $validation;
|
||||||
|
$this->toggleTwoFactorService = $toggleTwoFactorService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns two-factor token credentials that allow a user to configure
|
||||||
|
* it on their account. If two-factor is already enabled this endpoint
|
||||||
|
* will return a 400 error.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
if ($request->user()->totp_enabled) {
|
||||||
|
throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse::create([
|
||||||
|
'data' => [
|
||||||
|
'image_url_data' => $this->setupService->handle($request->user()),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a user's account to have two-factor enabled.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
|
* @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException
|
||||||
|
* @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException
|
||||||
|
* @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid
|
||||||
|
*/
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$validator = $this->validation->make($request->all(), [
|
||||||
|
'code' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
throw new ValidationException($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true);
|
||||||
|
|
||||||
|
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,7 +65,9 @@ class ToggleTwoFactorService
|
||||||
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
|
$isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window'));
|
||||||
|
|
||||||
if (! $isValidToken) {
|
if (! $isValidToken) {
|
||||||
throw new TwoFactorAuthenticationTokenInvalid;
|
throw new TwoFactorAuthenticationTokenInvalid(
|
||||||
|
'The token provided is not valid.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->repository->withoutFreshModel()->update($user->id, [
|
$this->repository->withoutFreshModel()->update($user->id, [
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"react-transition-group": "^4.3.0",
|
"react-transition-group": "^4.3.0",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"styled-components": "^4.4.1",
|
"styled-components": "^4.4.1",
|
||||||
|
"styled-components-breakpoint": "^3.0.0-preview.20",
|
||||||
"use-react-router": "^1.0.7",
|
"use-react-router": "^1.0.7",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
"xterm": "^3.14.4",
|
"xterm": "^3.14.4",
|
||||||
|
|
9
resources/scripts/api/account/enableAccountTwoFactor.ts
Normal file
9
resources/scripts/api/account/enableAccountTwoFactor.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import http from '@/api/http';
|
||||||
|
|
||||||
|
export default (code: string): Promise<void> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.post('/api/client/account/two-factor', { code })
|
||||||
|
.then(() => resolve())
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
9
resources/scripts/api/account/getTwoFactorTokenUrl.ts
Normal file
9
resources/scripts/api/account/getTwoFactorTokenUrl.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import http from '@/api/http';
|
||||||
|
|
||||||
|
export default (): Promise<string> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
http.get('/api/client/account/two-factor')
|
||||||
|
.then(({ data }) => resolve(data.data.image_url_data))
|
||||||
|
.catch(reject);
|
||||||
|
});
|
||||||
|
};
|
|
@ -8,6 +8,7 @@ import ServerRouter from '@/routers/ServerRouter';
|
||||||
import AuthenticationRouter from '@/routers/AuthenticationRouter';
|
import AuthenticationRouter from '@/routers/AuthenticationRouter';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { SiteSettings } from '@/state/settings';
|
import { SiteSettings } from '@/state/settings';
|
||||||
|
import { DefaultTheme, ThemeProvider } from 'styled-components';
|
||||||
|
|
||||||
interface ExtendedWindow extends Window {
|
interface ExtendedWindow extends Window {
|
||||||
SiteConfiguration?: SiteSettings;
|
SiteConfiguration?: SiteSettings;
|
||||||
|
@ -23,6 +24,16 @@ interface ExtendedWindow extends Window {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const theme: DefaultTheme = {
|
||||||
|
breakpoints: {
|
||||||
|
xs: 0,
|
||||||
|
sm: 576,
|
||||||
|
md: 768,
|
||||||
|
lg: 992,
|
||||||
|
xl: 1200,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
|
const { PterodactylUser, SiteConfiguration } = (window as ExtendedWindow);
|
||||||
if (PterodactylUser && !store.getState().user.data) {
|
if (PterodactylUser && !store.getState().user.data) {
|
||||||
|
@ -43,21 +54,23 @@ const App = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StoreProvider store={store}>
|
<ThemeProvider theme={theme}>
|
||||||
<Provider store={store}>
|
<StoreProvider store={store}>
|
||||||
<Router basename={'/'}>
|
<Provider store={store}>
|
||||||
<div className={'mx-auto w-auto'}>
|
<Router basename={'/'}>
|
||||||
<BrowserRouter basename={'/'}>
|
<div className={'mx-auto w-auto'}>
|
||||||
<Switch>
|
<BrowserRouter basename={'/'}>
|
||||||
<Route path="/server/:id" component={ServerRouter}/>
|
<Switch>
|
||||||
<Route path="/auth" component={AuthenticationRouter}/>
|
<Route path="/server/:id" component={ServerRouter}/>
|
||||||
<Route path="/" component={DashboardRouter}/>
|
<Route path="/auth" component={AuthenticationRouter}/>
|
||||||
</Switch>
|
<Route path="/" component={DashboardRouter}/>
|
||||||
</BrowserRouter>
|
</Switch>
|
||||||
</div>
|
</BrowserRouter>
|
||||||
</Router>
|
</div>
|
||||||
</Provider>
|
</Router>
|
||||||
</StoreProvider>
|
</Provider>
|
||||||
|
</StoreProvider>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,38 @@ import * as React from 'react';
|
||||||
import ContentBox from '@/components/elements/ContentBox';
|
import ContentBox from '@/components/elements/ContentBox';
|
||||||
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
|
||||||
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
|
||||||
|
import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { breakpoint } from 'styled-components-breakpoint';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
${tw`flex flex-wrap my-10`};
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
${tw`w-full`};
|
||||||
|
|
||||||
|
${breakpoint('md')`
|
||||||
|
width: calc(50% - 1rem);
|
||||||
|
`}
|
||||||
|
|
||||||
|
${breakpoint('xl')`
|
||||||
|
${tw`w-auto flex-1`};
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<div className={'flex my-10'}>
|
<Container>
|
||||||
<ContentBox className={'flex-1 mr-4'} title={'Update Password'} showFlashes={'account:password'}>
|
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
|
||||||
<UpdatePasswordForm/>
|
<UpdatePasswordForm/>
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
<ContentBox className={'flex-1 ml-4'} title={'Update Email Address'} showFlashes={'account:email'}>
|
<ContentBox className={'mt-8 md:mt-0 md:ml-8'} title={'Update Email Address'} showFlashes={'account:email'}>
|
||||||
<UpdateEmailAddressForm/>
|
<UpdateEmailAddressForm/>
|
||||||
</ContentBox>
|
</ContentBox>
|
||||||
</div>
|
<ContentBox className={'xl:ml-8 mt-8 xl:mt-0'} title={'Configure Two Factor'}>
|
||||||
|
<ConfigureTwoFactorForm/>
|
||||||
|
</ContentBox>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useStoreState } from 'easy-peasy';
|
||||||
|
import { ApplicationStore } from '@/state';
|
||||||
|
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const user = useStoreState((state: ApplicationStore) => state.user.data!);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
return user.useTotp ?
|
||||||
|
<div>
|
||||||
|
<p className={'text-sm'}>
|
||||||
|
Two-factor authentication is currently enabled on your account.
|
||||||
|
</p>
|
||||||
|
<div className={'mt-6'}>
|
||||||
|
<button className={'btn btn-red btn-secondary btn-sm'}>
|
||||||
|
Disable
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
:
|
||||||
|
<div>
|
||||||
|
<SetupTwoFactorModal visible={visible} onDismissed={() => setVisible(false)}/>
|
||||||
|
<p className={'text-sm'}>
|
||||||
|
You do not currently have two-factor authentication enabled on your account. Click
|
||||||
|
the button below to begin configuring it.
|
||||||
|
</p>
|
||||||
|
<div className={'mt-6'}>
|
||||||
|
<button
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
className={'btn btn-green btn-secondary btn-sm'}
|
||||||
|
>
|
||||||
|
Begin Setup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
;
|
||||||
|
};
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
|
||||||
|
import { Form, Formik, FormikActions } from 'formik';
|
||||||
|
import { object, string } from 'yup';
|
||||||
|
import Field from '@/components/elements/Field';
|
||||||
|
import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl';
|
||||||
|
import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor';
|
||||||
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import { Actions, useStoreActions } from 'easy-peasy';
|
||||||
|
import { ApplicationStore } from '@/state';
|
||||||
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
|
||||||
|
interface Values {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ visible, onDismissed }: RequiredModalProps) => {
|
||||||
|
const [ token, setToken ] = useState('');
|
||||||
|
const [ loading, setLoading ] = useState(true);
|
||||||
|
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) {
|
||||||
|
clearFlashes('account:two-factor');
|
||||||
|
getTwoFactorTokenUrl()
|
||||||
|
.then(setToken)
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ visible ]);
|
||||||
|
|
||||||
|
const submit = ({ code }: Values, { resetForm, setSubmitting }: FormikActions<Values>) => {
|
||||||
|
clearFlashes('account:two-factor');
|
||||||
|
enableAccountTwoFactor(code)
|
||||||
|
.then(() => {
|
||||||
|
resetForm();
|
||||||
|
setToken('');
|
||||||
|
setLoading(true);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
addError({ message: httpErrorToHuman(error), key: 'account:two-factor' });
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
onSubmit={submit}
|
||||||
|
initialValues={{ code: '' }}
|
||||||
|
validationSchema={object().shape({
|
||||||
|
code: string()
|
||||||
|
.required('You must provide an authentication code to continue.')
|
||||||
|
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, isValid, resetForm }) => (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
onDismissed={() => {
|
||||||
|
resetForm();
|
||||||
|
setToken('');
|
||||||
|
setLoading(true);
|
||||||
|
onDismissed();
|
||||||
|
}}
|
||||||
|
dismissable={!isSubmitting}
|
||||||
|
showSpinnerOverlay={loading || isSubmitting}
|
||||||
|
>
|
||||||
|
<Form className={'mb-0'}>
|
||||||
|
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
|
||||||
|
<div className={'flex'}>
|
||||||
|
<div className={'flex-1'}>
|
||||||
|
<div className={'w-64 h-64 bg-neutral-600 p-2 rounded'}>
|
||||||
|
{!token || !token.length ?
|
||||||
|
<img
|
||||||
|
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
|
||||||
|
className={'w-64 h-64 rounded'}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<img
|
||||||
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
className={'w-full h-full shadow-none rounded-0'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'flex-1 flex flex-col'}>
|
||||||
|
<div className={'flex-1'}>
|
||||||
|
<Field
|
||||||
|
id={'code'}
|
||||||
|
name={'code'}
|
||||||
|
type={'text'}
|
||||||
|
title={'Code From Authenticator'}
|
||||||
|
description={'Enter the code from your authenticator device after scanning the QR image.'}
|
||||||
|
autoFocus={!loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'text-right'}>
|
||||||
|
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
|
||||||
|
Setup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
};
|
2
resources/scripts/gloabl.d.ts
vendored
2
resources/scripts/gloabl.d.ts
vendored
|
@ -1 +1 @@
|
||||||
declare function tw(a: TemplateStringsArray | string): any;
|
declare function tw (a: TemplateStringsArray | string): any;
|
||||||
|
|
17
resources/scripts/style.d.ts
vendored
Normal file
17
resources/scripts/style.d.ts
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Breakpoints, css, DefaultTheme, StyledProps } from 'styled-components';
|
||||||
|
|
||||||
|
declare module 'styled-components' {
|
||||||
|
type Breakpoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|
||||||
|
export interface DefaultTheme {
|
||||||
|
breakpoints: {
|
||||||
|
[name in 'xs' | 'sm' | 'md' | 'lg' | 'xl']: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'styled-components-breakpoint' {
|
||||||
|
type CSSFunction = (...params: Parameters<typeof css>) => <P extends object>({ theme }: StyledProps<P>) => ReturnType<typeof css>;
|
||||||
|
|
||||||
|
export const breakpoint: (breakpointA: Breakpoints, breakpointB?: Breakpoints) => CSSFunction;
|
||||||
|
}
|
|
@ -145,32 +145,29 @@ a.btn {
|
||||||
@apply .rounded .p-2 .uppercase .tracking-wide .text-sm;
|
@apply .rounded .p-2 .uppercase .tracking-wide .text-sm;
|
||||||
transition: all 150ms linear;
|
transition: all 150ms linear;
|
||||||
|
|
||||||
/**
|
&.btn-secondary {
|
||||||
* Button Colors
|
@apply .border .border-neutral-600 .bg-transparent .text-neutral-200;
|
||||||
*/
|
|
||||||
&.btn-primary {
|
|
||||||
@apply .bg-primary-500 .border-primary-600 .border .text-primary-50;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
@apply .bg-primary-600 .border-primary-700;
|
@apply .border-neutral-500 .text-neutral-100;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-green {
|
&.btn-red:hover:not(:disabled) {
|
||||||
@apply .bg-green-500 .border-green-600 .border .text-green-50;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
@apply .bg-green-600 .border-green-700;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.btn-red {
|
|
||||||
&:not(.btn-secondary) {
|
|
||||||
@apply .bg-red-500 .border-red-600 .text-red-50;
|
@apply .bg-red-500 .border-red-600 .text-red-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-green:hover:not(:disabled) {
|
||||||
|
@apply .bg-green-500 .border-green-600 .text-green-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
&:not(.btn-secondary) {
|
||||||
|
@apply .bg-primary-500 .border-primary-600 .border .text-primary-50;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
@apply .bg-red-600 .border-red-700;
|
@apply .bg-primary-600 .border-primary-700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,16 +179,24 @@ a.btn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-secondary {
|
&.btn-green {
|
||||||
@apply .border .border-neutral-600 .bg-transparent .text-neutral-200;
|
&:not(.btn-secondary) {
|
||||||
|
@apply .bg-green-500 .border-green-600 .border .text-green-50;
|
||||||
&:hover:not(:disabled) {
|
|
||||||
@apply .border-neutral-500 .text-neutral-100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.btn-red:hover:not(:disabled) {
|
&:hover:not(:disabled), &.btn-secondary:active:not(:disabled) {
|
||||||
|
@apply .bg-green-600 .border-green-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-red {
|
||||||
|
&:not(.btn-secondary) {
|
||||||
@apply .bg-red-500 .border-red-600 .text-red-50;
|
@apply .bg-red-500 .border-red-600 .text-red-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled), &.btn-secondary:active:not(:disabled) {
|
||||||
|
@apply .bg-red-600 .border-red-700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,6 +16,9 @@ Route::get('/permissions', 'ClientController@permissions');
|
||||||
|
|
||||||
Route::group(['prefix' => '/account'], function () {
|
Route::group(['prefix' => '/account'], function () {
|
||||||
Route::get('/', 'AccountController@index')->name('api.client.account');
|
Route::get('/', 'AccountController@index')->name('api.client.account');
|
||||||
|
Route::get('/two-factor', 'TwoFactorController@index');
|
||||||
|
Route::post('/two-factor', 'TwoFactorController@store');
|
||||||
|
Route::delete('/two-factor', 'TwoFactorController@delete');
|
||||||
|
|
||||||
Route::put('/email', 'AccountController@updateEmail')->name('api.client.account.update-email');
|
Route::put('/email', 'AccountController@updateEmail')->name('api.client.account.update-email');
|
||||||
Route::put('/password', 'AccountController@updatePassword')->name('api.client.account.update-password');
|
Route::put('/password', 'AccountController@updatePassword')->name('api.client.account.update-password');
|
||||||
|
|
|
@ -6857,6 +6857,10 @@ style-loader@^0.23.1:
|
||||||
loader-utils "^1.1.0"
|
loader-utils "^1.1.0"
|
||||||
schema-utils "^1.0.0"
|
schema-utils "^1.0.0"
|
||||||
|
|
||||||
|
styled-components-breakpoint@^3.0.0-preview.20:
|
||||||
|
version "3.0.0-preview.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/styled-components-breakpoint/-/styled-components-breakpoint-3.0.0-preview.20.tgz#877e88a00c0cf66976f610a1d347839a1a0b6d70"
|
||||||
|
|
||||||
styled-components@^4.4.1:
|
styled-components@^4.4.1:
|
||||||
version "4.4.1"
|
version "4.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-4.4.1.tgz#e0631e889f01db67df4de576fedaca463f05c2f2"
|
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-4.4.1.tgz#e0631e889f01db67df4de576fedaca463f05c2f2"
|
||||||
|
|
Loading…
Reference in a new issue