diff --git a/app/DeactivatedUser.php b/app/DeactivatedUser.php new file mode 100644 index 0000000..7cc4b55 --- /dev/null +++ b/app/DeactivatedUser.php @@ -0,0 +1,18 @@ + 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users', + 'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users|unique:deactivated_users', 'email' => 'required|string|email|max:255|unique:users', 'password' => 'required|string|min:6|confirmed' ]; diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 76c9676..78636a9 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -2,10 +2,14 @@ namespace App\Http\Controllers; +use App\DeactivatedUser; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use Illuminate\Validation\ValidationException; class SettingController extends Controller { @@ -67,6 +71,51 @@ class SettingController extends Controller return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。'); } + public function deactivate() + { + return view('setting.deactivate'); + } + + public function destroyUser(Request $request) + { + // パスワードチェック + $validated = $request->validate([ + 'password' => 'required|string' + ]); + + if (!Hash::check($validated['password'], Auth::user()->getAuthPassword())) { + throw ValidationException::withMessages([ + 'password' => 'パスワードが正しくありません。' + ]); + } + + // データの削除 + set_time_limit(0); + DB::transaction(function () { + $user = Auth::user(); + + // 関連レコードの削除 + // TODO: 別にDELETE文相当のクエリを一発発行するだけでもいい? + foreach ($user->ejaculations as $ejaculation) { + $ejaculation->delete(); + } + foreach ($user->likes as $like) { + $like->delete(); + } + + // 先にログアウトしないとユーザーは消せない + Auth::logout(); + + // ユーザーの削除 + $user->delete(); + + // ユーザー名履歴に追記 + DeactivatedUser::create(['name' => $user->name]); + }); + + return view('setting.deactivated'); + } + // ( ◠‿◠ )☛ここに気づいたか・・・消えてもらう ▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああああ // public function password() // { diff --git a/app/User.php b/app/User.php index 1cafaa2..c82e5d0 100644 --- a/app/User.php +++ b/app/User.php @@ -53,6 +53,11 @@ class User extends Authenticatable return Auth::check() && $this->id === Auth::user()->id; } + public function ejaculations() + { + return $this->hasMany(Ejaculation::class); + } + public function likes() { return $this->hasMany(Like::class); diff --git a/database/factories/EjaculationFactory.php b/database/factories/EjaculationFactory.php new file mode 100644 index 0000000..65fbb6e --- /dev/null +++ b/database/factories/EjaculationFactory.php @@ -0,0 +1,12 @@ +define(Ejaculation::class, function (Faker $faker) { + return [ + 'ejaculated_date' => $faker->date('Y-m-d H:i:s'), + 'note' => $faker->text, + ]; +}); diff --git a/database/factories/LikeFactory.php b/database/factories/LikeFactory.php new file mode 100644 index 0000000..38e82a4 --- /dev/null +++ b/database/factories/LikeFactory.php @@ -0,0 +1,10 @@ +define(App\Like::class, function (Faker $faker) { + return [ + // + ]; +}); diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php deleted file mode 100644 index 7926c79..0000000 --- a/database/factories/ModelFactory.php +++ /dev/null @@ -1,24 +0,0 @@ -define(App\User::class, function (Faker\Generator $faker) { - static $password; - - return [ - 'name' => $faker->name, - 'email' => $faker->unique()->safeEmail, - 'password' => $password ?: $password = bcrypt('secret'), - 'remember_token' => str_random(10), - ]; -}); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..fed18ec --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,21 @@ +define(App\User::class, function (Faker\Generator $faker) { + static $password; + + return [ + 'name' => substr($faker->userName, 0, 15), + 'email' => $faker->unique()->safeEmail, + 'password' => $password ?: $password = bcrypt('secret'), + 'remember_token' => str_random(10), + 'display_name' => substr($faker->name, 0, 20), + 'is_protected' => false, + 'accept_analytics' => false, + 'private_likes' => false, + ]; +}); + +$factory->state(App\User::class, 'protected', [ + 'is_protected' => true, +]); diff --git a/database/migrations/2019_11_14_003449_create_deactivated_users_table.php b/database/migrations/2019_11_14_003449_create_deactivated_users_table.php new file mode 100644 index 0000000..3e4f475 --- /dev/null +++ b/database/migrations/2019_11_14_003449_create_deactivated_users_table.php @@ -0,0 +1,33 @@ +string('name', 15); + $table->timestamps(); + + $table->primary('name'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('deactivated_users'); + } +} diff --git a/resources/assets/js/setting/deactivate.js b/resources/assets/js/setting/deactivate.js new file mode 100644 index 0000000..e99acd0 --- /dev/null +++ b/resources/assets/js/setting/deactivate.js @@ -0,0 +1,5 @@ +$('#deactivate-form').on('submit', function () { + if (!confirm('本当にアカウントを削除してもよろしいですか?')) { + return false; + } +}); diff --git a/resources/views/setting/base.blade.php b/resources/views/setting/base.blade.php index d3274cf..2d53e94 100644 --- a/resources/views/setting/base.blade.php +++ b/resources/views/setting/base.blade.php @@ -10,6 +10,8 @@ href="{{ route('setting') }}"> プロフィール プライバシー + アカウントの削除 {{-- パスワード--}} @@ -19,4 +21,4 @@ -@endsection \ No newline at end of file +@endsection diff --git a/resources/views/setting/deactivate.blade.php b/resources/views/setting/deactivate.blade.php new file mode 100644 index 0000000..07df952 --- /dev/null +++ b/resources/views/setting/deactivate.blade.php @@ -0,0 +1,32 @@ +@extends('setting.base') + +@section('title', 'アカウントの削除') + +@section('tab-content') +

アカウントの削除

+
+

Tissueからあなたのアカウントに関する情報を削除します。

+
+

警告

+

削除はすぐに実行され、取り消すことはできません!

+

なりすましを防止するため、あなたのユーザー名はサーバーに記録されます。今後、同じユーザー名を使って再登録することはできません。

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

上記の条件に同意してアカウントを削除する場合は、パスワードを入力して削除ボタンを押してください。

+ + + @if ($errors->has('password')) +
{{ $errors->first('password') }}
+ @endif +
+ + +
+@endsection + +@push('script') + +@endpush diff --git a/resources/views/setting/deactivated.blade.php b/resources/views/setting/deactivated.blade.php new file mode 100644 index 0000000..1e5fafa --- /dev/null +++ b/resources/views/setting/deactivated.blade.php @@ -0,0 +1,16 @@ +@extends('layouts.base') + +@section('title', 'アカウント削除完了') + +@section('content') +
+

アカウントを削除しました

+
+

Tissueをご利用いただき、ありがとうございました。

+

トップページへ

+
+@endsection + +@push('script') + +@endpush diff --git a/routes/web.php b/routes/web.php index 6c0fbdb..184bd18 100644 --- a/routes/web.php +++ b/routes/web.php @@ -36,6 +36,8 @@ Route::middleware('auth')->group(function () { Route::post('/setting/profile', 'SettingController@updateProfile')->name('setting.profile.update'); Route::get('/setting/privacy', 'SettingController@privacy')->name('setting.privacy'); Route::post('/setting/privacy', 'SettingController@updatePrivacy')->name('setting.privacy.update'); + Route::get('/setting/deactivate', 'SettingController@deactivate')->name('setting.deactivate'); + Route::post('/setting/deactivate', 'SettingController@destroyUser')->name('setting.deactivate.destroy'); // Route::get('/setting/password', 'SettingController@password')->name('setting.password'); }); diff --git a/tests/Feature/SettingTest.php b/tests/Feature/SettingTest.php new file mode 100644 index 0000000..c54fd84 --- /dev/null +++ b/tests/Feature/SettingTest.php @@ -0,0 +1,64 @@ +create(); + $ejaculation = factory(Ejaculation::class)->create(['user_id' => $user->id]); + + $anotherUser = factory(User::class)->create(); + $anotherEjaculation = factory(Ejaculation::class)->create(['user_id' => $anotherUser->id]); + + $like = factory(Like::class)->create([ + 'user_id' => $user->id, + 'ejaculation_id' => $anotherEjaculation->id, + ]); + $anotherLike = factory(Like::class)->create([ + 'user_id' => $anotherUser->id, + 'ejaculation_id' => $ejaculation->id, + ]); + + $token = $this->getCsrfToken($user, '/setting/deactivate'); + $response = $this->actingAs($user) + ->followingRedirects() + ->post('/setting/deactivate', [ + '_token' => $token, + 'password' => 'secret', + ]); + + $response->assertStatus(200) + ->assertViewIs('setting.deactivated'); + $this->assertGuest(); + $this->assertDatabaseMissing('users', ['id' => $user->id]); + $this->assertDatabaseMissing('ejaculations', ['id' => $ejaculation->id]); + $this->assertDatabaseMissing('likes', ['id' => $like->id]); + $this->assertDatabaseMissing('likes', ['id' => $anotherLike->id]); + $this->assertDatabaseHas('deactivated_users', ['name' => $user->name]); + } + + /** + * テスト対象を呼び出す前にGETリクエストを行い、CSRFトークンを得る + * @param Authenticatable $user 認証情報 + * @param string $uri リクエスト先 + * @return string CSRFトークン + */ + private function getCsrfToken(Authenticatable $user, string $uri): string + { + $response = $this->actingAs($user)->get($uri); + $crawler = new Crawler($response->getContent()); + + return $crawler->filter('input[name=_token]')->attr('value'); + } +} diff --git a/webpack.mix.js b/webpack.mix.js index 3532d8b..bf24e44 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -16,6 +16,7 @@ mix.js('resources/assets/js/app.js', 'public/js') .js('resources/assets/js/home.js', 'public/js') .js('resources/assets/js/user/stats.js', 'public/js/user') .js('resources/assets/js/setting/privacy.js', 'public/js/setting') + .js('resources/assets/js/setting/deactivate.js', 'public/js/setting') .ts('resources/assets/js/checkin.ts', 'public/js') .sass('resources/assets/sass/app.scss', 'public/css') .autoload({