Model fixing, moving things around to improve code.

Adds unique UUID generator, moves functions into repositories for
adding servers and users, cleans up code, adding more comments.
This commit is contained in:
Dane Everitt 2015-12-13 22:22:16 -05:00
parent 01eaeaf178
commit 22b0bbf6ce
8 changed files with 333 additions and 250 deletions

View file

@ -3,11 +3,9 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Alert; use Alert;
use Debugbar;
use Hash;
use Uuid;
use Pterodactyl\Models\User; use Pterodactyl\Models\User;
use Pterodactyl\Repositories\UserRepository;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -52,18 +50,19 @@ class AccountsController extends Controller
'password_confirmation' => 'required' 'password_confirmation' => 'required'
]); ]);
//@TODO: re-generate UUID if conflict try {
$user = new User;
$user->uuid = Uuid::generate(4);
$user->username = $request->input('username'); $user = new UserRepository;
$user->email = $request->input('email'); $userid = $user->create($request->input('username'), $request->input('email'), $request->input('password'));
$user->password = Hash::make($request->input('password'));
$user->save(); Alert::success('Account has been successfully created.')->flash();
return redirect()->route('admin.accounts.view', ['id' => $userid]);
} catch (\Exception $e) {
Alert::danger('An error occured while attempting to add a new user. Please check the logs or try again.')->flash();
return redirect()->route('admin.accounts.new');
}
Alert::success('Account has been successfully created.')->flash();
return redirect()->route('admin.accounts.view', ['id' => $user->id]);
} }
} }

View file

