Merge pull request #299 from shikorism/feature/263-deactivate-account

アカウント削除
This commit is contained in:
shibafu 2019-12-12 20:25:58 +09:00 committed by GitHub
commit 251d7b9108
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 272 additions and 26 deletions

18
app/DeactivatedUser.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* 削除済Userのユーザー名履歴
*/
class DeactivatedUser extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'name'
];
}

View File

@ -48,7 +48,7 @@ class RegisterController extends Controller
protected function validator(array $data)
{
$rules = [
'name' => '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'
];

View File

@ -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()
// {

View File

@ -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);

View File

@ -0,0 +1,12 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\Ejaculation;
use Faker\Generator as Faker;
$factory->define(Ejaculation::class, function (Faker $faker) {
return [
'ejaculated_date' => $faker->date('Y-m-d H:i:s'),
'note' => $faker->text,
];
});

View File

@ -0,0 +1,10 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use Faker\Generator as Faker;
$factory->define(App\Like::class, function (Faker $faker) {
return [
//
];
});

View File

@ -1,24 +0,0 @@
<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->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),
];
});

View File

@ -0,0 +1,21 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->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,
]);

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDeactivatedUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('deactivated_users', function (Blueprint $table) {
$table->string('name', 15);
$table->timestamps();
$table->primary('name');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('deactivated_users');
}
}

View File

@ -0,0 +1,5 @@
$('#deactivate-form').on('submit', function () {
if (!confirm('本当にアカウントを削除してもよろしいですか?')) {
return false;
}
});

View File

@ -10,6 +10,8 @@
href="{{ route('setting') }}"><span class="oi oi-person mr-1"></span> プロフィール</a>
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.privacy' ? 'active' : '' }}"
href="{{ route('setting.privacy') }}"><span class="oi oi-shield mr-1"></span> プライバシー</a>
<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.deactivate' ? 'active' : '' }}"
href="{{ route('setting.deactivate') }}"><span class="oi oi-trash mr-1"></span> アカウントの削除</a>
{{--<a class="list-group-item list-group-item-action {{ Route::currentRouteName() === 'setting.password' ? 'active' : '' }}"
href="{{ route('setting.password') }}"><span class="oi oi-key mr-1"></span> パスワード</a>--}}
</div>
@ -19,4 +21,4 @@
</div>
</div>
</div>
@endsection
@endsection

View File

@ -0,0 +1,32 @@
@extends('setting.base')
@section('title', 'アカウントの削除')
@section('tab-content')
<h3>アカウントの削除</h3>
<hr>
<p>Tissueからあなたのアカウントに関する情報を削除します。</p>
<div class="alert alert-danger">
<h4 class="alert-heading"><span class="oi oi-warning"></span> 警告</h4>
<p><strong>削除はすぐに実行され、取り消すことはできません!</strong></p>
<p class="my-0">なりすましを防止するため、あなたのユーザー名はサーバーに記録されます。今後、同じユーザー名を使って再登録することはできません。</p>
</div>
<form id="deactivate-form" action="{{ route('setting.deactivate.destroy') }}" method="post">
{{ csrf_field() }}
<div class="form-group">
<p>上記の条件に同意してアカウントを削除する場合は、パスワードを入力して削除ボタンを押してください。</p>
<input name="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" required>
@if ($errors->has('password'))
<div class="invalid-feedback">{{ $errors->first('password') }}</div>
@endif
</div>
<button type="submit" class="btn btn-danger mt-2">削除</button>
</form>
@endsection
@push('script')
<script src="{{ mix('js/setting/deactivate.js') }}"></script>
@endpush

View File

@ -0,0 +1,16 @@
@extends('layouts.base')
@section('title', 'アカウント削除完了')
@section('content')
<div class="container">
<h3>アカウントを削除しました</h3>
<hr>
<p>Tissueをご利用いただき、ありがとうございました。</p>
<p class="my-5 text-center"><a class="btn btn-link" href="{{ route('home') }}">トップページへ</a></p>
</div>
@endsection
@push('script')
<script src="{{ mix('js/setting/deactivate.js') }}"></script>
@endpush

View File

@ -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');
});

View File

@ -0,0 +1,64 @@
<?php
namespace Tests\Feature;
use App\Ejaculation;
use App\Like;
use App\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Symfony\Component\DomCrawler\Crawler;
use Tests\TestCase;
class SettingTest extends TestCase
{
public function testDestroyUser()
{
$user = factory(User::class)->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');
}
}

1
webpack.mix.js vendored
View File

@ -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({