From 98b3355158b8b97abbb56cfdcc549a0e8c759b0f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 12 Jan 2016 01:05:44 -0500 Subject: [PATCH 01/14] very basic initial push of API --- .env.example | 5 + app/Exceptions/Handler.php | 2 +- app/Http/Controllers/API/AuthController.php | 64 ++++++ app/Http/Controllers/API/BaseController.php | 11 ++ app/Http/Controllers/API/UserController.php | 86 ++------ app/Http/Kernel.php | 3 +- app/Http/Middleware/APIAuthenticate.php | 46 ----- app/Http/Routes/APIRoutes.php | 47 +++++ app/Transformers/UserTransformer.php | 21 ++ composer.json | 4 +- config/api.php | 209 ++++++++++++++++++++ config/app.php | 7 + config/jwt.php | 168 ++++++++++++++++ 13 files changed, 555 insertions(+), 118 deletions(-) create mode 100644 app/Http/Controllers/API/AuthController.php create mode 100644 app/Http/Controllers/API/BaseController.php delete mode 100644 app/Http/Middleware/APIAuthenticate.php create mode 100644 app/Http/Routes/APIRoutes.php create mode 100644 app/Transformers/UserTransformer.php create mode 100644 config/api.php create mode 100644 config/jwt.php diff --git a/.env.example b/.env.example index 5278a54f9..e282046f9 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ APP_ENV=local APP_DEBUG=true APP_KEY=SomeRandomString +JWT_SECRET=ChangeMe DB_HOST=localhost DB_PORT=3306 @@ -22,3 +23,7 @@ MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null + +API_PREFIX=api +API_VERSION=v1 +API_DEBUG=false diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 1ef91c60c..d3a4599a7 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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.'; diff --git a/app/Http/Controllers/API/AuthController.php b/app/Http/Controllers/API/AuthController.php new file mode 100644 index 000000000..6f95aa6c8 --- /dev/null +++ b/app/Http/Controllers/API/AuthController.php @@ -0,0 +1,64 @@ +"}) + */ + public function postLogin(Request $request) { + $credentials = $request->only('email', 'password'); + + try { + $token = JWTAuth::attempt($credentials, [ + 'permissions' => [ + 'view_users' => true, + 'edit_users' => true, + 'delete_users' => false, + ] + ]); + if (!$token) { + throw new UnauthorizedHttpException(''); + } + } catch (JWTException $ex) { + throw new ServiceUnavailableHttpException(''); + } + + return compact('token'); + } + + /** + * Check if Authenticated + * + * @Post("/validate") + * @Versions({"v1"}) + * @Request(headers={"Authorization": "Bearer "}) + * @Response(204); + */ + public function postValidate(Request $request) { + return $this->response->noContent(); + } + +} diff --git a/app/Http/Controllers/API/BaseController.php b/app/Http/Controllers/API/BaseController.php new file mode 100644 index 000000000..b0ade2a64 --- /dev/null +++ b/app/Http/Controllers/API/BaseController.php @@ -0,0 +1,11 @@ +header('X-Authorization'), 'get-users')) { - return API::noPermissionError(); - } - - return response()->json([ - 'users' => User::all() - ]); - } - - /** - * Returns JSON response about a user given their ID. - * If fields are provided only those fields are returned. + * List All Users * - * Does not return protected fields (i.e. password & totp_secret) + * Lists all users currently on the system. * - * @param Request $request - * @param int $id - * @param string $fields - * @return Response + * @Get("/{?page}") + * @Versions({"v1"}) + * @Parameters({ + * @Parameter("page", type="integer", description="The page of results to view.", default=1) + * }) + * @Response(200) */ - public function getUser(Request $request, $id, $fields = null) - { - - // 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); - } - } - - 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); - } - throw $e; - } - + public function getUsers(Request $request) { + $users = Models\User::paginate(15); + return $this->response->paginator($users, new UserTransformer); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 5ae0c37ad..513fa4d9d 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -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, ]; } diff --git a/app/Http/Middleware/APIAuthenticate.php b/app/Http/Middleware/APIAuthenticate.php deleted file mode 100644 index 8b59f7e27..000000000 --- a/app/Http/Middleware/APIAuthenticate.php +++ /dev/null @@ -1,46 +0,0 @@ -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); - - } -} diff --git a/app/Http/Routes/APIRoutes.php b/app/Http/Routes/APIRoutes.php new file mode 100644 index 000000000..355a1a3b7 --- /dev/null +++ b/app/Http/Routes/APIRoutes.php @@ -0,0 +1,47 @@ +extend('jwt', function ($app) { + return new \Dingo\Api\Auth\Provider\JWT($app['Tymon\JWTAuth\JWTAuth']); + }); + + $api = app('Dingo\Api\Routing\Router'); + + $api->version('v1', function ($api) { + $api->post('auth/login', [ + 'as' => 'api.auth.login', + 'uses' => 'Pterodactyl\Http\Controllers\API\AuthController@postLogin' + ]); + + $api->post('auth/validate', [ + 'middleware' => 'api.auth', + 'as' => 'api.auth.validate', + 'uses' => 'Pterodactyl\Http\Controllers\API\AuthController@postValidate' + ]); + }); + + $api->version('v1', ['middleware' => 'api.auth'], function ($api) { + + $api->get('users', [ + 'as' => 'api.auth.validate', + 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUsers' + ]); + + $api->get('users/{id}', function($id) { + return Models\User::findOrFail($id); + }); + + + }); + } + +} diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php new file mode 100644 index 000000000..22e4dffe6 --- /dev/null +++ b/app/Transformers/UserTransformer.php @@ -0,0 +1,21 @@ +=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", "webpatser/laravel-uuid": "^2.0", "prologue/alerts": "^0.4.0", - "s1lentium/iptools": "^1.0" + "s1lentium/iptools": "^1.0", + "tymon/jwt-auth": "^0.5.6" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/config/api.php b/config/api.php new file mode 100644 index 000000000..872723046 --- /dev/null +++ b/config/api.php @@ -0,0 +1,209 @@ + 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' => [ + 'jwt' => 'Dingo\Api\Auth\Provider\JWT' + ], + + /* + |-------------------------------------------------------------------------- + | 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, + + ], + +]; diff --git a/config/app.php b/config/app.php index 5fb65fa0b..aa29a7be3 100644 --- a/config/app.php +++ b/config/app.php @@ -112,6 +112,9 @@ return [ 'providers' => [ + Dingo\Api\Provider\LaravelServiceProvider::class, + Tymon\JWTAuth\Providers\JWTAuthServiceProvider::class, + /* * Laravel Framework Service Providers... */ @@ -179,6 +182,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, @@ -187,6 +192,8 @@ return [ 'Hash' => Illuminate\Support\Facades\Hash::class, 'Input' => Illuminate\Support\Facades\Input::class, 'Inspiring' => Illuminate\Foundation\Inspiring::class, + 'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class, + 'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class, 'Lang' => Illuminate\Support\Facades\Lang::class, 'Log' => Illuminate\Support\Facades\Log::class, 'Mail' => Illuminate\Support\Facades\Mail::class, diff --git a/config/jwt.php b/config/jwt.php new file mode 100644 index 000000000..7c1101320 --- /dev/null +++ b/config/jwt.php @@ -0,0 +1,168 @@ + env('JWT_SECRET', 'changeme'), + + /* + |-------------------------------------------------------------------------- + | JWT time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token will be valid for. + | Defaults to 1 hour + | + */ + + 'ttl' => 60, + + /* + |-------------------------------------------------------------------------- + | Refresh time to live + |-------------------------------------------------------------------------- + | + | Specify the length of time (in minutes) that the token can be refreshed + | within. I.E. The user can refresh their token within a 2 week window of + | the original token being created until they must re-authenticate. + | Defaults to 2 weeks + | + */ + + 'refresh_ttl' => 20160, + + /* + |-------------------------------------------------------------------------- + | JWT hashing algorithm + |-------------------------------------------------------------------------- + | + | Specify the hashing algorithm that will be used to sign the token. + | + | See here: https://github.com/namshi/jose/tree/2.2.0/src/Namshi/JOSE/Signer + | for possible values + | + */ + + 'algo' => 'HS256', + + /* + |-------------------------------------------------------------------------- + | User Model namespace + |-------------------------------------------------------------------------- + | + | Specify the full namespace to your User model. + | e.g. 'Acme\Entities\User' + | + */ + + 'user' => 'Pterodactyl\Models\User', + + /* + |-------------------------------------------------------------------------- + | User identifier + |-------------------------------------------------------------------------- + | + | Specify a unique property of the user that will be added as the 'sub' + | claim of the token payload. + | + */ + + 'identifier' => 'id', + + /* + |-------------------------------------------------------------------------- + | Required Claims + |-------------------------------------------------------------------------- + | + | Specify the required claims that must exist in any token. + | A TokenInvalidException will be thrown if any of these claims are not + | present in the payload. + | + */ + + 'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'], + + /* + |-------------------------------------------------------------------------- + | Blacklist Enabled + |-------------------------------------------------------------------------- + | + | In order to invalidate tokens, you must have the the blacklist enabled. + | If you do not want or need this functionality, then set this to false. + | + */ + + 'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Providers + |-------------------------------------------------------------------------- + | + | Specify the various providers used throughout the package. + | + */ + + 'providers' => [ + + /* + |-------------------------------------------------------------------------- + | User Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to find the user based + | on the subject claim + | + */ + + 'user' => 'Tymon\JWTAuth\Providers\User\EloquentUserAdapter', + + /* + |-------------------------------------------------------------------------- + | JWT Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to create and decode the tokens. + | + */ + + 'jwt' => 'Tymon\JWTAuth\Providers\JWT\NamshiAdapter', + + /* + |-------------------------------------------------------------------------- + | Authentication Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to authenticate users. + | + */ + + 'auth' => function ($app) { + return new Tymon\JWTAuth\Providers\Auth\IlluminateAuthAdapter($app['auth']); + }, + + /* + |-------------------------------------------------------------------------- + | Storage Provider + |-------------------------------------------------------------------------- + | + | Specify the provider that is used to store tokens in the blacklist + | + */ + + 'storage' => function ($app) { + return new Tymon\JWTAuth\Providers\Storage\IlluminateCacheAdapter($app['cache']); + } + + ] + +]; From 2def94c958330ad267e1a1a8485c3c27fb34ddce Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 12 Jan 2016 21:50:43 -0500 Subject: [PATCH 02/14] Update routes to use CSRF protection --- app/Http/Routes/AdminRoutes.php | 15 ++++++++++----- app/Http/Routes/AuthRoutes.php | 3 ++- app/Http/Routes/BaseRoutes.php | 6 ++++-- app/Http/Routes/RestRoutes.php | 31 ------------------------------- app/Http/Routes/ServerRoutes.php | 3 ++- 5 files changed, 18 insertions(+), 40 deletions(-) delete mode 100644 app/Http/Routes/RestRoutes.php diff --git a/app/Http/Routes/AdminRoutes.php b/app/Http/Routes/AdminRoutes.php index 568cae4b0..2e22b5285 100644 --- a/app/Http/Routes/AdminRoutes.php +++ b/app/Http/Routes/AdminRoutes.php @@ -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('/', [ diff --git a/app/Http/Routes/AuthRoutes.php b/app/Http/Routes/AuthRoutes.php index fa6e9771c..944011cfc 100644 --- a/app/Http/Routes/AuthRoutes.php +++ b/app/Http/Routes/AuthRoutes.php @@ -12,7 +12,8 @@ class AuthRoutes { $router->group([ 'prefix' => 'auth', 'middleware' => [ - 'guest' + 'guest', + 'csrf' ] ], function () use ($router) { diff --git a/app/Http/Routes/BaseRoutes.php b/app/Http/Routes/BaseRoutes.php index 7081db9d4..16a3795df 100644 --- a/app/Http/Routes/BaseRoutes.php +++ b/app/Http/Routes/BaseRoutes.php @@ -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('/', [ diff --git a/app/Http/Routes/RestRoutes.php b/app/Http/Routes/RestRoutes.php deleted file mode 100644 index 0e98cc981..000000000 --- a/app/Http/Routes/RestRoutes.php +++ /dev/null @@ -1,31 +0,0 @@ -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]+'); - }); - }); - } - -} diff --git a/app/Http/Routes/ServerRoutes.php b/app/Http/Routes/ServerRoutes.php index 333bdacd6..a00ec390b 100644 --- a/app/Http/Routes/ServerRoutes.php +++ b/app/Http/Routes/ServerRoutes.php @@ -11,7 +11,8 @@ class ServerRoutes { 'prefix' => 'server/{server}', 'middleware' => [ 'auth', - 'server' + 'server', + 'csrf' ] ], function ($server) use ($router) { // Index View for Server From 72acf063538f3b383107f8c224f8c81970057529 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 12 Jan 2016 22:59:24 -0500 Subject: [PATCH 03/14] Improve API auth to rate limit requests and verify they are root_admin --- app/Http/Controllers/API/AuthController.php | 80 ++++++++++++++++++--- 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/API/AuthController.php b/app/Http/Controllers/API/AuthController.php index 6f95aa6c8..638a0afda 100644 --- a/app/Http/Controllers/API/AuthController.php +++ b/app/Http/Controllers/API/AuthController.php @@ -3,11 +3,19 @@ namespace Pterodactyl\Http\Controllers\API; use JWTAuth; +use Hash; +use Validator; + use Tymon\JWTAuth\Exceptions\JWTException; +use Dingo\Api\Exception\StoreResourceFailedException; +use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; +use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; + use Illuminate\Http\Request; -use \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; -use \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; +use Illuminate\Foundation\Auth\ThrottlesLogins; +use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers; use Pterodactyl\Transformers\UserTransformer; use Pterodactyl\Models; @@ -18,6 +26,32 @@ use Pterodactyl\Models; class AuthController extends BaseController { + use AuthenticatesAndRegistersUsers, ThrottlesLogins; + + /** + * Lockout time for failed login requests. + * + * @var integer + */ + protected $lockoutTime = 120; + + /** + * After how many attempts should logins be throttled and locked. + * + * @var integer + */ + protected $maxLoginAttempts = 3; + + /** + * Create a new authentication controller instance. + * + * @return void + */ + public function __construct() + { + // + } + /** * Authenticate * @@ -29,16 +63,42 @@ class AuthController extends BaseController * @Response(200, body={"token": ""}) */ public function postLogin(Request $request) { - $credentials = $request->only('email', 'password'); + + $validator = Validator::make($request->only(['email', 'password']), [ + 'email' => 'required|email', + 'password' => 'required|min:8' + ]); + + if ($validator->fails()) { + throw new StoreResourceFailedException('Required authentication fields were invalid.', $validator->errors()); + } + + $throttled = $this->isUsingThrottlesLoginsTrait(); + if ($throttled && $this->hasTooManyLoginAttempts($request)) { + throw new TooManyRequestsHttpException('You have been login throttled for 120 seconds.'); + } + + // Is the email & password valid? + $user = Models\User::where('email', $request->input('email'))->first(); + if (!$user || !Hash::check($request->input('password'), $user->password)) { + if ($throttled) { + $this->incrementLoginAttempts($request); + } + throw new UnauthorizedHttpException('A user by those credentials was not found.'); + } + + // @TODO: validate TOTP if enabled on account? + // Perhaps this could be implemented in such a way that they login to their + // account and generate a one time password that can be used? Would be a pain in + // the butt for multiple API requests though. Maybe just included a 'totp' field + // that can include the token for that timestamp. Would allow for programtic + // generation of the code and API requests. + if ($user->root_admin !== 1) { + throw new UnauthorizedHttpException('This account does not have permission to interface this API.'); + } try { - $token = JWTAuth::attempt($credentials, [ - 'permissions' => [ - 'view_users' => true, - 'edit_users' => true, - 'delete_users' => false, - ] - ]); + $token = JWTAuth::fromUser($user); if (!$token) { throw new UnauthorizedHttpException(''); } From 3114c1f73ee1b15270cf1be849654cc2f741bc1b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 12 Jan 2016 22:59:34 -0500 Subject: [PATCH 04/14] Add user specific listing route --- app/Http/Controllers/API/UserController.php | 31 ++++++++++++++++++++- app/Http/Routes/APIRoutes.php | 9 +++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/API/UserController.php b/app/Http/Controllers/API/UserController.php index 146575552..f4f3cf250 100644 --- a/app/Http/Controllers/API/UserController.php +++ b/app/Http/Controllers/API/UserController.php @@ -25,9 +25,38 @@ class UserController extends BaseController * }) * @Response(200) */ - public function getUsers(Request $request) { + public function getUsers(Request $request) + { $users = Models\User::paginate(15); return $this->response->paginator($users, new UserTransformer); } + /** + * List Specific User + * + * Lists specific fields about a user or all fields pertaining to that user. + * + * @Get("/{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 getUserByID(Request $request, $id, $fields = null) + { + $query = Models\User::where('id', $id); + + if (!is_null($fields)) { + foreach(explode(',', $fields) as $field) { + if (!empty($field)) { + $query->addSelect($field); + } + } + } + + return $query->first(); + } + } diff --git a/app/Http/Routes/APIRoutes.php b/app/Http/Routes/APIRoutes.php index 355a1a3b7..3525e6cd1 100644 --- a/app/Http/Routes/APIRoutes.php +++ b/app/Http/Routes/APIRoutes.php @@ -32,13 +32,14 @@ class APIRoutes $api->version('v1', ['middleware' => 'api.auth'], function ($api) { $api->get('users', [ - 'as' => 'api.auth.validate', + 'as' => 'api.users', 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUsers' ]); - $api->get('users/{id}', function($id) { - return Models\User::findOrFail($id); - }); + $api->get('users/{id}/{fields?}', [ + 'as' => 'api.users.view', + 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUserByID' + ]); }); From 695728295a779f4b3b985dee4e60b82b62e851aa Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 12 Jan 2016 23:43:33 -0500 Subject: [PATCH 05/14] Add support for creating a user using the API --- app/Http/Controllers/API/AuthController.php | 2 +- app/Http/Controllers/API/UserController.php | 87 ++++++++++++++++++- .../Controllers/Admin/AccountsController.php | 17 +--- app/Http/Routes/APIRoutes.php | 6 +- app/Repositories/UserRepository.php | 30 ++++++- 5 files changed, 122 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/API/AuthController.php b/app/Http/Controllers/API/AuthController.php index 638a0afda..613f872f5 100644 --- a/app/Http/Controllers/API/AuthController.php +++ b/app/Http/Controllers/API/AuthController.php @@ -115,7 +115,7 @@ class AuthController extends BaseController * @Post("/validate") * @Versions({"v1"}) * @Request(headers={"Authorization": "Bearer "}) - * @Response(204); + * @Response(204) */ public function postValidate(Request $request) { return $this->response->noContent(); diff --git a/app/Http/Controllers/API/UserController.php b/app/Http/Controllers/API/UserController.php index f4f3cf250..e3cc7d9d7 100644 --- a/app/Http/Controllers/API/UserController.php +++ b/app/Http/Controllers/API/UserController.php @@ -4,8 +4,11 @@ namespace Pterodactyl\Http\Controllers\API; use Illuminate\Http\Request; +use Dingo\Api\Exception\StoreResourceFailedException; + use Pterodactyl\Transformers\UserTransformer; use Pterodactyl\Models; +use Pterodactyl\Repositories\UserRepository; /** * @Resource("Users", uri="/users") @@ -21,7 +24,7 @@ class UserController extends BaseController * @Get("/{?page}") * @Versions({"v1"}) * @Parameters({ - * @Parameter("page", type="integer", description="The page of results to view.", default=1) + * @Parameter("page", type="integer", description="The page of results to view.", default=1) * }) * @Response(200) */ @@ -39,8 +42,8 @@ class UserController extends BaseController * @Get("/{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.") + * @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) */ @@ -59,4 +62,82 @@ class UserController extends BaseController return $query->first(); } + /** + * Create a New User + * + * @Post("/") + * @Versions({"v1"}) + * @Transaction({ + * @Request({ + * "email": "foo@example.com", + * "password": "foopassword", + * "admin": false + * }, headers={"Authorization": "Bearer "}), + * @Response(200, body={"id": 1}), + * @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 postUsers(Request $request) + { + try { + $user = new UserRepository; + $create = $user->create($request->input('email'), $request->input('password'), $request->input('admin')); + return [ 'id' => $create ]; + } catch (\Pterodactyl\Exceptions\DisplayValidationException $ex) { + throw new StoreResourceFailedException('A validation error occured.', json_decode($ex->getMessage(), true)); + } catch (\Exception $ex) { + throw new StoreResourceFailedException('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("/{id}") + * @Versions({"v1"}) + * @Transaction({ + * @Request({ + * "email": "new@email.com" + * }, headers={"Authorization": "Bearer "}), + * @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) + { + // + } + + /** + * Delete a User + * + * @Delete("/{id}") + * @Versions({"v1"}) + * @Transaction({ + * @Request(headers={"Authorization": "Bearer "}), + * @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) + { + // + } + } diff --git a/app/Http/Controllers/Admin/AccountsController.php b/app/Http/Controllers/Admin/AccountsController.php index 813162a78..77a82d9f3 100644 --- a/app/Http/Controllers/Admin/AccountsController.php +++ b/app/Http/Controllers/Admin/AccountsController.php @@ -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) diff --git a/app/Http/Routes/APIRoutes.php b/app/Http/Routes/APIRoutes.php index 3525e6cd1..58157d8a9 100644 --- a/app/Http/Routes/APIRoutes.php +++ b/app/Http/Routes/APIRoutes.php @@ -36,12 +36,16 @@ class APIRoutes 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUsers' ]); + $api->post('users', [ + 'as' => 'api.users.post', + 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@postUsers' + ]); + $api->get('users/{id}/{fields?}', [ 'as' => 'api.users.view', 'uses' => 'Pterodactyl\Http\Controllers\API\UserController@getUserByID' ]); - }); } diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index adffbf305..aa633d392 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -2,11 +2,14 @@ namespace Pterodactyl\Repositories; +use Validator; use Hash; use Pterodactyl\Models\User; use Pterodactyl\Services\UuidService; +use Pterodactyl\Exceptions\DisplayValidationException; + class UserRepository { @@ -22,16 +25,39 @@ 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) { + + $validator = Validator::make([ + 'email' => $email, + 'password' => $password, + 'admin' => $admin + ], [ + 'email' => 'required|email|unique:users,email', + 'password' => 'required|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})', + '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 User; $uuid = new UuidService; $user->uuid = $uuid->generate('users', 'uuid'); $user->email = $email; $user->password = Hash::make($password); + $user->root_admin = ($admin) ? 1 : 0; - return ($user->save()) ? $user->id : false; + try { + $user->save(); + return $user->id; + } catch (\Exception $ex) { + throw $e; + } } /** From 4604500349d00cf39c74313c17d0cb4308745dfe Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 12 Jan 2016 23:49:56 -0500 Subject: [PATCH 06/14] Replace tabs with Spaces I *really* wish Atom would stop doing this to me. --- app/Http/Controllers/API/UserController.php | 38 +++--- app/Models/User.php | 8 +- app/Repositories/UserRepository.php | 2 + resources/lang/de/auth.php | 2 +- resources/lang/de/validation.php | 2 +- .../views/admin/accounts/index.blade.php | 36 +++--- resources/views/admin/accounts/new.blade.php | 96 ++++++++-------- resources/views/admin/index.blade.php | 6 +- .../views/admin/locations/index.blade.php | 38 +++--- resources/views/admin/nodes/index.blade.php | 38 +++--- resources/views/admin/nodes/new.blade.php | 8 +- resources/views/admin/servers/index.blade.php | 38 +++--- resources/views/auth/reset.blade.php | 6 +- resources/views/base/account.blade.php | 108 +++++++++--------- resources/views/emails/new_password.blade.php | 22 ++-- resources/views/emails/password.blade.php | 22 ++-- resources/views/errors/installing.blade.php | 8 +- 17 files changed, 240 insertions(+), 238 deletions(-) diff --git a/app/Http/Controllers/API/UserController.php b/app/Http/Controllers/API/UserController.php index e3cc7d9d7..006d73ed0 100644 --- a/app/Http/Controllers/API/UserController.php +++ b/app/Http/Controllers/API/UserController.php @@ -24,7 +24,7 @@ class UserController extends BaseController * @Get("/{?page}") * @Versions({"v1"}) * @Parameters({ - * @Parameter("page", type="integer", description="The page of results to view.", default=1) + * @Parameter("page", type="integer", description="The page of results to view.", default=1) * }) * @Response(200) */ @@ -42,8 +42,8 @@ class UserController extends BaseController * @Get("/{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.") + * @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) */ @@ -68,18 +68,18 @@ class UserController extends BaseController * @Post("/") * @Versions({"v1"}) * @Transaction({ - * @Request({ - * "email": "foo@example.com", - * "password": "foopassword", - * "admin": false + * @Request({ + * "email": "foo@example.com", + * "password": "foopassword", + * "admin": false * }, headers={"Authorization": "Bearer "}), * @Response(200, body={"id": 1}), * @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."] + * "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 * }) @@ -106,9 +106,9 @@ class UserController extends BaseController * @Patch("/{id}") * @Versions({"v1"}) * @Transaction({ - * @Request({ - * "email": "new@email.com" - * }, headers={"Authorization": "Bearer "}), + * @Request({ + * "email": "new@email.com" + * }, headers={"Authorization": "Bearer "}), * @Response(200, body={"email": "new@email.com"}), * @Response(422) * }) @@ -127,12 +127,12 @@ class UserController extends BaseController * @Delete("/{id}") * @Versions({"v1"}) * @Transaction({ - * @Request(headers={"Authorization": "Bearer "}), - * @Response(204), - * @Response(422) + * @Request(headers={"Authorization": "Bearer "}), + * @Response(204), + * @Response(422) * }) * @Parameters({ - * @Parameter("id", type="integer", required=true, description="The ID of the user to delete.") + * @Parameter("id", type="integer", required=true, description="The ID of the user to delete.") * }) */ public function deleteUser(Request $request, $id) diff --git a/app/Models/User.php b/app/Models/User.php index df2aa8afe..742f194bc 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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,})'. diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php index aa633d392..62f690871 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/UserRepository.php @@ -84,6 +84,8 @@ class UserRepository */ public function delete($id) { + // @TODO cannot delete user with associated servers! + // clean up subusers! return User::destroy($id); } diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php index 3e7428f63..6c30d699d 100644 --- a/resources/lang/de/auth.php +++ b/resources/lang/de/auth.php @@ -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. */ ]; /* diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php index 346d3448b..e029d182c 100644 --- a/resources/lang/de/validation.php +++ b/resources/lang/de/validation.php @@ -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.', diff --git a/resources/views/admin/accounts/index.blade.php b/resources/views/admin/accounts/index.blade.php index 57ba64c19..98ae0dfb2 100644 --- a/resources/views/admin/accounts/index.blade.php +++ b/resources/views/admin/accounts/index.blade.php @@ -7,35 +7,35 @@ @section('content')
+
  • Admin Control
  • +
  • Accounts
  • +

    All Registered Users


    - - - - +
    Email
    + + + - - - - @foreach ($users as $user) - - + + + + @foreach ($users as $user) + + - - @endforeach - -
    Email Account Created Account Updated
    {{ $user->email }} @if($user->root_admin === 1)Administrator@endif
    {{ $user->email }} @if($user->root_admin === 1)Administrator@endif {{ $user->created_at }} {{ $user->updated_at }}
    + + @endforeach + +
    {!! $users->render() !!}
    @endsection diff --git a/resources/views/admin/accounts/new.blade.php b/resources/views/admin/accounts/new.blade.php index 398daa03a..0eefdbadf 100644 --- a/resources/views/admin/accounts/new.blade.php +++ b/resources/views/admin/accounts/new.blade.php @@ -10,65 +10,65 @@
  • Admin Controls
  • Accounts
  • Add New Account
  • - +

    Create New Account


    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    {!! csrf_field() !!} - - -
    -
    -
    - + + +
    +
    +
    + @endsection diff --git a/resources/views/admin/index.blade.php b/resources/views/admin/index.blade.php index 4f6dac515..f950c6a1d 100644 --- a/resources/views/admin/index.blade.php +++ b/resources/views/admin/index.blade.php @@ -7,14 +7,14 @@ @section('content')
    +
  • Admin Control
  • +

    Pterodactyl Admin Control Panel


    Welcome to the most advanced, lightweight, and user-friendly open source game server control panel.

    @endsection diff --git a/resources/views/admin/locations/index.blade.php b/resources/views/admin/locations/index.blade.php index be73c9503..c84dd238e 100644 --- a/resources/views/admin/locations/index.blade.php +++ b/resources/views/admin/locations/index.blade.php @@ -7,37 +7,37 @@ @section('content')
    +
  • Admin Control
  • +
  • Locations
  • +

    All Locations


    - - - - +
    Location
    + + + - + - - - - @foreach ($locations as $location) - - + + + + @foreach ($locations as $location) + + - - @endforeach - -
    Location DescriptionNodesNodes Servers
    {{ $location->short }}
    {{ $location->short }} {{ $location->long }} {{ $location->a_nodeCount }} {{ $location->a_serverCount }}
    + + @endforeach + +
    {!! $locations->render() !!}
    @endsection diff --git a/resources/views/admin/nodes/index.blade.php b/resources/views/admin/nodes/index.blade.php index d8286f8b1..a6dee7ced 100644 --- a/resources/views/admin/nodes/index.blade.php +++ b/resources/views/admin/nodes/index.blade.php @@ -7,27 +7,27 @@ @section('content')
    +
  • Admin Control
  • +
  • Nodes
  • +

    All Nodes


    - - - - +
    Name
    + + + - + - - - - @foreach ($nodes as $node) - - + + + + @foreach ($nodes as $node) + + @@ -35,17 +35,17 @@ - - @endforeach - -
    Name LocationFQDNFQDN HTTPS Public
    {{ $node->name }}
    {{ $node->name }} {{ $node->a_locationName }} {{ $node->fqdn }}
    + + @endforeach + +
    {!! $nodes->render() !!}
    @endsection diff --git a/resources/views/admin/nodes/new.blade.php b/resources/views/admin/nodes/new.blade.php index d63ee4075..53269dbb8 100644 --- a/resources/views/admin/nodes/new.blade.php +++ b/resources/views/admin/nodes/new.blade.php @@ -7,10 +7,10 @@ @section('content')
    +
  • Create New Node
  • +

    Create New Node


    @@ -158,7 +158,7 @@
    @endsection diff --git a/resources/views/auth/reset.blade.php b/resources/views/auth/reset.blade.php index 4b0eb2f45..3cbd78db2 100644 --- a/resources/views/auth/reset.blade.php +++ b/resources/views/auth/reset.blade.php @@ -14,20 +14,20 @@ {{ trans('auth.resetpassword') }}
    - +
    -
    +
    -
    +
    diff --git a/resources/views/base/account.blade.php b/resources/views/base/account.blade.php index 516e93cff..7378398aa 100644 --- a/resources/views/base/account.blade.php +++ b/resources/views/base/account.blade.php @@ -7,61 +7,61 @@ @section('content')
    -
    -
    -

    {{ trans('base.account.update_pass') }}


    - -
    - -
    - -
    -
    -
    - -
    - +
    +
    +

    {{ trans('base.account.update_pass') }}


    + +
    + +
    + +
    +
    +
    + +
    +

    {{ trans('base.password_req') }}

    -
    -
    -
    - -
    - -
    -
    -
    -
    - {!! csrf_field() !!} - -
    -
    - -
    -
    -

    {{ trans('base.account.update_email') }}


    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - {!! csrf_field() !!} - -
    -
    -
    -
    -
    +
    +
    +
    + +
    + +
    +
    +
    +
    + {!! csrf_field() !!} + +
    +
    + +
    +
    +

    {{ trans('base.account.update_email') }}


    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + {!! csrf_field() !!} + +
    +
    +
    +
    +