diff --git a/app/Console/Commands/MergeDuplicateAccounts.php b/app/Console/Commands/MergeDuplicateAccounts.php new file mode 100644 index 00000000..9fb9c9bc --- /dev/null +++ b/app/Console/Commands/MergeDuplicateAccounts.php @@ -0,0 +1,162 @@ +. + */ + +namespace Poniverse\Ponyfm\Console\Commands; + +use Carbon\Carbon; +use DB; +use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Poniverse\Ponyfm\Album; +use Poniverse\Ponyfm\Comment; +use Poniverse\Ponyfm\Favourite; +use Poniverse\Ponyfm\Follower; +use Poniverse\Ponyfm\Image; +use Poniverse\Ponyfm\PinnedPlaylist; +use Poniverse\Ponyfm\Playlist; +use Poniverse\Ponyfm\ResourceLogItem; +use Poniverse\Ponyfm\ResourceUser; +use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\User; + +class MergeDuplicateAccounts extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'auth:merge-duplicates'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Merges duplicate accounts'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + // Get list of affected users + $usernames = DB::table('users') + ->select(['username', DB::raw('COUNT(*) as count')]) + ->whereNull('disabled_at') + ->groupBy(DB::raw('LOWER(username)')) + ->having('count', '>=', 2) + ->lists('username'); + + foreach($usernames as $username) { + // Find the relevant accounts + // ========================== + + /** @var Collection $accounts */ + $accounts = User::where('username', $username)->orderBy('created_at', 'ASC')->get(); + $firstAccount = $accounts[0]; + $accounts->forget(0); + $accountIds = $accounts->pluck('id'); + + + // Reassign content + // ================ + // This is done with the less-efficient-than-raw-SQL Eloquent + // methods to generate appropriate revision logs. + + $this->info('Merging duplicates for: '.$firstAccount->username); + DB::transaction(function() use ($accounts, $accountIds, $firstAccount) { + foreach (Album::whereIn('user_id', $accountIds)->get() as $album) { + $album->user_id = $firstAccount->id; + $album->save(); + } + + foreach (Comment::whereIn('user_id', $accountIds)->get() as $comment) { + $comment->user_id = $firstAccount->id; + $comment->save(); + } + + foreach (Favourite::whereIn('user_id', $accountIds)->get() as $favourite) { + $favourite->user_id = $firstAccount->id; + $favourite->save(); + } + + foreach (Follower::whereIn('artist_id', $accountIds)->get() as $follow) { + $follow->artist_id = $firstAccount->id; + $follow->save(); + } + + foreach (Image::whereIn('uploaded_by', $accountIds)->get() as $image) { + $image->uploaded_by = $firstAccount->id; + $image->save(); + } + + foreach (Image::whereIn('uploaded_by', $accountIds)->get() as $image) { + $image->uploaded_by = $firstAccount->id; + $image->save(); + } + + DB::table('oauth2_tokens')->whereIn('user_id', $accountIds)->update(['user_id' => $firstAccount->id]); + + foreach (PinnedPlaylist::whereIn('user_id', $accountIds)->get() as $playlist) { + $playlist->user_id = $firstAccount->id; + $playlist->save(); + } + + foreach (Playlist::whereIn('user_id', $accountIds)->get() as $playlist) { + $playlist->user_id = $firstAccount->id; + $playlist->save(); + } + + foreach (ResourceLogItem::whereIn('user_id', $accountIds)->get() as $item) { + $item->user_id = $firstAccount->id; + $item->save(); + } + + foreach (ResourceUser::whereIn('user_id', $accountIds)->get() as $item) { + $item->user_id = $firstAccount->id; + $item->save(); + } + + foreach (Track::whereIn('user_id', $accountIds)->get() as $track) { + $track->user_id = $firstAccount->id; + $track->save(); + } + + foreach($accounts as $account) { + $account->disabled_at = Carbon::now(); + $account->save(); + } + }); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ca89fb41..dd8f9de5 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -44,6 +44,7 @@ class Kernel extends ConsoleKernel \Poniverse\Ponyfm\Console\Commands\ClearTrackCache::class, \Poniverse\Ponyfm\Console\Commands\RebuildTrackCache::class, \Poniverse\Ponyfm\Console\Commands\RebuildFilesizes::class, + \Poniverse\Ponyfm\Console\Commands\MergeDuplicateAccounts::class, ]; /** diff --git a/app/Http/Controllers/Api/Web/ArtistsController.php b/app/Http/Controllers/Api/Web/ArtistsController.php index 3b27357d..b54044ae 100644 --- a/app/Http/Controllers/Api/Web/ArtistsController.php +++ b/app/Http/Controllers/Api/Web/ArtistsController.php @@ -112,6 +112,7 @@ class ArtistsController extends ApiControllerBase public function getShow($slug) { $user = User::whereSlug($slug) + ->whereNull('disabled_at') ->userDetails() ->with([ 'comments' => function ($query) { diff --git a/app/Http/Controllers/ArtistsController.php b/app/Http/Controllers/ArtistsController.php index 9e7a4c7a..6ecebfed 100644 --- a/app/Http/Controllers/ArtistsController.php +++ b/app/Http/Controllers/ArtistsController.php @@ -34,7 +34,7 @@ class ArtistsController extends Controller public function getProfile($slug) { - $user = User::whereSlug($slug)->first(); + $user = User::whereSlug($slug)->whereNull('disabled_at')->first(); if (!$user) { App::abort('404'); } @@ -45,10 +45,10 @@ class ArtistsController extends Controller public function getShortlink($id) { $user = User::find($id); - if (!$user) { + if (!$user || $user->disabled_at !== NULL) { App::abort('404'); } - return Redirect::action('ArtistsController@getProfile', [$id]); + return Redirect::action('ArtistsController@getProfile', [$user->slug]); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 9c51a069..2559c0a4 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -36,6 +36,7 @@ class Kernel extends HttpKernel \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Poniverse\Ponyfm\Http\Middleware\VerifyCsrfToken::class, + \Poniverse\Ponyfm\Http\Middleware\DisabledAccountCheck::class, \Poniverse\Ponyfm\Http\Middleware\Profiler::class, ]; diff --git a/app/Http/Middleware/DisabledAccountCheck.php b/app/Http/Middleware/DisabledAccountCheck.php new file mode 100644 index 00000000..ff4cf00a --- /dev/null +++ b/app/Http/Middleware/DisabledAccountCheck.php @@ -0,0 +1,65 @@ +. + */ + +namespace Poniverse\Ponyfm\Http\Middleware; + +use Closure; +use Illuminate\Contracts\Auth\Guard; +use Response; + +class DisabledAccountCheck +{ + /** + * The Guard implementation. + * + * @var Guard + */ + protected $auth; + + /** + * Create a new filter instance. + * + * @param Guard $auth + */ + public function __construct(Guard $auth) { + $this->auth = $auth; + } + + + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + if ($this->auth->check() + && $this->auth->user()->disabled_at !== null + && !($request->getMethod() === 'POST' && $request->getRequestUri() == '/auth/logout') + ){ + return Response::view('home.account-disabled', ['username' => $this->auth->user()->username], 403); + } + + return $next($request); + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 9289f759..c33f64ca 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -54,6 +54,7 @@ Route::get('playlists', 'PlaylistsController@getIndex'); Route::get('/register', 'AccountController@getRegister'); Route::get('/login', 'AuthController@getLogin'); +Route::post('/auth/logout', 'AuthController@postLogout'); Route::get('/auth/oauth', 'AuthController@getOAuth'); Route::get('/about', function() { return View::make('pages.about'); }); diff --git a/app/User.php b/app/User.php index 562d7727..50d6f785 100644 --- a/app/User.php +++ b/app/User.php @@ -46,6 +46,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon 'avatar_id' => 'integer', 'is_archived' => 'boolean', ]; + protected $dates = ['created_at', 'updated_at', 'disabled_at']; + protected $hidden = ['disabled_at']; public function scopeUserDetails($query) { diff --git a/database/migrations/2015_12_29_152005_add_account_disabled_column.php b/database/migrations/2015_12_29_152005_add_account_disabled_column.php new file mode 100644 index 00000000..347f9b79 --- /dev/null +++ b/database/migrations/2015_12_29_152005_add_account_disabled_column.php @@ -0,0 +1,49 @@ +. + */ + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class AddAccountDisabledColumn extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::table('users', function(Blueprint $table){ + $table->dateTime('disabled_at')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function(Blueprint $table){ + $table->dropColumn('disabled_at'); + }); + } +} diff --git a/resources/views/home/account-disabled.blade.php b/resources/views/home/account-disabled.blade.php new file mode 100644 index 00000000..14304f20 --- /dev/null +++ b/resources/views/home/account-disabled.blade.php @@ -0,0 +1,43 @@ +{{-- + Pony.fm - A community for pony fan music. + Copyright (C) 2015 Peter Deltchev + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +--}} + + Account disabled :: Pony.fm + + + +

Account disabled

+

Your Pony.fm account, {{ $username }}, has been disabled.

+

If you believe this to be in error, + contact feld0@pony.fm.

+

+ + {{ csrf_field() }} +

+ +