Show console when an admin is viewing an installing server

This commit is contained in:
Dane Everitt 2020-04-26 13:21:39 -07:00
parent 446dc8b33d
commit 6056b6f45d
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
4 changed files with 130 additions and 88 deletions

View File

@ -17,6 +17,16 @@ class AuthenticateServerAccess
*/ */
private $repository; private $repository;
/**
* Routes that this middleware should not apply to if the user is an admin.
*
* @var string[]
*/
protected $except = [
'api:client:server.view',
'api:client:server.ws',
];
/** /**
* AuthenticateServerAccess constructor. * AuthenticateServerAccess constructor.
* *
@ -36,6 +46,8 @@ class AuthenticateServerAccess
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
/** @var \Pterodactyl\Models\User $user */
$user = $request->user();
$server = $request->route()->parameter('server'); $server = $request->route()->parameter('server');
if (! $server instanceof Server) { if (! $server instanceof Server) {
@ -45,9 +57,9 @@ class AuthenticateServerAccess
// At the very least, ensure that the user trying to make this request is the // At the very least, ensure that the user trying to make this request is the
// server owner, a subuser, or a root admin. We'll leave it up to the controllers // server owner, a subuser, or a root admin. We'll leave it up to the controllers
// to authenticate more detailed permissions if needed. // to authenticate more detailed permissions if needed.
if ($request->user()->id !== $server->owner_id && ! $request->user()->root_admin) { if ($user->id !== $server->owner_id && ! $user->root_admin) {
// Check for subuser status. // Check for subuser status.
if (! $server->subusers->contains('user_id', $request->user()->id)) { if (! $server->subusers->contains('user_id', $user->id)) {
throw new NotFoundHttpException(trans('exceptions.api.resource_not_found')); throw new NotFoundHttpException(trans('exceptions.api.resource_not_found'));
} }
} }
@ -57,7 +69,11 @@ class AuthenticateServerAccess
} }
if (! $server->isInstalled()) { if (! $server->isInstalled()) {
throw new ConflictHttpException('Server has not completed the installation process.'); // Throw an exception for all server routes; however if the user is an admin and requesting the
// server details, don't throw the exception for them.
if (! $user->root_admin || ($user->root_admin && ! $request->routeIs($this->except))) {
throw new ConflictHttpException('Server has not completed the installation process.');
}
} }
$request->attributes->set('server', $server); $request->attributes->set('server', $server);

View File