@ -3,6 +3,7 @@
namespace Pterodactyl\Http\Controllers\Admin; namespace Pterodactyl\Http\Controllers\Admin;
use Debugbar; use Debugbar;
use Pterodactyl\Repositories\ServerRepository;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Models\Node; use Pterodactyl\Models\Node;
use Pterodactyl\Models\Location; use Pterodactyl\Models\Location;
@ -56,7 +57,8 @@ class ServersController extends Controller
{ {
try { try {
$resp = Server::addServer($request->all()); $server = new ServerRepository;
$resp = $server->create($request->all());
echo $resp . '<br />'; echo $resp . '<br />';
} catch (\Exception $e) { } catch (\Exception $e) {
Debugbar::addException($e); Debugbar::addException($e);

View file

@ -3,14 +3,12 @@
namespace Pterodactyl\Http\Controllers\Base; namespace Pterodactyl\Http\Controllers\Base;
use Auth; use Auth;
use Debugbar; use Hash;
use Google2FA; use Google2FA;
use Log;
use Alert; use Alert;
use Pterodactyl\Exceptions\AccountNotFoundException;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Models\User;
use Pterodactyl\Models\Server; use Pterodactyl\Models\Server;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -74,22 +72,18 @@ class IndexController extends Controller
public function putAccountTotp(Request $request) public function putAccountTotp(Request $request)
{ {
try { $user = $request->user();
$totpSecret = User::setTotpSecret(Auth::user()->id);
} catch (\Exception $e) { $user->totp_secret = Google2FA::generateSecretKey();
if ($e instanceof AccountNotFoundException) { $user->save();
return response($e->getMessage(), 500);
}
throw $e;
}
return response()->json([ return response()->json([
'qrImage' => Google2FA::getQRCodeGoogleUrl( 'qrImage' => Google2FA::getQRCodeGoogleUrl(
'Pterodactyl', 'Pterodactyl',
Auth::user()->email, $user->email,
$totpSecret $user->totp_secret
), ),
'secret' => $totpSecret 'secret' => $user->totp_secret
]); ]);
} }
@ -104,21 +98,16 @@ class IndexController extends Controller
{ {
if (!$request->has('token')) { if (!$request->has('token')) {
return response('No input \'token\' defined.', 500); return response(null, 500);
} }
try { $user = $request->user();
if(User::toggleTotp(Auth::user()->id, $request->input('token'))) { if($user->toggleTotp($request->input('token'))) {
return response('true'); return response('true');
}
return response('false');
} catch (\Exception $e) {
if ($e instanceof AccountNotFoundException) {
return response($e->getMessage(), 500);
}
throw $e;
} }
return response('false');
} }
/** /**
@ -135,21 +124,14 @@ class IndexController extends Controller
return redirect()->route('account.totp'); return redirect()->route('account.totp');
} }
try { $user = $request->user();
if(User::toggleTotp(Auth::user()->id, $request->input('token'))) { if($user->toggleTotp($request->input('token'))) {
return redirect()->route('account.totp');
}
Alert::danger('Unable to disable TOTP on this account, was the token correct?')->flash();
return redirect()->route('account.totp'); return redirect()->route('account.totp');
} catch (\Exception $e) {
if ($e instanceof AccountNotFoundException) {
Alert::danger('An error occured while attempting to perform this action.')->flash();
return redirect()->route('account.totp');
}
throw $e;
} }
Alert::danger('The TOTP token provided was invalid.')->flash();
return redirect()->route('account.totp');
} }
/** /**
@ -177,23 +159,19 @@ class IndexController extends Controller
'password' => 'required' 'password' => 'required'
]); ]);
if (!password_verify($request->input('password'), Auth::user()->password)) { $user = $request->user();
if (!password_verify($request->input('password'), $user->password)) {
Alert::danger('The password provided was not valid for this account.')->flash(); Alert::danger('The password provided was not valid for this account.')->flash();
return redirect()->route('account'); return redirect()->route('account');
} }
// Met Validation, lets roll out. $user->email = $request->input('new_email');
try { $user->save();
User::setEmail(Auth::user()->id, $request->input('new_email'));
Alert::success('Your email address has successfully been updated.')->flash(); Alert::success('Your email address has successfully been updated.')->flash();
return redirect()->route('account'); return redirect()->route('account');
} catch (\Exception $e) {
if ($e instanceof AccountNotFoundException || $e instanceof DisplayException) {
Alert::danger($e->getMessage())->flash();
return redirect()->route('account');
}
throw $e;
}
} }
/** /**
@ -211,24 +189,22 @@ class IndexController extends Controller
'new_password_confirmation' => 'required' 'new_password_confirmation' => 'required'
]); ]);
if (!password_verify($request->input('current_password'), Auth::user()->password)) { $user = $request->user();
if (!password_verify($request->input('current_password'), $user->password)) {
Alert::danger('The password provided was not valid for this account.')->flash(); Alert::danger('The password provided was not valid for this account.')->flash();
return redirect()->route('account'); return redirect()->route('account');
} }
// Met Validation, lets roll out.
try { try {
User::setPassword(Auth::user()->id, $request->input('new_password')); $user->setPassword($request->input('new_password'));
Alert::success('Your password has successfully been updated.')->flash(); Alert::success('Your password has successfully been updated.')->flash();
return redirect()->route('account'); } catch (DisplayException $e) {
} catch (\Exception $e) { Alert::danger($e->getMessage())->flash();
if ($e instanceof AccountNotFoundException || $e instanceof DisplayException) {
Alert::danger($e->getMessage())->flash();
return redirect()->route('account');
}
throw $e;
} }
return redirect()->route('account');
} }
} }

View file

@ -3,16 +3,8 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use Auth; use Auth;
use DB;
use Debugbar;
use Validator;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\AccountNotFoundException;
use Pterodactyl\Exceptions\DisplayValidationException;
use Pterodactyl\Models;
use Pterodactyl\Models\Subuser;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Server extends Model class Server extends Model
@ -50,18 +42,6 @@ class Server extends Model
self::$user = Auth::user(); self::$user = Auth::user();
} }
protected static function generateSFTPUsername($name)
{
$name = preg_replace('/\s+/', '', $name);
if (strlen($name) > 6) {
return strtolower('ptdl-' . substr($name, 0, 6) . '_' . str_random(5));
}
return strtolower('ptdl-' . $name . '_' . str_random((11 - strlen($name))));
}
/** /**
* Determine if we need to change the server's daemonSecret value to * Determine if we need to change the server's daemonSecret value to
* match that of the user if they are a subuser. * match that of the user if they are a subuser.
@ -76,7 +56,7 @@ class Server extends Model
return $server->daemonSecret; return $server->daemonSecret;
} }
$subuser = Models\Subuser::where('server_id', $server->id)->where('user_id', self::$user->id)->first(); $subuser = Subuser::where('server_id', $server->id)->where('user_id', self::$user->id)->first();
if (is_null($subuser)) { if (is_null($subuser)) {
return null; return null;
@ -101,7 +81,7 @@ class Server extends Model
->where('active', 1); ->where('active', 1);
if (self::$user->root_admin !== 1) { if (self::$user->root_admin !== 1) {
$query->whereIn('servers.id', Models\Subuser::accessServers()); $query->whereIn('servers.id', Subuser::accessServers());
} }
return $query->get(); return $query->get();
@ -124,7 +104,7 @@ class Server extends Model
$query = self::where('uuidShort', $uuid)->where('active', 1); $query = self::where('uuidShort', $uuid)->where('active', 1);
if (self::$user->root_admin !== 1) { if (self::$user->root_admin !== 1) {
$query->whereIn('servers.id', Models\Subuser::accessServers()); $query->whereIn('servers.id', Subuser::accessServers());
} }
$result = $query->first(); $result = $query->first();
@ -158,84 +138,4 @@ class Server extends Model
} }
/**
* Adds a new server to the system.
* @param array $data An array of data descriptors for creating the server. These should align to the columns in the database.
*/
public static function addServer(array $data)
{
// Validate Fields
$validator = Validator::make($data, [
'owner' => 'required|email|exists:users,email',
'node' => 'required|numeric|min:1|exists:nodes,id',
'name' => 'required|regex:([\w -]{4,35})',
'memory' => 'required|numeric|min:1',
'disk' => 'required|numeric|min:1',
'cpu' => 'required|numeric|min:0',
'io' => 'required|numeric|min:10|max:1000',
'ip' => 'required|ip',
'port' => 'required|numeric|min:1|max:65535',
'service' => 'required|numeric|min:1|exists:services,id',
'option' => 'required|numeric|min:1|exists:service_options,id',
'custom_image_name' => 'required_if:use_custom_image,on',
]);
// 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(json_encode($validator->errors()->all()));
}
// Get the User ID; user exists since we passed the 'exists:users,email' part of the validation
$user = Models\User::select('id')->where('email', $data['owner'])->first();
// Verify IP & Port are a.) free and b.) assigned to the node.
// We know the node exists because of 'exists:nodes,id' in the validation
$node = Models\Node::find($data['node']);
$allocation = Models\Allocation::where('ip', $data['ip'])->where('port', $data['port'])->where('node', $data['node'])->whereNull('assigned_to')->first();
// Something failed in the query, either that combo doesn't exist, or it is in use.
if (!$allocation) {
throw new DisplayException('The selected IP/Port combination (' . $data['ip'] . ':' . $data['port'] . ') is either already in use, or unavaliable for this node.');
}
// Validate those Service Option Variables
// We know the service and option exists because of the validation.
// We need to verify that the option exists for the service, and then check for
// any required variable fields. (fields are labeled env_<env_variable>)
$option = Models\ServiceOptions::where('id', $data['option'])->where('parent_service', $data['service'])->first();
if (!$option) {
throw new DisplayException('The requested service option does not exist for the specified service.');
}
// Check those Variables
$variables = Models\ServiceVariables::where('option_id', $data['option'])->get();
if ($variables) {
foreach($variables as $variable) {
// Is the variable required?
if (!$data['env_' . $variable->env_variable]) {
if ($variable->required === 1) {
throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.');
}
$data['env_' . $variable->env_variable] = $variable->default_value;
continue;
}
// Check aganist Regex Pattern
if (!is_null($variable->regex) && !preg_match($variable->regex, $data['env_' . $variable->env_variable])) {
throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').');
}
continue;
}
}
return self::generateSFTPUsername($data['name']);
}
} }

