Merge pull request #29 from Pterodactyl/add-restful-api

Add initial API Implementation
This commit is contained in:
Dane Everitt 2016-01-16 00:32:06 -05:00
commit 7670cf1466
47 changed files with 1522 additions and 462 deletions

View file

@ -22,3 +22,7 @@ MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
API_PREFIX=api
API_VERSION=v1
API_DEBUG=false

View file

@ -55,7 +55,7 @@ class Handler extends ExceptionHandler
$e = new NotFoundHttpException($e->getMessage(), $e);
}
if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('api/*') || $request->is('remote/*')) {
if ($request->isXmlHttpRequest() || $request->ajax() || $request->is('remote/*')) {
$exception = 'An exception occured while attempting to perform this action, please try again.';

View file

@ -0,0 +1,11 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Dingo\Api\Routing\Helpers;
use Illuminate\Routing\Controller;
class BaseController extends Controller
{
use Helpers;
}

View file

@ -0,0 +1,43 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use DB;
use Illuminate\Http\Request;
use Pterodactyl\Models\Location;
/**
* @Resource("Servers")
*/
class LocationController extends BaseController
{
public function __construct()
{
//
}
/**
* List All Locations
*
* Lists all locations currently on the system.
*
* @Get("/locations")
* @Versions({"v1"})
* @Response(200)
*/
public function getLocations(Request $request)
{
$locations = Location::select('locations.*', DB::raw('GROUP_CONCAT(nodes.id) as nodes'))
->join('nodes', 'locations.id', '=', 'nodes.location')
->groupBy('locations.id')
->get();
foreach($locations as &$location) {
$location->nodes = explode(',', $location->nodes);
}
return $locations;
}
}

View file

@ -0,0 +1,177 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Illuminate\Http\Request;
use Pterodactyl\Models;
use Pterodactyl\Transformers\NodeTransformer;
use Pterodactyl\Repositories\NodeRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Dingo\Api\Exception\ResourceException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* @Resource("Servers")
*/
class NodeController extends BaseController
{
public function __construct()
{
//
}
/**
* List All Nodes
*
* Lists all nodes currently on the system.
*
* @Get("/nodes/{?page}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(200)
*/
public function getNodes(Request $request)
{
$nodes = Models\Node::paginate(50);
return $this->response->paginator($nodes, new NodeTransformer);
}
/**
* Create a New Node
*
* @Post("/nodes")
* @Versions({"v1"})
* @Transaction({
* @Request({
* 'name' => 'My API Node',
* 'location' => 1,
* 'public' => 1,
* 'fqdn' => 'daemon.wuzzle.woo',
* 'scheme' => 'https',
* 'memory' => 10240,
* 'memory_overallocate' => 100,
* 'disk' => 204800,
* 'disk_overallocate' => -1,
* 'daemonBase' => '/srv/daemon-data',
* 'daemonSFTP' => 2022,
* 'daemonListen' => 8080
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(201),
* @Response(422, body={
* "message": "A validation error occured.",
* "errors": {},
* "status_code": 422
* }),
* @Response(503, body={
* "message": "There was an error while attempting to add this node to the system.",
* "status_code": 503
* })
* })
*/
public function postNode(Request $request)
{
try {
$node = new NodeRepository;
$new = $node->create($request->all());
return $this->response->created(route('api.nodes.view', [
'id' => $new
]));
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $e) {
throw new BadRequestHttpException('There was an error while attempting to add this node to the system.');
}
}
/**
* List Specific Node
*
* Lists specific fields about a server or all fields pertaining to that node.
*
* @Get("/nodes/{id}/{?fields}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the node to get information on."),
* @Parameter("fields", type="string", required=false, description="A comma delimidated list of fields to include.")
* })
* @Response(200)
*/
public function getNode(Request $request, $id, $fields = null)
{
$query = Models\Node::where('id', $id);
if (!is_null($request->input('fields'))) {
foreach(explode(',', $request->input('fields')) as $field) {
if (!empty($field)) {
$query->addSelect($field);
}
}
}
try {
if (!$query->first()) {
throw new NotFoundHttpException('No node by that ID was found.');
}
return $query->first();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
}
}
/**
* List Node Allocations
*
* Returns a listing of all node allocations.
*
* @Get("/nodes/{id}/allocations")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the node to get allocations for."),
* })
* @Response(200)
*/
public function getNodeAllocations(Request $request, $id)
{
$allocations = Models\Allocation::select('ip', 'port', 'assigned_to')->where('node', $id)->orderBy('ip', 'asc')->orderBy('port', 'asc')->get();
if ($allocations->count() < 1) {
throw new NotFoundHttpException('No allocations where found for the requested node.');
}
return $allocations;
}
/**
* Delete Node
*
* @Delete("/nodes/{id}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the node."),
* })
* @Response(204)
*/
public function deleteNode(Request $request, $id)
{
try {
$node = new NodeRepository;
$node->delete($id);
return $this->response->noContent();
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch(\Exception $e) {
throw new ServiceUnavailableHttpException('An error occured while attempting to delete this node.');
}
}
}

View file

@ -0,0 +1,179 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Illuminate\Http\Request;
use Pterodactyl\Models;
use Pterodactyl\Transformers\ServerTransformer;
use Pterodactyl\Repositories\ServerRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Dingo\Api\Exception\ResourceException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* @Resource("Servers")
*/
class ServerController extends BaseController
{
public function __construct()
{
//
}
/**
* List All Servers
*
* Lists all servers currently on the system.
*
* @Get("/servers/{?page}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(200)
*/
public function getServers(Request $request)
{
$servers = Models\Server::paginate(50);
return $this->response->paginator($servers, new ServerTransformer);
}
/**
* Create Server
*
* @Post("/servers")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(201)
*/
public function postServer(Request $request)
{
try {
$server = new ServerRepository;
$new = $server->create($request->all());
return $this->response->created(route('api.servers.view', [
'id' => $new
]));
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $e) {
throw new BadRequestHttpException('There was an error while attempting to add this server to the system.');
}
}
/**
* List Specific Server
*
* Lists specific fields about a server or all fields pertaining to that server.
*
* @Get("/servers/{id}{?fields}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server to get information on."),
* @Parameter("fields", type="string", required=false, description="A comma delimidated list of fields to include.")
* })
* @Response(200)
*/
public function getServer(Request $request, $id)
{
$query = Models\Server::where('id', $id);
if (!is_null($request->input('fields'))) {
foreach(explode(',', $request->input('fields')) as $field) {
if (!empty($field)) {
$query->addSelect($field);
}
}
}
try {
if (!$query->first()) {
throw new NotFoundHttpException('No server by that ID was found.');
}
return $query->first();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
}
}
/**
* Suspend Server
*
* @Post("/servers/{id}/suspend")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server."),
* })
* @Response(204)
*/
public function postServerSuspend(Request $request, $id)
{
try {
$server = new ServerRepository;
$server->suspend($id);
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('An error occured while attempting to suspend this server instance.');
}
}
/**
* Unsuspend Server
*
* @Post("/servers/{id}/unsuspend")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server."),
* })
* @Response(204)
*/
public function postServerUnsuspend(Request $request, $id)
{
try {
$server = new ServerRepository;
$server->unsuspend($id);
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('An error occured while attempting to unsuspend this server instance.');
}
}
/**
* Delete Server
*
* @Delete("/servers/{id}/{force}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the server."),
* @Parameter("force", type="string", required=false, description="Use 'force' if the server should be removed regardless of daemon response."),
* })
* @Response(204)
*/
public function deleteServer(Request $request, $id, $force = null)
{
try {
$server = new ServerRepository;
$server->deleteServer($id, $force);
return $this->response->noContent();
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch(\Exception $e) {
throw new ServiceUnavailableHttpException('An error occured while attempting to delete this server.');
}
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Pterodactyl\Http\Controllers\API;
use Illuminate\Http\Request;
use Pterodactyl\Models;
use Pterodactyl\Transformers\ServiceTransformer;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @Resource("Services")
*/
class ServiceController extends BaseController
{
public function __construct()
{
//
}
public function getServices(Request $request)
{
return Models\Service::all();
}
public function getService(Request $request, $id)
{
$service = Models\Service::find($id);
if (!$service) {
throw new NotFoundHttpException('No service by that ID was found.');
}
$options = Models\ServiceOptions::select('id', 'name', 'description', 'tag', 'docker_image')->where('parent_service', $service->id)->get();
foreach($options as &$opt) {
$opt->variables = Models\ServiceVariables::where('option_id', $opt->id)->get();
}
return [
'service' => $service,
'options' => $options
];
}
}

View file

@ -2,82 +2,180 @@
namespace Pterodactyl\Http\Controllers\API;
use Gate;
use Log;
use Debugbar;
use Pterodactyl\Models\API;
use Pterodactyl\Models\User;
use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request;
class UserController extends Controller
use Dingo\Api\Exception\ResourceException;
use Pterodactyl\Models;
use Pterodactyl\Transformers\UserTransformer;
use Pterodactyl\Repositories\UserRepository;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
/**
* @Resource("Users")
*/
class UserController extends BaseController
{
/**
* Constructor
* List All Users
*
* Lists all users currently on the system.
*
* @Get("/users/{?page}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("page", type="integer", description="The page of results to view.", default=1)
* })
* @Response(200)
*/
public function __construct()
public function getUsers(Request $request)
{
//
}
public function getAllUsers(Request $request)
{
// Policies don't work if the user isn't logged in for whatever reason in Laravel...
if(!API::checkPermission($request->header('X-Authorization'), 'get-users')) {
return API::noPermissionError();
}
return response()->json([
'users' => User::all()
]);
$users = Models\User::paginate(50);
return $this->response->paginator($users, new UserTransformer);
}
/**
* Returns JSON response about a user given their ID.
* If fields are provided only those fields are returned.
* List Specific User
*
* Does not return protected fields (i.e. password & totp_secret)
* Lists specific fields about a user or all fields pertaining to that user.
*
* @param Request $request
* @param int $id
* @param string $fields
* @return Response
* @Get("/users/{id}/{fields}")
* @Versions({"v1"})
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the user to get information on."),
* @Parameter("fields", type="string", required=false, description="A comma delimidated list of fields to include.")
* })
* @Response(200)
*/
public function getUser(Request $request, $id, $fields = null)
public function getUser(Request $request, $id)
{
$query = Models\User::where('id', $id);
// Policies don't work if the user isn't logged in for whatever reason in Laravel...
if(!API::checkPermission($request->header('X-Authorization'), 'get-users')) {
return API::noPermissionError();
}
if (is_null($fields)) {
return response()->json(User::find($id));
}
$query = User::where('id', $id);
$explode = explode(',', $fields);
foreach($explode as &$exploded) {
if(!empty($exploded)) {
$query->addSelect($exploded);
if (!is_null($request->input('fields'))) {
foreach(explode(',', $request->input('fields')) as $field) {
if (!empty($field)) {
$query->addSelect($field);
}
}
}
try {
return response()->json($query->get());
} catch (\Exception $e) {
if ($e instanceof \Illuminate\Database\QueryException) {
return response()->json([
'error' => 'One of the fields provided in your argument list is invalid.'
], 500);
if (!$query->first()) {
throw new NotFoundHttpException('No user by that ID was found.');
}
throw $e;
return $query->first();
} catch (NotFoundHttpException $ex) {
throw $ex;
} catch (\Exception $ex) {
throw new BadRequestHttpException('There was an issue with the fields passed in the request.');
}
}
/**
* Create a New User
*
* @Post("/users")
* @Versions({"v1"})
* @Transaction({
* @Request({
* "email": "foo@example.com",
* "password": "foopassword",
* "admin": false
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(201),
* @Response(422, body={
* "message": "A validation error occured.",
* "errors": {
* "email": {"The email field is required."},
* "password": {"The password field is required."},
* "admin": {"The admin field is required."}
* },
* "status_code": 422
* })
* })
*/
public function postUser(Request $request)
{
try {
$user = new UserRepository;
$create = $user->create($request->input('email'), $request->input('password'), $request->input('admin'));
return $this->response->created(route('api.users.view', [
'id' => $create
]));
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('Unable to create a user on the system due to an error.');
}
}
/**
* Update an Existing User
*
* The data sent in the request will be used to update the existing user on the system.
*
* @Patch("/users/{id}")
* @Versions({"v1"})
* @Transaction({
* @Request({
* "email": "new@email.com"
* }, headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(200, body={"email": "new@email.com"}),
* @Response(422)
* })
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the user to modify.")
* })
*/
public function patchUser(Request $request, $id)
{
try {
$user = new UserRepository;
$user->update($id, $request->all());
return Models\User::findOrFail($id);
} catch (DisplayValidationException $ex) {
throw new ResourceException('A validation error occured.', json_decode($ex->getMessage(), true));
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('Unable to update a user on the system due to an error.');
}
}
/**
* Delete a User
*
* @Delete("/users/{id}")
* @Versions({"v1"})
* @Transaction({
* @Request(headers={"Authorization": "Bearer <jwt-token>"}),
* @Response(204),
* @Response(422)
* })
* @Parameters({
* @Parameter("id", type="integer", required=true, description="The ID of the user to delete.")
* })
*/
public function deleteUser(Request $request, $id)
{
try {
$user = new UserRepository;
$user->delete($id);
return $this->response->noContent();
} catch (DisplayException $ex) {
throw new ResourceException($ex->getMessage());
} catch (\Exception $ex) {
throw new ServiceUnavailableHttpException('Unable to delete this user due to an error.');
}
}
}

View file

@ -62,27 +62,18 @@ class AccountsController extends Controller
public function postNew(Request $request)
{
$this->validate($request, [
'email' => 'required|email|unique:users,email',
'password' => 'required|confirmed|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})'
]);
try {
$user = new UserRepository;
$userid = $user->create($request->input('username'), $request->input('email'), $request->input('password'));
if (!$userid) {
throw new \Exception('Unable to create user, response was not an integer.');
}
Alert::success('Account has been successfully created.')->flash();
return redirect()->route('admin.accounts.view', ['id' => $userid]);
} catch (\Exception $e) {
Log::error($e);
} catch (\Pterodactyl\Exceptions\DisplayValidationException $ex) {
return redirect()->route('admin.nodes.view', $id)->withErrors(json_decode($e->getMessage()))->withInput();
} catch (\Exception $ex) {
Log::error($ex);
Alert::danger('An error occured while attempting to add a new user. ' . $e->getMessage())->flash();
return redirect()->route('admin.accounts.new');
}
}
public function postUpdate(Request $request)

View file

@ -17,7 +17,6 @@ class Kernel extends HttpKernel
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
];
/**
@ -30,7 +29,7 @@ class Kernel extends HttpKernel
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class,
'server' => \Pterodactyl\Http\Middleware\CheckServer::class,
'api' => \Pterodactyl\Http\Middleware\APIAuthenticate::class,
'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class,
'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class,
];
}

View file

@ -1,46 +0,0 @@
<?php
namespace Pterodactyl\Http\Middleware;
use Closure;
use Debugbar;
use Pterodactyl\Models\API;
class APIAuthenticate
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if(!$request->header('X-Authorization')) {
return response()->json([
'error' => 'Authorization header was missing with this request. Please pass the \'X-Authorization\' header with your request.'
], 403);
}
$api = API::where('key', $request->header('X-Authorization'))->first();
if (!$api) {
return response()->json([
'error' => 'Invalid API key was provided in the request.'
], 403);
}
if (!is_null($api->allowed_ips)) {
if (!in_array($request->ip(), json_decode($api->allowed_ips, true))) {
return response()->json([
'error' => 'This IP (' . $request->ip() . ') is not permitted to access the API with that token.'
], 403);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace Pterodactyl\Http\Middleware;
use Pterodactyl\Models\APIKey;
use Pterodactyl\Models\APIPermission;
use Illuminate\Http\Request;
use Dingo\Api\Routing\Route;
use Dingo\Api\Auth\Provider\Authorization;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; // 400
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; // 401
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; // 403
class APISecretToken extends Authorization
{
protected $algo = 'sha256';
protected $permissionAllowed = false;
public function __construct()
{
//
}
public function getAuthorizationMethod()
{
return 'Authorization';
}
public function authenticate(Request $request, Route $route)
{
if (!$request->bearerToken() || empty($request->bearerToken())) {
throw new UnauthorizedHttpException('The authentication header was missing or malformed');
}
list($public, $hashed) = explode('.', $request->bearerToken());
$key = APIKey::where('public', $public)->first();
if (!$key) {
throw new AccessDeniedHttpException('Invalid API Key.');
}
// Check for Resource Permissions
if (!empty($request->route()->getName())) {
if(!is_null($key->allowed_ips)) {
if (!in_array($request->ip(), json_decode($key->allowed_ips))) {
throw new AccessDeniedHttpException('This IP address does not have permission to use this API key.');
}
}
foreach(APIPermission::where('key_id', $key->id)->get() as &$row) {
if ($row->permission === '*' || $row->permission === $request->route()->getName()) {
$this->permissionAllowed = true;
continue;
}
}
if (!$this->permissionAllowed) {
throw new AccessDeniedHttpException('You do not have permission to access this resource.');
}
}
if($this->_generateHMAC($request->fullUrl(), $request->getContent(), $key->secret) !== base64_decode($hashed)) {
throw new BadRequestHttpException('The hashed body was not valid. Potential modification of contents in route.');
}
return true;
}
protected function _generateHMAC($url, $body, $key)
{
$data = urldecode($url) . '.' . $body;
return hash_hmac($this->algo, $data, $key, true);
}
}

View file

@ -0,0 +1,129 @@
<?php
namespace Pterodactyl\Http\Routes;
use Pterodactyl\Models;
use Illuminate\Routing\Router;
class APIRoutes
{
public function map(Router $router) {
$api = app('Dingo\Api\Routing\Router');
$api->version('v1', ['middleware' => 'api.auth'], function ($api) {
/**
* User Routes
*/
$api->get('users', [
'as' => 'api.users',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUsers'
]);
$api->post('users', [
'as' => 'api.users.post',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@postUser'
]);
$api->get('users/{id}', [
'as' => 'api.users.view',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUser'
]);
$api->patch('users/{id}', [
'as' => 'api.users.patch',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@patchUser'
]);
$api->delete('users/{id}', [
'as' => 'api.users.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\UserController@deleteUser'
]);
/**
* Server Routes
*/
$api->get('servers', [
'as' => 'api.servers',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@getServers'
]);
$api->post('servers', [
'as' => 'api.servers.post',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@postServer'
]);
$api->get('servers/{id}', [
'as' => 'api.servers.view',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@getServer'
]);
$api->post('servers/{id}/suspend', [
'as' => 'api.servers.suspend',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@postServerSuspend'
]);
$api->post('servers/{id}/unsuspend', [
'as' => 'api.servers.unsuspend',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@postServerUnsuspend'
]);
$api->delete('servers/{id}/{force?}', [
'as' => 'api.servers.delete',
'uses' => 'Pterodactyl\Http\Controllers\API\ServerController@deleteServer'
]);
/**
* Node Routes
*/
$api->get('nodes', [
'as' => 'api.nodes',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@getNodes'
]);
$api->post('nodes', [
'as' => 'api.nodes.post',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@postNode'
]);
$api->get('nodes/{id}', [
'as' => 'api.nodes.view',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@getNode'
]);
$api->get('nodes/{id}/allocations', [
'as' => 'api.nodes.view_allocations',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@getNodeAllocations'
]);
$api->delete('nodes/{id}', [
'as' => 'api.nodes.view',
'uses' => 'Pterodactyl\Http\Controllers\API\NodeController@deleteNode'
]);
/**
* Location Routes
*/
$api->get('locations', [
'as' => 'api.locations',
'uses' => 'Pterodactyl\Http\Controllers\API\LocationController@getLocations'
]);
/**
* Service Routes
*/
$api->get('services', [
'as' => 'api.services',
'uses' => 'Pterodactyl\Http\Controllers\API\ServiceController@getServices'
]);
$api->get('services/{id}', [
'as' => 'api.services.view',
'uses' => 'Pterodactyl\Http\Controllers\API\ServiceController@getService'
]);
});
}
}

View file

@ -13,7 +13,8 @@ class AdminRoutes {
'as' => 'admin.index',
'middleware' => [
'auth',
'admin'
'admin',
'csrf'
],
'uses' => 'Admin\BaseController@getIndex'
]);
@ -22,7 +23,8 @@ class AdminRoutes {
'prefix' => 'admin/accounts',
'middleware' => [
'auth',
'admin'
'admin',
'csrf'
]
], function () use ($router) {
@ -66,7 +68,8 @@ class AdminRoutes {
'prefix' => 'admin/servers',
'middleware' => [
'auth',
'admin'
'admin',
'csrf'
]
], function () use ($router) {
@ -148,7 +151,8 @@ class AdminRoutes {
'prefix' => 'admin/nodes',
'middleware' => [
'auth',
'admin'
'admin',
'csrf'
]
], function () use ($router) {
@ -204,7 +208,8 @@ class AdminRoutes {
'prefix' => 'admin/locations',
'middleware' => [
'auth',
'admin'
'admin',
'csrf'
]
], function () use ($router) {
$router->get('/', [

View file

@ -12,7 +12,8 @@ class AuthRoutes {
$router->group([
'prefix' => 'auth',
'middleware' => [
'guest'
'guest',
'csrf'
]
], function () use ($router) {

View file

@ -31,7 +31,8 @@ class BaseRoutes {
$router->group([
'profix' => 'account',
'middleware' => [
'auth'
'auth',
'csrf'
]
], function () use ($router) {
$router->get('account', [
@ -50,7 +51,8 @@ class BaseRoutes {
$router->group([
'prefix' => 'account/totp',
'middleware' => [
'auth'
'auth',
'csrf'
]
], function () use ($router) {
$router->get('/', [

View file

@ -1,31 +0,0 @@
<?php
namespace Pterodactyl\Http\Routes;
use Illuminate\Routing\Router;
class RestRoutes {
public function map(Router $router) {
$router->group([
'prefix' => 'api/v1',
'middleware' => [
'api'
]
], function () use ($router) {
// Users endpoint for API
$router->group(['prefix' => 'users'], function () use ($router) {
// Returns all users
$router->get('/', [
'uses' => 'API\UserController@getAllUsers'
]);
// Return listing of user [with only specified fields]
$router->get('/{id}/{fields?}', [
'uses' => 'API\UserController@getUser'
])->where('id', '[0-9]+');
});
});
}
}

View file

@ -11,7 +11,8 @@ class ServerRoutes {
'prefix' => 'server/{server}',
'middleware' => [
'auth',
'server'
'server',
'csrf'
]
], function ($server) use ($router) {
// Index View for Server

View file

@ -1,63 +0,0 @@
<?php
namespace Pterodactyl\Models;
use Log;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Models\APIPermission;
use Illuminate\Database\Eloquent\Model;
class API extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'api';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = ['daemonSecret'];
public function permissions()
{
return $this->hasMany(APIPermission::class);
}
public static function findKey($key)
{
return self::where('key', $key)->first();
}
/**
* Determine if an API key has permission to perform an action.
*
* @param string $key
* @param string $permission
* @return boolean
*/
public static function checkPermission($key, $permission)
{
$api = self::findKey($key);
if (!$api) {
throw new DisplayException('The requested API key (' . $key . ') was not found in the system.');
}
return APIPermission::check($api->id, $permission);
}
public static function noPermissionError($error = 'You do not have permission to perform this action with this API key.')
{
return response()->json([
'error' => 'You do not have permission to perform this action with this API key.'
], 403);
}
}

17
app/Models/APIKey.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace Pterodactyl\Models;
use Illuminate\Database\Eloquent\Model;
class APIKey extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'api_keys';
}

View file

@ -2,7 +2,6 @@
namespace Pterodactyl\Models;
use Debugbar;
use Illuminate\Database\Eloquent\Model;
class APIPermission extends Model
@ -15,16 +14,4 @@ class APIPermission extends Model
*/
protected $table = 'api_permissions';
/**
* Checks if an API key has a specific permission.
*
* @param int $id
* @param string $permission
* @return boolean
*/
public static function check($id, $permission)
{
return self::where('key_id', $id)->where('permission', $permission)->exists();
}
}

View file

@ -34,7 +34,7 @@ class User extends Model implements AuthenticatableContract,
*
* @var array
*/
protected $fillable = ['name', 'email', 'password'];
protected $fillable = ['name', 'email', 'password', 'use_totp', 'totp_secret', 'language'];
/**
* The attributes excluded from the model's JSON form.
@ -70,10 +70,10 @@ class User extends Model implements AuthenticatableContract,
/**
* Set a user password to a new value assuming it meets the following requirements:
* - 8 or more characters in length
* - at least one uppercase character
* - at least one lowercase character
* - at least one number
* - 8 or more characters in length
* - at least one uppercase character
* - at least one lowercase character
* - at least one number
*
* @param string $password The raw password to set the account password to.
* @param string $regex The regex to use when validating the password. Defaults to '((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})'.

View file

@ -183,4 +183,10 @@ class NodeRepository {
}
}
public function delete($id)
{
// @TODO: add logic;
return true;
}
}

View file

@ -685,4 +685,28 @@ class ServerRepository
return $server->save();
}
/**
* Suspends a server instance making it unable to be booted or used by a user.
* @param integer $id
* @return boolean
*/
public function suspend($id)
{
// @TODO: Implement logic; not doing it now since that is outside of the
// scope of this API brance.
return true;
}
/**
* Unsuspends a server instance.
* @param integer $id
* @return boolean
*/
public function unsuspend($id)
{
// @TODO: Implement logic; not doing it now since that is outside of the
// scope of this API brance.
return true;
}
}

View file

@ -2,11 +2,16 @@
namespace Pterodactyl\Repositories;
use DB;
use Hash;
use Validator;
use Pterodactyl\Models\User;
use Pterodactyl\Models;
use Pterodactyl\Services\UuidService;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Exceptions\DisplayException;
class UserRepository
{
@ -22,32 +27,70 @@ class UserRepository
* @param string $password An unhashed version of the user's password.
* @return bool|integer
*/
public function create($email, $password)
public function create($email, $password, $admin = false)
{
$user = new User;
$validator = Validator::make([
'email' => $email,
'password' => $password,
'root_admin' => $admin
], [
'email' => 'required|email|unique:users,email',
'password' => 'required|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
'root_admin' => 'required|boolean'
]);
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
$user = new Models\User;
$uuid = new UuidService;
$user->uuid = $uuid->generate('users', 'uuid');
$user->email = $email;
$user->password = Hash::make($password);
$user->language = 'en';
$user->root_admin = ($admin) ? 1 : 0;
return ($user->save()) ? $user->id : false;
try {
$user->save();
return $user->id;
} catch (\Exception $ex) {
throw $e;
}
}
/**
* Updates a user on the panel.
*
* @param integer $id
* @param array $user An array of columns and their associated values to update for the user.
* @param array $data An array of columns and their associated values to update for the user.
* @return boolean
*/
public function update($id, array $user)
public function update($id, array $data)
{
if(array_key_exists('password', $user)) {
$user['password'] = Hash::make($user['password']);
$validator = Validator::make($data, [
'email' => 'email|unique:users,email,' . $id,
'password' => 'regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})',
'root_admin' => 'boolean',
'language' => 'string|min:1|max:5',
'use_totp' => 'boolean',
'totp_secret' => 'size:16'
]);
// Run validator, throw catchable and displayable exception if it fails.
// Exception includes a JSON result of failed validation rules.
if ($validator->fails()) {
throw new DisplayValidationException($validator->errors());
}
return User::find($id)->update($user);
if(array_key_exists('password', $data)) {
$user['password'] = Hash::make($data['password']);
}
return Models\User::findOrFail($id)->update($data);
}
/**
@ -58,7 +101,22 @@ class UserRepository
*/
public function delete($id)
{
return User::destroy($id);
if(Models\Server::where('owner', $id)->count() > 0) {
throw new DisplayException('Cannot delete a user with active servers attached to thier account.');
}
DB::beginTransaction();
Models\Permission::where('user_id', $id)->delete();
Models\Subuser::where('user_id', $id)->delete();
Models\User::destroy($id);
try {
DB::commit();
return true;
} catch (\Exception $ex) {
throw $ex;
}
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Transformers;
use Pterodactyl\Models\Node;
use League\Fractal\TransformerAbstract;
class NodeTransformer extends TransformerAbstract
{
/**
* Turn this item object into a generic array
*
* @return array
*/
public function transform(Node $node)
{
return $node;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Transformers;
use Pterodactyl\Models\Server;
use League\Fractal\TransformerAbstract;
class ServerTransformer extends TransformerAbstract
{
/**
* Turn this item object into a generic array
*
* @return array
*/
public function transform(Server $server)
{
return $server;
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Pterodactyl\Transformers;
use Pterodactyl\Models\User;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract
{
/**
* Turn this item object into a generic array
*
* @return array
*/
public function transform(User $user)
{
return $user;
}
}

View file

@ -8,6 +8,7 @@
"php": ">=5.5.9",
"laravel/framework": "5.2.*",
"barryvdh/laravel-debugbar": "^2.0",
"dingo/api": "1.0.*@dev",
"doctrine/dbal": "^2.5",
"guzzlehttp/guzzle": "^6.1",
"pragmarx/google2fa": "^0.7.1",

209
config/api.php Normal file
View file

@ -0,0 +1,209 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Standards Tree
|--------------------------------------------------------------------------
|
| Versioning an API with Dingo revolves around content negotiation and
| custom MIME types. A custom type will belong to one of three
| standards trees, the Vendor tree (vnd), the Personal tree
| (prs), and the Unregistered tree (x).
|
| By default the Unregistered tree (x) is used, however, should you wish
| to you can register your type with the IANA. For more details:
| https://tools.ietf.org/html/rfc6838
|
*/
'standardsTree' => env('API_STANDARDS_TREE', 'x'),
/*
|--------------------------------------------------------------------------
| API Subtype
|--------------------------------------------------------------------------
|
| Your subtype will follow the standards tree you use when used in the
| "Accept" header to negotiate the content type and version.
|
| For example: Accept: application/x.SUBTYPE.v1+json
|
*/
'subtype' => env('API_SUBTYPE', 'pterodactyl'),
/*
|--------------------------------------------------------------------------
| Default API Version
|--------------------------------------------------------------------------
|
| This is the default version when strict mode is disabled and your API
| is accessed via a web browser. It's also used as the default version
| when generating your APIs documentation.
|
*/
'version' => env('API_VERSION', 'v1'),
/*
|--------------------------------------------------------------------------
| Default API Prefix
|--------------------------------------------------------------------------
|
| A default prefix to use for your API routes so you don't have to
| specify it for each group.
|
*/
'prefix' => env('API_PREFIX', null),
/*
|--------------------------------------------------------------------------
| Default API Domain
|--------------------------------------------------------------------------
|
| A default domain to use for your API routes so you don't have to
| specify it for each group.
|
*/
'domain' => env('API_DOMAIN', null),
/*
|--------------------------------------------------------------------------
| Name
|--------------------------------------------------------------------------
|
| When documenting your API using the API Blueprint syntax you can
| configure a default name to avoid having to manually specify
| one when using the command.
|
*/
'name' => env('API_NAME', 'Pterodactyl Panel API'),
/*
|--------------------------------------------------------------------------
| Conditional Requests
|--------------------------------------------------------------------------
|
| Globally enable conditional requests so that an ETag header is added to
| any successful response. Subsequent requests will perform a check and
| will return a 304 Not Modified. This can also be enabled or disabled
| on certain groups or routes.
|
*/
'conditionalRequest' => env('API_CONDITIONAL_REQUEST', true),
/*
|--------------------------------------------------------------------------
| Strict Mode
|--------------------------------------------------------------------------
|
| Enabling strict mode will require clients to send a valid Accept header
| with every request. This also voids the default API version, meaning
| your API will not be browsable via a web browser.
|
*/
'strict' => env('API_STRICT', false),
/*
|--------------------------------------------------------------------------
| Debug Mode
|--------------------------------------------------------------------------
|
| Enabling debug mode will result in error responses caused by thrown
| exceptions to have a "debug" key that will be populated with
| more detailed information on the exception.
|
*/
'debug' => env('API_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Generic Error Format
|--------------------------------------------------------------------------
|
| When some HTTP exceptions are not caught and dealt with the API will
| generate a generic error response in the format provided. Any
| keys that aren't replaced with corresponding values will be
| removed from the final response.
|
*/
'errorFormat' => [
'message' => ':message',
'errors' => ':errors',
'code' => ':code',
'status_code' => ':status_code',
'debug' => ':debug',
],
/*
|--------------------------------------------------------------------------
| Authentication Providers
|--------------------------------------------------------------------------
|
| The authentication providers that should be used when attempting to
| authenticate an incoming API request.
|
*/
'auth' => [
'custom' => 'Pterodactyl\Http\Middleware\APISecretToken'
],
/*
|--------------------------------------------------------------------------
| Throttling / Rate Limiting
|--------------------------------------------------------------------------
|
| Consumers of your API can be limited to the amount of requests they can
| make. You can create your own throttles or simply change the default
| throttles.
|
*/
'throttling' => [
],
/*
|--------------------------------------------------------------------------
| Response Transformer
|--------------------------------------------------------------------------
|
| Responses can be transformed so that they are easier to format. By
| default a Fractal transformer will be used to transform any
| responses prior to formatting. You can easily replace
| this with your own transformer.
|
*/
'transformer' => env('API_TRANSFORMER', Dingo\Api\Transformer\Adapter\Fractal::class),
/*
|--------------------------------------------------------------------------
| Response Formats
|--------------------------------------------------------------------------
|
| Responses can be returned in multiple formats by registering different
| response formatters. You can also customize an existing response
| formatter.
|
*/
'defaultFormat' => env('API_DEFAULT_FORMAT', 'json'),
'formats' => [
'json' => Dingo\Api\Http\Response\Format\Json::class,
],
];

View file

@ -112,6 +112,8 @@ return [
'providers' => [
Dingo\Api\Provider\LaravelServiceProvider::class,
/*
* Laravel Framework Service Providers...
*/
@ -179,6 +181,8 @@ return [
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
'DingoAPI' => Dingo\Api\Facade\API::class,
'DingoRoute' => Dingo\Api\Facade\Route::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTableApiKeys extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('api_keys', function (Blueprint $table) {
$table->increments('id');
$table->char('public', 16);
$table->char('secret', 32);
$table->json('allowed_ips')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('api_keys');
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTableApiPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('api_permissions', function (Blueprint $table) {
$table->increments('id');
$table->integer('key_id')->unsigned();
$table->string('permission');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('api_permissions');
}
}

View file

@ -18,7 +18,7 @@ return [
'sendlink' => 'Senden Sie Passwort-Reset Link.', /* Send password reset link */
'emailsent' => 'E-Mail gesendet.', /* Email Sent */
'remeberme' => 'Login merken.', /* Remember my Login*/
'totp_failed' => 'Die TOTP Token vorgesehen war ist ungültig.' /* The TOTP token was provided is not valid. */
'totp_failed' => 'Die TOTP Token vorgesehen war ist ungültig.' /* The TOTP token was provided is not valid. */
];
/*

View file

@ -69,7 +69,7 @@ return [
'array' => 'The :attribute must contain :size items.',
],
'string' => 'The :attribute must be a string.',
'totp' => 'The totp token is invalid. Did it expire?',
'totp' => 'The totp token is invalid. Did it expire?',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.',

View file

@ -7,35 +7,35 @@
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li class="active">Accounts</li>
</ul>
<li><a href="/admin">Admin Control</a></li>
<li class="active">Accounts</li>
</ul>
<h3>All Registered Users</h3><hr />
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Email</th>
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>Email</th>
<th>Account Created</th>
<th>Account Updated</th>
</tr>
</thead>
<tbody>
@foreach ($users as $user)
<tr>
<td><a href="/admin/accounts/view/{{ $user->id }}"><code>{{ $user->email }}</code></a> @if($user->root_admin === 1)<span class="badge">Administrator</span>@endif</td>
</tr>
</thead>
<tbody>
@foreach ($users as $user)
<tr>
<td><a href="/admin/accounts/view/{{ $user->id }}"><code>{{ $user->email }}</code></a> @if($user->root_admin === 1)<span class="badge">Administrator</span>@endif</td>
<td>{{ $user->created_at }}</td>
<td>{{ $user->updated_at }}</td>
</tr>
@endforeach
</tbody>
</table>
</tr>
@endforeach
</tbody>
</table>
<div class="row">
<div class="col-md-12 text-center">{!! $users->render() !!}</div>
</div>
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/accounts']").addClass('active');
$('#sidebar_links').find("a[href='/admin/accounts']").addClass('active');
});
</script>
@endsection

View file

@ -10,65 +10,65 @@
<li><a href="/admin">Admin Controls</a></li>
<li><a href="/admin/accounts">Accounts</a></li>
<li class="active">Add New Account</li>
</ul>
</ul>
<h3>Create New Account</h3><hr />
<form action="new" method="post">
<fieldset>
<div class="form-group">
<label for="email" class="control-label">Email</label>
<div>
<input type="text" autocomplete="off" name="email" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="gen_pass" class=" alert alert-success" style="display:none;margin-bottom: 10px;"></div>
</div>
<div class="form-group col-md-6">
<label for="pass" class="control-label">Password</label>
<div>
<input type="password" name="password" class="form-control" />
</div>
</div>
<div class="form-group col-md-6">
<label for="pass_2" class="control-label">Password Again</label>
<div>
<input type="password" name="password_confirmation" class="form-control" />
</div>
</div>
</div>
<div class="form-group">
<div>
<fieldset>
<div class="form-group">
<label for="email" class="control-label">Email</label>
<div>
<input type="text" autocomplete="off" name="email" class="form-control" />
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="gen_pass" class=" alert alert-success" style="display:none;margin-bottom: 10px;"></div>
</div>
<div class="form-group col-md-6">
<label for="pass" class="control-label">Password</label>
<div>
<input type="password" name="password" class="form-control" />
</div>
</div>
<div class="form-group col-md-6">
<label for="pass_2" class="control-label">Password Again</label>
<div>
<input type="password" name="password_confirmation" class="form-control" />
</div>
</div>
</div>
<div class="form-group">
<div>
{!! csrf_field() !!}
<button class="btn btn-primary btn-sm" type="submit">Create Account</button>
<button class="btn btn-default btn-sm" id="gen_pass_bttn" type="button">Generate Password</button>
</div>
</div>
</fieldset>
</form>
<button class="btn btn-primary btn-sm" type="submit">Create Account</button>
<button class="btn btn-default btn-sm" id="gen_pass_bttn" type="button">Generate Password</button>
</div>
</div>
</fieldset>
</form>
</div>
<script>
$(document).ready(function(){
$("#sidebar_links").find("a[href='/admin/account/new']").addClass('active');
$("#gen_pass_bttn").click(function(e){
e.preventDefault();
$.ajax({
type: "GET",
url: "/password-gen/12",
$("#sidebar_links").find("a[href='/admin/account/new']").addClass('active');
$("#gen_pass_bttn").click(function(e){
e.preventDefault();
$.ajax({
type: "GET",
url: "/password-gen/12",
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
},
success: function(data) {
$("#gen_pass").html('<strong>Generated Password:</strong> ' + data).slideDown();
$('input[name="password"], input[name="password_confirmation"]').val(data);
return false;
}
});
return false;
});
success: function(data) {
$("#gen_pass").html('<strong>Generated Password:</strong> ' + data).slideDown();
$('input[name="password"], input[name="password_confirmation"]').val(data);
return false;
}
});
return false;
});
});
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/accounts/new']").addClass('active');
$('#sidebar_links').find("a[href='/admin/accounts/new']").addClass('active');
});
</script>
@endsection

View file

@ -7,14 +7,14 @@
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li class="active">Admin Control</li>
</ul>
<li class="active">Admin Control</li>
</ul>
<h3 class="nopad">Pterodactyl Admin Control Panel</h3><hr />
<p>Welcome to the most advanced, lightweight, and user-friendly open source game server control panel.</p>
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin']").addClass('active');
$('#sidebar_links').find("a[href='/admin']").addClass('active');
});
</script>
@endsection

View file

@ -7,37 +7,37 @@
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li class="active">Locations</li>
</ul>
<li><a href="/admin">Admin Control</a></li>
<li class="active">Locations</li>
</ul>
<h3>All Locations</h3><hr />
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Location</th>
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Location</th>
<th>Description</th>
<th class="text-center">Nodes</th>
<th class="text-center">Nodes</th>
<th class="text-center">Servers</th>
</tr>
</thead>
<tbody>
@foreach ($locations as $location)
<tr>
<td><a href="#/edit/{{ $location->id }}" data-action="edit" data-location="{{ $location->id }}"><code>{{ $location->short }}</code></td>
</tr>
</thead>
<tbody>
@foreach ($locations as $location)
<tr>
<td><a href="#/edit/{{ $location->id }}" data-action="edit" data-location="{{ $location->id }}"><code>{{ $location->short }}</code></td>
<td>{{ $location->long }}</td>
<td class="text-center">{{ $location->a_nodeCount }}</td>
<td class="text-center">{{ $location->a_serverCount }}</td>
</tr>
@endforeach
</tbody>
</table>
</tr>
@endforeach
</tbody>
</table>
<div class="row">
<div class="col-md-12 text-center">{!! $locations->render() !!}</div>
</div>
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/locations']").addClass('active');
$('#sidebar_links').find("a[href='/admin/locations']").addClass('active');
});
</script>
@endsection

View file

@ -7,27 +7,27 @@
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li class="active">Nodes</li>
</ul>
<li><a href="/admin">Admin Control</a></li>
<li class="active">Nodes</li>
</ul>
<h3>All Nodes</h3><hr />
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Name</th>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Name</th>
<th class="visible-lg">Location</th>
<th>FQDN</th>
<th>FQDN</th>
<th class="hidden-xs">Memory</th>
<th class="hidden-xs">Disk</th>
<th class="text-center hidden-xs">Servers</th>
<th class="text-center">HTTPS</th>
<th class="text-center">Public</th>
</tr>
</thead>
<tbody>
@foreach ($nodes as $node)
<tr>
<td><a href="/admin/nodes/view/{{ $node->id }}">{{ $node->name }}</td>
</tr>
</thead>
<tbody>
@foreach ($nodes as $node)
<tr>
<td><a href="/admin/nodes/view/{{ $node->id }}">{{ $node->name }}</td>
<td class="visible-lg">{{ $node->a_locationName }}</td>
<td><code>{{ $node->fqdn }}</code></td>
<td class="hidden-xs">{{ $node->memory }} MB</td>
@ -35,17 +35,17 @@
<td class="text-center hidden-xs">{{ $node->a_serverCount }}</td>
<td class="text-center" style="color:{{ ($node->scheme === 'https') ? '#50af51' : '#d9534f' }}"><i class="fa fa-{{ ($node->scheme === 'https') ? 'lock' : 'unlock' }}"></i></td>
<td class="text-center"><i class="fa fa-{{ ($node->public === 1) ? 'check' : 'times' }}"></i></td>
</tr>
@endforeach
</tbody>
</table>
</tr>
@endforeach
</tbody>
</table>
<div class="row">
<div class="col-md-12 text-center">{!! $nodes->render() !!}</div>
</div>
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/nodes']").addClass('active');
$('#sidebar_links').find("a[href='/admin/nodes']").addClass('active');
});
</script>
@endsection

View file

@ -7,10 +7,10 @@
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li><a href="/admin">Admin Control</a></li>
<li><a href="/admin/nodes">Nodes</a></li>
<li class="active">Create New Node</li>
</ul>
<li class="active">Create New Node</li>
</ul>
<h3>Create New Node</h3><hr />
<form action="/admin/nodes/new" method="POST">
<div class="well">
@ -158,7 +158,7 @@
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/nodes/new']").addClass('active');
$('#sidebar_links').find("a[href='/admin/nodes/new']").addClass('active');
$('[data-toggle="popover"]').popover({
placement: 'auto'
});

View file

@ -7,39 +7,39 @@
@section('content')
<div class="col-md-12">
<ul class="breadcrumb">
<li><a href="/admin">Admin Control</a></li>
<li class="active">Servers</li>
</ul>
<li><a href="/admin">Admin Control</a></li>
<li class="active">Servers</li>
</ul>
<h3>All Servers</h3><hr />
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Server Name</th>
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>Server Name</th>
<th>Owner</th>
<th class="hidden-xs">Node</th>
<th class="hidden-xs">Node</th>
<th>Default Connection</th>
<th class="hidden-xs">SFTP Username</th>
</tr>
</thead>
<tbody>
@foreach ($servers as $server)
<tr class="dynUpdate @if($server->active !== 1)active @endif" id="{{ $server->uuidShort }}">
<td><a href="/admin/servers/view/{{ $server->id }}">{{ $server->name }}</td>
</tr>
</thead>
<tbody>
@foreach ($servers as $server)
<tr class="dynUpdate @if($server->active !== 1)active @endif" id="{{ $server->uuidShort }}">
<td><a href="/admin/servers/view/{{ $server->id }}">{{ $server->name }}</td>
<td><a href="/admin/accounts/view/{{ $server->owner }}">{{ $server->a_ownerEmail }}</a></td>
<td class="hidden-xs"><a href="/admin/nodes/view/{{ $server->node }}">{{ $server->a_nodeName }}</a></td>
<td><code>{{ $server->ip }}:{{ $server->port }}</code></td>
<td class="hidden-xs"><code>{{ $server->username }}</code></td>
</tr>
@endforeach
</tbody>
</table>
</tr>
@endforeach
</tbody>
</table>
<div class="row">
<div class="col-md-12 text-center">{!! $servers->render() !!}</div>
</div>
</div>
<script>
$(document).ready(function () {
$('#sidebar_links').find("a[href='/admin/servers']").addClass('active');
$('#sidebar_links').find("a[href='/admin/servers']").addClass('active');
});
</script>
@endsection

View file

@ -14,20 +14,20 @@
<form action="/auth/password/verify" method="POST">
<legend>{{ trans('auth.resetpassword') }}</legend>
<fieldset>
<input type="hidden" name="token" value="{{ $token }}">
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group">
<label for="email" class="control-label">{{ trans('strings.email') }}</label>
<div>
<input type="text" class="form-control" name="email" id="email" value="{{ old('email') }}" placeholder="{{ trans('strings.email') }}" />
</div>
</div>
<div class="form-group">
<div class="form-group">
<label for="password" class="control-label">{{ trans('strings.password') }}</label>
<div>
<input type="password" class="form-control" name="password" id="password" placeholder="{{ trans('strings.password') }}" />
</div>
</div>
<div class="form-group">
<div class="form-group">
<label for="password_confirmation" class="control-label">{{ trans('auth.confirmpassword') }}</label>
<div>
<input type="password" class="form-control" id="password_confirmation" name="password_confirmation" />

View file

@ -7,61 +7,61 @@
@section('content')
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<h3 class="nopad">{{ trans('base.account.update_pass') }}</h3><hr />
<form action="/account/password" method="post">
<div class="form-group">
<label for="current_password" class="control-label">{{ trans('strings.current_password') }}</label>
<div>
<input type="password" class="form-control" name="current_password" />
</div>
</div>
<div class="form-group">
<label for="new_password" class="control-label">{{ trans('base.account.new_password') }}</label>
<div>
<input type="password" class="form-control" name="new_password" />
<div class="row">
<div class="col-md-6">
<h3 class="nopad">{{ trans('base.account.update_pass') }}</h3><hr />
<form action="/account/password" method="post">
<div class="form-group">
<label for="current_password" class="control-label">{{ trans('strings.current_password') }}</label>
<div>
<input type="password" class="form-control" name="current_password" />
</div>
</div>
<div class="form-group">
<label for="new_password" class="control-label">{{ trans('base.account.new_password') }}</label>
<div>
<input type="password" class="form-control" name="new_password" />
<p class="text-muted"><small>{{ trans('base.password_req') }}</small></p>
</div>
</div>
<div class="form-group">
<label for="new_password_again" class="control-label">{{ trans('base.account.new_password') }} {{ trans('strings.again') }}</label>
<div>
<input type="password" class="form-control" name="new_password_confirmation" />
</div>
</div>
<div class="form-group">
<div>
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm" value="{{ trans('base.account.update_pass') }}" />
</div>
</div>
</form>
</div>
<div class="col-md-6">
<h3 class="nopad">{{ trans('base.account.update_email') }}</h3><hr />
<form action="/account/email" method="post">
<div class="form-group">
<label for="new_email" class="control-label">{{ trans('base.account.new_email') }}</label>
<div>
<input type="text" class="form-control" name="new_email" />
</div>
</div>
<div class="form-group">
<label for="password" class="control-label">{{ trans('strings.current_password') }}</label>
<div>
<input type="password" class="form-control" name="password" />
</div>
</div>
<div class="form-group">
<div>
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm" value="{{ trans('base.account.update_email') }}" />
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="new_password_again" class="control-label">{{ trans('base.account.new_password') }} {{ trans('strings.again') }}</label>
<div>
<input type="password" class="form-control" name="new_password_confirmation" />
</div>
</div>
<div class="form-group">
<div>
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm" value="{{ trans('base.account.update_pass') }}" />
</div>
</div>
</form>
</div>
<div class="col-md-6">
<h3 class="nopad">{{ trans('base.account.update_email') }}</h3><hr />
<form action="/account/email" method="post">
<div class="form-group">
<label for="new_email" class="control-label">{{ trans('base.account.new_email') }}</label>
<div>
<input type="text" class="form-control" name="new_email" />
</div>
</div>
<div class="form-group">
<label for="password" class="control-label">{{ trans('strings.current_password') }}</label>
<div>
<input type="password" class="form-control" name="password" />
</div>
</div>
<div class="form-group">
<div>
{!! csrf_field() !!}
<input type="submit" class="btn btn-primary btn-sm" value="{{ trans('base.account.update_email') }}" />
</div>
</div>
</form>
</div>
</div>
</div>
<script>
$(document).ready(function () {

View file

@ -1,14 +1,14 @@
<html>
<head>
<title>Pterodactyl - Admin Reset Password</title>
</head>
<body>
<center><h1>Pterodactyl - Admin Reset Password</h1></center>
<p>Hello there! You are receiving this email because an admin has reset the password on your Pterodactyl account.</p>
<p><strong>Login:</strong> <a href="{{ config('app.url') }}/auth/login">{{ config('app.url') }}/auth/login</a><br>
<strong>Email:</strong> {{ $user->email }}<br>
<strong>Password:</strong> {{ $password }}</p>
<p>Thanks,<br>Pterodactyl</p>
</body>
<head>
<title>Pterodactyl - Admin Reset Password</title>
</head>
<body>
<center><h1>Pterodactyl - Admin Reset Password</h1></center>
<p>Hello there! You are receiving this email because an admin has reset the password on your Pterodactyl account.</p>
<p><strong>Login:</strong> <a href="{{ config('app.url') }}/auth/login">{{ config('app.url') }}/auth/login</a><br>
<strong>Email:</strong> {{ $user->email }}<br>
<strong>Password:</strong> {{ $password }}</p>
<p>Thanks,<br>Pterodactyl</p>
</body>
</html>

View file

@ -1,14 +1,14 @@
<html>
<head>
<title>Pterodactyl Lost Password Recovery</title>
</head>
<body>
<center><h1>Pterodactyl Lost Password Recovery</h1></center>
<p>Hello there! You are receiving this email because you requested a new password for your Pterodactyl account.</p>
<p>Please click the link below to confirm that you wish to change your password. If you did not make this request, or do not wish to continue simply ignore this email and nothing will happen. <strong>This link will expire in 1 hour.</strong></p>
<p><a href="{{ url('auth/password/verify/'.$token) }}">{{ url('auth/password/verify/'.$token) }}</a></p>
<p>Please do not hesitate to contact us if you belive something is wrong.
<p>Thanks!<br />Pterodactyl</p>
</body>
<head>
<title>Pterodactyl Lost Password Recovery</title>
</head>
<body>
<center><h1>Pterodactyl Lost Password Recovery</h1></center>
<p>Hello there! You are receiving this email because you requested a new password for your Pterodactyl account.</p>
<p>Please click the link below to confirm that you wish to change your password. If you did not make this request, or do not wish to continue simply ignore this email and nothing will happen. <strong>This link will expire in 1 hour.</strong></p>
<p><a href="{{ url('auth/password/verify/'.$token) }}">{{ url('auth/password/verify/'.$token) }}</a></p>
<p>Please do not hesitate to contact us if you belive something is wrong.
<p>Thanks!<br />Pterodactyl</p>
</body>
</html>

View file

@ -10,10 +10,10 @@
</div>
<div class="panel-body">
<p style="margin-bottom:0;">The requested server is still completing the install process. Please check back in a few minutes, you should recieve an email as soon as this process is completed.</p>
<br /><br />
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-danger" style="width: 75%"></div>
</div>
<br /><br />
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-danger" style="width: 75%"></div>
</div>
</div>
</div>
<p style="text-align:center;"><a href="{{ URL::previous() }}">Take me back</a> or <a href="/">go home</a>.</p>