diff --git a/app/Commands/CommandResponse.php b/app/Commands/CommandResponse.php index ddb5e62f..7af02cd0 100644 --- a/app/Commands/CommandResponse.php +++ b/app/Commands/CommandResponse.php @@ -30,11 +30,16 @@ class CommandResponse private $_validator; private $_response; private $_didFail; + /** + * @var int Used for HTTP responses. + */ + private $_statusCode; - public static function fail($validatorOrMessages) + public static function fail($validatorOrMessages, int $statusCode = 400) { $response = new CommandResponse(); $response->_didFail = true; + $response->_statusCode = $statusCode; if (is_array($validatorOrMessages)) { $response->_messages = $validatorOrMessages; @@ -47,11 +52,12 @@ class CommandResponse return $response; } - public static function succeed($response = null) + public static function succeed($response = null, int $statusCode = 200) { $cmdResponse = new CommandResponse(); $cmdResponse->_didFail = false; $cmdResponse->_response = $response; + $cmdResponse->_statusCode = $statusCode; return $cmdResponse; } @@ -76,6 +82,14 @@ class CommandResponse return $this->_response; } + /** + * @return int + */ + public function getStatusCode():int + { + return $this->_statusCode; + } + /** * @return Validator */ diff --git a/app/Commands/CreateUserCommand.php b/app/Commands/CreateUserCommand.php new file mode 100644 index 00000000..9870a2cd --- /dev/null +++ b/app/Commands/CreateUserCommand.php @@ -0,0 +1,93 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Gate; +use Poniverse\Ponyfm\Models\User; +use Validator; + +class CreateUserCommand extends CommandBase +{ + private $username; + private $displayName; + private $email; + private $createArchivedUser; + + public function __construct( + string $username, + string $displayName, + string $email = null, + bool $createArchivedUser = false + ) { + $this->username = $username; + $this->displayName = $displayName; + $this->email = $email; + $this->createArchivedUser = $createArchivedUser; + } + + /** + * @return bool + */ + public function authorize() + { + return Gate::allows('create-user'); + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + $rules = [ + 'username' => config('ponyfm.validation_rules.username'), + 'display_name' => config('ponyfm.validation_rules.display_name'), + 'email' => 'email', + 'create_archived_user' => 'boolean', + ]; + + $validator = Validator::make([ + 'username' => $this->username, + 'display_name' => $this->displayName, + ], $rules); + + if ($validator->fails()) { + return CommandResponse::fail([ + 'message' => $validator->getMessageBag()->first(), + 'user' => null + ]); + } + + // Attempt to create the user. + $user = User::findOrCreate($this->username, $this->displayName, $this->email, $this->createArchivedUser); + if ($user->wasRecentlyCreated) { + return CommandResponse::succeed([ + 'message' => 'New user successfully created!', + 'user' => User::mapPublicUserSummary($user) + ], 201); + } else { + return CommandResponse::fail([ + 'message' => 'A user with that username already exists.', + 'user' => User::mapPublicUserSummary($user) + ], 409); + } + } +} diff --git a/app/Commands/SaveAccountSettingsCommand.php b/app/Commands/SaveAccountSettingsCommand.php index 8454a53c..91901977 100644 --- a/app/Commands/SaveAccountSettingsCommand.php +++ b/app/Commands/SaveAccountSettingsCommand.php @@ -89,7 +89,7 @@ class SaveAccountSettingsCommand extends CommandBase 'slug' => [ 'required', 'unique:users,slug,'.$this->_user->id, - 'min:3', + 'min:'.config('ponyfm.user_slug_minimum_length'), 'regex:/^[a-z\d-]+$/', 'is_not_reserved_slug' ] diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index ba061245..1e52d708 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -44,4 +44,9 @@ class AdminController extends Controller { return View::make('shared.null'); } + + public function getUsers() + { + return View::make('shared.null'); + } } diff --git a/app/Http/Controllers/Api/Web/ArtistsController.php b/app/Http/Controllers/Api/Web/ArtistsController.php index 1f3015fc..03946b11 100644 --- a/app/Http/Controllers/Api/Web/ArtistsController.php +++ b/app/Http/Controllers/Api/Web/ArtistsController.php @@ -21,6 +21,7 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use Gate; +use Poniverse\Ponyfm\Commands\CreateUserCommand; use Poniverse\Ponyfm\Models\Album; use Poniverse\Ponyfm\Models\Comment; use Poniverse\Ponyfm\Models\Favourite; @@ -29,9 +30,9 @@ use Poniverse\Ponyfm\Models\Image; use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\User; use Poniverse\Ponyfm\Models\Follower; -use Illuminate\Support\Facades\App; -use Illuminate\Support\Facades\Input; -use Illuminate\Support\Facades\Response; +use App; +use Input; +use Response; use ColorThief\ColorThief; use Helpers; @@ -228,4 +229,9 @@ class ArtistsController extends ApiControllerBase return Response::json(["artists" => $users, "current_page" => $page, "total_pages" => ceil($count / $perPage)], 200); } + + public function postIndex() { + $name = Input::json('username'); + return $this->execute(new CreateUserCommand($name, $name, null, true)); + } } diff --git a/app/Http/Controllers/ApiControllerBase.php b/app/Http/Controllers/ApiControllerBase.php index 414f93f2..a80e301c 100644 --- a/app/Http/Controllers/ApiControllerBase.php +++ b/app/Http/Controllers/ApiControllerBase.php @@ -43,10 +43,10 @@ abstract class ApiControllerBase extends Controller return Response::json([ 'message' => 'Validation failed', 'errors' => $result->getMessages() - ], 400); + ], $result->getStatusCode()); } - return Response::json($result->getResponse(), 200); + return Response::json($result->getResponse(), $result->getStatusCode()); } public function notAuthorized() diff --git a/app/Http/routes.php b/app/Http/routes.php index 96b68f56..dbfdfe3e 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -101,6 +101,7 @@ Route::group(['prefix' => 'api/web'], function() { Route::get('/comments/{type}/{id}', 'Api\Web\CommentsController@getIndex')->where('id', '\d+'); Route::get('/artists', 'Api\Web\ArtistsController@getIndex'); + Route::post('/artists', 'Api\Web\ArtistsController@postIndex'); Route::get('/artists/{slug}', 'Api\Web\ArtistsController@getShow'); Route::get('/artists/{slug}/content', 'Api\Web\ArtistsController@getContent'); Route::get('/artists/{slug}/favourites', 'Api\Web\ArtistsController@getFavourites'); @@ -175,6 +176,7 @@ Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-ar Route::get('/genres', 'AdminController@getGenres'); Route::get('/tracks', 'AdminController@getTracks'); Route::get('/show-songs', 'AdminController@getShowSongs'); + Route::get('/users', 'AdminController@getUsers'); Route::get('/', 'AdminController@getIndex'); }); diff --git a/app/Models/User.php b/app/Models/User.php index bf739743..dc82ccb6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -20,6 +20,7 @@ namespace Poniverse\Ponyfm\Models; +use DB; use Gravatar; use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Passwords\CanResetPassword; @@ -114,6 +115,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon */ private static function getUniqueSlugForName(string $name):string { $baseSlug = Str::slug($name); + + // Ensure that the slug we generate is long enough. + for ($i = Str::length($baseSlug); $i < config('ponyfm.user_slug_minimum_length'); $i++) { + $baseSlug = $baseSlug.'-'; + } + $slugBeingTried = $baseSlug; $counter = 2; @@ -135,15 +142,23 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } /** - * @param string $username + * @param string $username used to perform the search * @param string $displayName - * @param string $email + * @param string|null $email set to null if creating an archived user + * @param bool $createArchivedUser if true, includes archived users in the search and creates an archived user * @return User */ - public static function findOrCreate(string $username, string $displayName, string $email) { - $user = static::where('username', $username) - ->where('is_archived', false) - ->first(); + public static function findOrCreate( + string $username, + string $displayName, + string $email = null, + bool $createArchivedUser = false + ) { + $user = static::where(DB::raw('LOWER(username)'), Str::lower($username)); + if (false === $createArchivedUser) { + $user = $user->where('is_archived', false); + } + $user = $user->first(); if (null !== $user) { return $user; @@ -156,7 +171,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon $user->slug = self::getUniqueSlugForName($displayName); $user->email = $email; $user->uses_gravatar = true; + $user->is_archived = $createArchivedUser; $user->save(); + $user = $user->fresh(); + $user->wasRecentlyCreated = true; return $user; } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 9a6abbe2..b20dadb2 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -68,6 +68,10 @@ class AuthServiceProvider extends ServiceProvider return $user->hasRole('admin'); }); + $gate->define('create-user', function(User $user) { + return $user->hasRole('admin'); + }); + $this->registerPolicies($gate); } } diff --git a/config/ponyfm.php b/config/ponyfm.php index c94ed5c1..6d7ae949 100644 --- a/config/ponyfm.php +++ b/config/ponyfm.php @@ -89,4 +89,31 @@ return [ 'indexing_queue' => 'indexing', + /* + |-------------------------------------------------------------------------- + | Global validation rules + |-------------------------------------------------------------------------- + | + | Data fields that are validated in multiple places have their validation + | rules centralized here. + | + */ + + 'validation_rules' => [ + 'username' => ['required', 'min:3', 'max:26'], + 'display_name' => ['required', 'min:3', 'max:26'], + ], + + /* + |-------------------------------------------------------------------------- + | Minimum length of a user slug + |-------------------------------------------------------------------------- + | + | No profile slugs shorter than this will be generated. This setting is + | intended to pre-emptively avoid collisions with very short URL's that may + | be desirable for future site functionality. + | + */ + + 'user_slug_minimum_length' => 3 ]; diff --git a/package.json b/package.json index 37609566..3a5dc6a4 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "packages": {}, "dependencies": {}, "devDependencies": { - "angular": "^1.5.0", + "angular": "^1.5.6", "angular-chart.js": "^1.0.0-alpha6", "angular-strap": "^2.3.8", "angular-ui-router": "^0.2.18", diff --git a/public/templates/admin/_layout.html b/public/templates/admin/_layout.html index 6e485deb..9a610319 100644 --- a/public/templates/admin/_layout.html +++ b/public/templates/admin/_layout.html @@ -2,6 +2,7 @@
Archived profiles are for organizing music by artists who aren't on Pony.fm themselves. They can always be claimed by the artist and converted to a "proper" profile later.
+ +Use the universal search in the sidebar. /)
+Ask a Pony.fm developer or sysadmin to run the following for you:
./artisan accounts:merge+