@ -12,6 +12,7 @@ import SuspenseSpinner from '@/components/elements/SuspenseSpinner';
import TitledGreyBox from '@/components/elements/TitledGreyBox'; import TitledGreyBox from '@/components/elements/TitledGreyBox';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import PageContentBlock from '@/components/elements/PageContentBlock'; import PageContentBlock from '@/components/elements/PageContentBlock';
import ContentContainer from '@/components/elements/ContentContainer';
type PowerAction = 'start' | 'stop' | 'restart' | 'kill'; type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
@ -123,36 +124,47 @@ export default () => {
<span className={'text-neutral-500'}> / {server.limits.disk} MB</span> <span className={'text-neutral-500'}> / {server.limits.disk} MB</span>
</p> </p>
</TitledGreyBox> </TitledGreyBox>
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny={true}> {!server.isInstalling ?
<div className={'grey-box justify-center'}> <Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny={true}>
<Can action={'control.start'}> <div className={'grey-box justify-center'}>
<button <Can action={'control.start'}>
className={'btn btn-secondary btn-green btn-xs mr-2'} <button
disabled={status !== 'offline'} className={'btn btn-secondary btn-green btn-xs mr-2'}
onClick={e => { disabled={status !== 'offline'}
e.preventDefault(); onClick={e => {
sendPowerCommand('start'); e.preventDefault();
}} sendPowerCommand('start');
> }}
Start >
</button> Start
</Can> </button>
<Can action={'control.restart'}> </Can>
<button <Can action={'control.restart'}>
className={'btn btn-secondary btn-primary btn-xs mr-2'} <button
onClick={e => { className={'btn btn-secondary btn-primary btn-xs mr-2'}
e.preventDefault(); onClick={e => {
sendPowerCommand('restart'); e.preventDefault();
}} sendPowerCommand('restart');
> }}
Restart >
</button> Restart
</Can> </button>
<Can action={'control.stop'}> </Can>
<StopOrKillButton onPress={action => sendPowerCommand(action)}/> <Can action={'control.stop'}>
</Can> <StopOrKillButton onPress={action => sendPowerCommand(action)}/>
</Can>
</div>
</Can>
:
<div className={'mt-4 rounded bg-yellow-500 p-3'}>
<ContentContainer>
<p className={'text-sm text-yellow-900'}>
This server is currently running its installation process and most actions are
unavailable.
</p>
</ContentContainer>
</div> </div>
</Can> }
</div> </div>
<div className={'flex-1 ml-4'}> <div className={'flex-1 ml-4'}>
<SuspenseSpinner> <SuspenseSpinner>

View File

@ -20,12 +20,16 @@ import Spinner from '@/components/elements/Spinner';
import ServerError from '@/components/screens/ServerError'; import ServerError from '@/components/screens/ServerError';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import NotFound from '@/components/screens/NotFound'; import NotFound from '@/components/screens/NotFound';
import { useStoreState } from 'easy-peasy';
import ServerInstallingBar from '@/components/elements/ServerInstallingBar';
import useServer from '@/plugins/useServer';
import ScreenBlock from '@/components/screens/ScreenBlock'; import ScreenBlock from '@/components/screens/ScreenBlock';
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const { rootAdmin } = useStoreState(state => state.user.data!);
const [ error, setError ] = useState(''); const [ error, setError ] = useState('');
const [ installing, setInstalling ] = useState(false); const [ installing, setInstalling ] = useState(false);
const server = ServerContext.useStoreState(state => state.server.data); const server = useServer();
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState); const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
@ -33,6 +37,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
clearServerState(); clearServerState();
}, []); }, []);
useEffect(() => {
setInstalling(server?.isInstalling !== false);
}, [ server?.isInstalling ]);
useEffect(() => { useEffect(() => {
setError(''); setError('');
setInstalling(false); setInstalling(false);
@ -55,19 +63,12 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
<React.Fragment key={'server-router'}> <React.Fragment key={'server-router'}>
<NavigationBar/> <NavigationBar/>
{!server ? {!server ?
!installing ? error ?
error ? <ServerError message={error}/>
<ServerError message={error}/>
:
<div className={'flex justify-center m-20'}>
<Spinner size={'large'}/>
</div>
: :
<ScreenBlock <div className={'flex justify-center m-20'}>
title={'Your server is installing.'} <Spinner size={'large'}/>
image={'/assets/svgs/server_installing.svg'} </div>
message={'Please check back in a few minutes.'}
/>
: :
<> <>
<CSSTransition timeout={250} classNames={'fade'} appear={true} in={true}> <CSSTransition timeout={250} classNames={'fade'} appear={true} in={true}>
@ -95,29 +96,43 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
</div> </div>
</div> </div>
</CSSTransition> </CSSTransition>
<WebsocketHandler/> {(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
<TransitionRouter> <ScreenBlock
<Switch location={location}> title={'Your server is installing.'}
<Route path={`${match.path}`} component={ServerConsole} exact/> image={'/assets/svgs/server_installing.svg'}
<Route path={`${match.path}/files`} component={FileManagerContainer} exact/> message={'Please check back in a few minutes.'}
<Route />
path={`${match.path}/files/:action(edit|new)`} :
render={props => ( <>
<SuspenseSpinner> <WebsocketHandler/>
<FileEditContainer {...props as any}/> <TransitionRouter>
</SuspenseSpinner> <Switch location={location}>
)} <Route path={`${match.path}`} component={ServerConsole} exact/>
exact <Route path={`${match.path}/files`} component={FileManagerContainer} exact/>
/> <Route
<Route path={`${match.path}/databases`} component={DatabasesContainer} exact/> path={`${match.path}/files/:action(edit|new)`}
<Route path={`${match.path}/schedules`} component={ScheduleContainer} exact/> render={props => (
<Route path={`${match.path}/schedules/:id`} component={ScheduleEditContainer} exact/> <SuspenseSpinner>
<Route path={`${match.path}/users`} component={UsersContainer} exact/> <FileEditContainer {...props as any}/>
<Route path={`${match.path}/backups`} component={BackupContainer} exact/> </SuspenseSpinner>
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/> )}
<Route path={'*'} component={NotFound}/> exact
</Switch> />
</TransitionRouter> <Route path={`${match.path}/databases`} component={DatabasesContainer} exact/>
<Route path={`${match.path}/schedules`} component={ScheduleContainer} exact/>
<Route
path={`${match.path}/schedules/:id`}
component={ScheduleEditContainer}
exact
/>
<Route path={`${match.path}/users`} component={UsersContainer} exact/>
<Route path={`${match.path}/backups`} component={BackupContainer} exact/>
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/>
<Route path={'*'} component={NotFound}/>
</Switch>
</TransitionRouter>
</>
}
</> </>
} }
</React.Fragment> </React.Fragment>

View File

@ -11,17 +11,17 @@ use Pterodactyl\Http\Middleware\Api\Client\Server\AuthenticateServerAccess;
| Endpoint: /api/client | Endpoint: /api/client
| |
*/ */
Route::get('/', 'ClientController@index')->name('api.client.index'); Route::get('/', 'ClientController@index')->name('api:client.index');
Route::get('/permissions', 'ClientController@permissions'); 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::get('/two-factor', 'TwoFactorController@index');
Route::post('/two-factor', 'TwoFactorController@store'); Route::post('/two-factor', 'TwoFactorController@store');
Route::delete('/two-factor', 'TwoFactorController@delete'); 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');
Route::get('/api-keys', 'ApiKeyController@index'); Route::get('/api-keys', 'ApiKeyController@index');
Route::post('/api-keys', 'ApiKeyController@store'); Route::post('/api-keys', 'ApiKeyController@store');
@ -37,30 +37,29 @@ Route::group(['prefix' => '/account'], function () {
| |
*/ */
Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServerAccess::class]], function () { Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServerAccess::class]], function () {
Route::get('/', 'Servers\ServerController@index')->name('api.client.servers.view'); Route::get('/', 'Servers\ServerController@index')->name('api:client:server.view');
Route::get('/websocket', 'Servers\WebsocketController')->name('api.client.servers.websocket'); Route::get('/websocket', 'Servers\WebsocketController')->name('api:client:server.ws');
Route::get('/resources', 'Servers\ResourceUtilizationController') Route::get('/resources', 'Servers\ResourceUtilizationController')->name('api:client:server.resources');
->name('api.client.servers.resources');
Route::post('/command', 'Servers\CommandController@index')->name('api.client.servers.command'); Route::post('/command', 'Servers\CommandController@index');
Route::post('/power', 'Servers\PowerController@index')->name('api.client.servers.power'); Route::post('/power', 'Servers\PowerController@index');
Route::group(['prefix' => '/databases'], function () { Route::group(['prefix' => '/databases'], function () {
Route::get('/', 'Servers\DatabaseController@index')->name('api.client.servers.databases'); Route::get('/', 'Servers\DatabaseController@index');
Route::post('/', 'Servers\DatabaseController@store'); Route::post('/', 'Servers\DatabaseController@store');
Route::post('/{database}/rotate-password', 'Servers\DatabaseController@rotatePassword'); Route::post('/{database}/rotate-password', 'Servers\DatabaseController@rotatePassword');
Route::delete('/{database}', 'Servers\DatabaseController@delete')->name('api.client.servers.databases.delete'); Route::delete('/{database}', 'Servers\DatabaseController@delete');
}); });
Route::group(['prefix' => '/files'], function () { Route::group(['prefix' => '/files'], function () {
Route::get('/list', 'Servers\FileController@listDirectory')->name('api.client.servers.files.list'); Route::get('/list', 'Servers\FileController@listDirectory');
Route::get('/contents', 'Servers\FileController@getFileContents')->name('api.client.servers.files.contents'); Route::get('/contents', 'Servers\FileController@getFileContents');
Route::get('/download', 'Servers\FileController@download'); Route::get('/download', 'Servers\FileController@download');
Route::put('/rename', 'Servers\FileController@renameFile')->name('api.client.servers.files.rename'); Route::put('/rename', 'Servers\FileController@renameFile');
Route::post('/copy', 'Servers\FileController@copyFile')->name('api.client.servers.files.copy'); Route::post('/copy', 'Servers\FileController@copyFile');
Route::post('/write', 'Servers\FileController@writeFileContents')->name('api.client.servers.files.write'); Route::post('/write', 'Servers\FileController@writeFileContents');
Route::post('/delete', 'Servers\FileController@delete')->name('api.client.servers.files.delete'); Route::post('/delete', 'Servers\FileController@delete');
Route::post('/create-folder', 'Servers\FileController@createFolder')->name('api.client.servers.files.create-folder'); Route::post('/create-folder', 'Servers\FileController@createFolder');
}); });
Route::group(['prefix' => '/schedules'], function () { Route::group(['prefix' => '/schedules'], function () {
@ -76,7 +75,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
}); });
Route::group(['prefix' => '/network'], function () { Route::group(['prefix' => '/network'], function () {
Route::get('/', 'Servers\NetworkController@index')->name('api.client.servers.network'); Route::get('/', 'Servers\NetworkController@index');
}); });
Route::group(['prefix' => '/users'], function () { Route::group(['prefix' => '/users'], function () {