View file

@ -2,6 +2,7 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use Hash;
use Google2FA; use Google2FA;
use Pterodactyl\Exceptions\AccountNotFoundException; use Pterodactyl\Exceptions\AccountNotFoundException;
use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayException;
@ -47,51 +48,21 @@ class User extends Model implements AuthenticatableContract,
return $this->hasMany(Permission::class); return $this->hasMany(Permission::class);
} }
/**
* Sets the TOTP secret for an account.
*
* @param int $id Account ID for which we want to generate a TOTP secret
* @return string
*/
public static function setTotpSecret($id)
{
$totpSecretKey = Google2FA::generateSecretKey();
$user = User::find($id);
if (!$user) {
throw new AccountNotFoundException('An account with that ID (' . $id . ') does not exist in the system.');
}
$user->totp_secret = $totpSecretKey;
$user->save();
return $totpSecretKey;
}
/** /**
* Enables or disables TOTP on an account if the token is valid. * Enables or disables TOTP on an account if the token is valid.
* *
* @param int $id Account ID for which we want to generate a TOTP secret * @param int $token The token that we want to verify.
* @return boolean * @return boolean
*/ */
public static function toggleTotp($id, $token) public function toggleTotp($token)
{ {
$user = User::find($id); if (!Google2FA::verifyKey($this->totp_secret, $token)) {
if (!$user) {
throw new AccountNotFoundException('An account with that ID (' . $id . ') does not exist in the system.');
}
if (!Google2FA::verifyKey($user->totp_secret, $token)) {
return false; return false;
} }
$user->use_totp = ($user->use_totp === 1) ? 0 : 1; $this->use_totp = !$this->use_totp;
$user->save(); $this->save();
return true; return true;
@ -104,51 +75,19 @@ class User extends Model implements AuthenticatableContract,
* - at least one lowercase character * - at least one lowercase character
* - at least one number * - at least one number
* *
* @param int $id The ID of the account to update the password on.
* @param string $password The raw password to set the account password to. * @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,})'. * @param string $regex The regex to use when validating the password. Defaults to '((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})'.
* @return void * @return void
*/ */
public static function setPassword($id, $password, $regex = '((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})') public function setPassword($password, $regex = '((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})')
{ {
$user = User::find($id);
if (!$user) {
throw new AccountNotFoundException('An account with that ID (' . $id . ') does not exist in the system.');
}
if (!preg_match($regex, $password)) { if (!preg_match($regex, $password)) {
throw new DisplayException('The password passed did not meet the minimum password requirements.'); throw new DisplayException('The password passed did not meet the minimum password requirements.');
} }
$user->password = password_hash($password, PASSWORD_BCRYPT); $this->password = Hash::make($password);
$user->save(); $this->save();
return;
}
/**
* Updates the email address for an account.
*
* @param int $id
* @param string $email
* @return void
*/
public static function setEmail($id, $email)
{
$user = User::find($id);
if (!$user) {
throw new AccountNotFoundException('An account with that ID (' . $id . ') does not exist in the system.');
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new DisplayException('The email provided (' . $email . ') was not valid.');
}
$user->email = $email;
$user->save();
return; return;

View file

@ -0,0 +1,122 @@
<?php
namespace Pterodactyl\Repositories;
use DB;
use Validator;
use Pterodactyl\Models;
use Pterodactyl\Services\UuidService;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Exceptions\AccountNotFoundException;
use Pterodactyl\Exceptions\DisplayValidationException;
class ServerRepository
{
public function __construct()
{
//
}
/**
* Generates a SFTP username for a server given a server name.
*
* @param string $name
* @return string
*/
protected function generateSFTPUsername($name)
{
$name = preg_replace('/\s+/', '', $name);
if (strlen($name) > 6) {
return strtolower('ptdl-' . substr($name, 0, 6) . '_' . str_random(5));
}
return strtolower('ptdl-' . $name . '_' . str_random((11 - strlen($name))));
}
/**
* Adds a new server to the system.
* @param array $data An array of data descriptors for creating the server. These should align to the columns in the database.
*/
public function create(array $data)
{
// Validate Fields
$validator = Validator::make($data, [
'owner' => 'required|email|exists:users,email',
'node' => 'required|numeric|min:1|exists:nodes,id',
'name' => 'required|regex:([\w -]{4,35})',
'memory' => 'required|numeric|min:1',
'disk' => 'required|numeric|min:1',
'cpu' => 'required|numeric|min:0',
'io' => 'required|numeric|min:10|max:1000',
'ip' => 'required|ip',
'port' => 'required|numeric|min:1|max:65535',
'service' => 'required|numeric|min:1|exists:services,id',
'option' => 'required|numeric|min:1|exists:service_options,id',
'custom_image_name' => 'required_if:use_custom_image,on',
]);
// 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(json_encode($validator->errors()->all()));
}
// Get the User ID; user exists since we passed the 'exists:users,email' part of the validation
$user = Models\User::select('id')->where('email', $data['owner'])->first();
// Verify IP & Port are a.) free and b.) assigned to the node.
// We know the node exists because of 'exists:nodes,id' in the validation
$node = Models\Node::find($data['node']);
$allocation = Models\Allocation::where('ip', $data['ip'])->where('port', $data['port'])->where('node', $data['node'])->whereNull('assigned_to')->first();
// Something failed in the query, either that combo doesn't exist, or it is in use.
if (!$allocation) {
throw new DisplayException('The selected IP/Port combination (' . $data['ip'] . ':' . $data['port'] . ') is either already in use, or unavaliable for this node.');
}
// Validate those Service Option Variables
// We know the service and option exists because of the validation.
// We need to verify that the option exists for the service, and then check for
// any required variable fields. (fields are labeled env_<env_variable>)
$option = Models\ServiceOptions::where('id', $data['option'])->where('parent_service', $data['service'])->first();
if (!$option) {
throw new DisplayException('The requested service option does not exist for the specified service.');
}
// Check those Variables
$variables = Models\ServiceVariables::where('option_id', $data['option'])->get();
if ($variables) {
foreach($variables as $variable) {
// Is the variable required?
if (!$data['env_' . $variable->env_variable]) {
if ($variable->required === 1) {
throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.');
}
$data['env_' . $variable->env_variable] = $variable->default_value;
continue;
}
// Check aganist Regex Pattern
if (!is_null($variable->regex) && !preg_match($variable->regex, $data['env_' . $variable->env_variable])) {
throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').');
}
continue;
}
}
return (new UuidService)->generateShort();
//return $this->generateSFTPUsername($data['name']);
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Pterodactyl\Repositories;
use Hash;
use Pterodactyl\Models\User;
use Pterodactyl\Services\UuidService;
class UserRepository
{
public function __construct()
{
//
}
/**
* Creates a user on the panel. Returns the created user's ID.
*
* @param string $username
* @param string $email
* @param string $password An unhashed version of the user's password.
* @return integer
*/
public function create($username, $email, $password)
{
$user = new User;
$uuid = new UuidService;
$user->uuid = $uuid->table('users')->generate();
$user->username = $username;
$user->email = $email;
$user->password = Hash::make($password);
$user->save();
return $user->id;
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace Pterodactyl\Services;
use DB;
use Uuid;
class UuidService
{
/**
* @var string
*/
protected $table = 'users';
/**
* @var string
*/
protected $field = 'uuid';
/**
* Constructor
*/
public function __construct()
{
//
}
/**
* Set the table that we need to be checking in the database.
*
* @param string $table
* @return void
*/
public function table($table)
{
$this->table = $table;
return $this;
}
/**
* Set the field in the given table that we want to check for a unique UUID.
*
* @param string $field
* @return void
*/
public function field($field)
{
$this->field = $field;
return $this;
}
/**
* Generate a unique UUID validating against specified table and column.
* Defaults to `users.uuid`
*
* @param integer $type The type of UUID to generate.
* @return string
*/
public function generate($type = 4)
{
$return = false;
do {
$uuid = LaravelUUID::generate($type);
if (!DB::table($this->table)->where($this->field, $uuid)->exists()) {
$return = $uuid;
}
} while (!$return);
return $return;
}
/**
* Generates a ShortUUID code which is 8 characters long and is used for identifying servers in the system.
*
* @param string $table
* @param string $field
* @return string
*/
public function generateShort($table = 'servers', $field = 'uuidShort')
{
$return = false;
do {
$short = substr(Uuid::generate(4), 0, 8);
if (!DB::table($table)->where($field, $short)->exists()) {
$return = $short;
}
} while (!$return);
return $return;
}
}