Skip to content

Commit 2c6f940

Browse files
committed
feat: Backend Support for changing Passwords and Emails with Testing
1 parent 023d31e commit 2c6f940

6 files changed

Lines changed: 289 additions & 1 deletion

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Validation\Rule;
8+
9+
class EmailController extends Controller {
10+
public function update(Request $request) {
11+
$request->merge(['current_email' => $request->user()->email]);
12+
13+
$validated = $request->validate([
14+
'password' => ['required', 'current_password'],
15+
'email' => ['required', 'email', 'different:current_email'],
16+
Rule::unique('users', 'email')->ignore($request->user()->id),
17+
], [
18+
'email.different' => 'You already use this email.',
19+
]);
20+
21+
$request->user()->update([
22+
'email' => $validated['email'],
23+
]);
24+
25+
return response(null, 204);
26+
}
27+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Hash;
8+
use Illuminate\Validation\Rules\Password;
9+
10+
class PasswordController extends Controller {
11+
public function update(Request $request) {
12+
$validated = $request->validate([
13+
'current_password' => ['required', 'current_password'],
14+
'password' => ['required', Password::defaults(), 'confirmed'],
15+
]);
16+
17+
$request->user()->update([
18+
'password' => Hash::make($validated['password']),
19+
]);
20+
21+
return response(null, 204);
22+
}
23+
24+
/**
25+
* Handle an incoming new password request. (Starter Pack Demo Code)
26+
*
27+
* @throws \Illuminate\Validation\ValidationException
28+
*/
29+
public function store(Request $request) {
30+
$request->validate([
31+
'token' => 'required',
32+
'email' => 'required|email',
33+
// 'password' => ['required', 'confirmed', Rules\Password::defaults()],
34+
]);
35+
36+
// Here we will attempt to reset the user's password. If it is successful we
37+
// will update the password on an actual user model and persist it to the
38+
// database. Otherwise we will parse the error and return the response.
39+
// $status = Password::reset(
40+
// $request->only('email', 'password', 'password_confirmation', 'token'),
41+
// function ($user) use ($request) {
42+
// $user->forceFill([
43+
// 'password' => Hash::make($request->password),
44+
// 'remember_token' => Str::random(60),
45+
// ])->save();
46+
47+
// event(new PasswordReset($user));
48+
// }
49+
// );
50+
51+
// If the password was successfully reset, we will redirect the user back to
52+
// the application's home authenticated view. If there is an error we can
53+
// redirect them back to where they came from with their error message.
54+
// if ($status == Password::PasswordReset) {
55+
// return to_route('login')->with('status', __($status));
56+
// }
57+
58+
// throw ValidationException::withMessages([
59+
// 'email' => [__($status)],
60+
// ]);
61+
}
62+
}

resources/js/types/requests.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,14 @@ export interface SeriesUpdateRequest {
3939
tags: { name: string; id: number; folder_tag_id?: number }[];
4040
deleted_tags: number[];
4141
}
42+
43+
export interface ChangePasswordRequest {
44+
current_password: string;
45+
password: string;
46+
password_confirmation: string;
47+
}
48+
49+
export interface ChangeEmailRequest {
50+
email: string;
51+
password: string;
52+
}

routes/api.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
use App\Http\Controllers\Api\V1\AnalyticsController;
44
use App\Http\Controllers\Api\V1\AuthController;
55
use App\Http\Controllers\Api\V1\CategoryController;
6+
use App\Http\Controllers\Api\V1\EmailController;
67
use App\Http\Controllers\Api\V1\ExternalMetadataController;
78
use App\Http\Controllers\Api\V1\FolderController;
89
use App\Http\Controllers\Api\V1\JobController;
910
use App\Http\Controllers\Api\V1\MetadataController;
11+
use App\Http\Controllers\Api\V1\PasswordController;
1012
use App\Http\Controllers\Api\V1\PlaybackController;
1113
use App\Http\Controllers\Api\V1\ProfileController;
1214
use App\Http\Controllers\Api\V1\RecordController;
1315
use App\Http\Controllers\Api\V1\SeriesController;
16+
use App\Http\Controllers\API\V1\SessionController;
1417
use App\Http\Controllers\Api\V1\SubTaskController;
1518
use App\Http\Controllers\Api\V1\TagController;
1619
use App\Http\Controllers\Api\V1\TaskController;
@@ -32,6 +35,13 @@
3235
Route::get('/auth', [AuthController::class, 'authenticate']); // New
3336
Route::delete('/logout', [AuthController::class, 'destroy']); // New
3437

38+
Route::prefix('settings')->group(function () {
39+
Route::put('/password', [PasswordController::class, 'update'])->name('password.update');
40+
Route::get('/sessions', [SessionController::class, 'index'])->name('sessions.list');
41+
Route::put('/email', [EmailController::class, 'update'])->name('email.update');
42+
});
43+
44+
3545
Route::get('/active-sessions', [UserController::class, 'sessionCount']);
3646
Route::get('/user-view-count/{metadata}', [RecordController::class, 'userViewCount']);
3747
Route::get('/profiles/search/{username?}', [ProfileController::class, 'findUser']);
@@ -88,7 +98,7 @@
8898
// public
8999

90100
Route::post('/login', [AuthController::class, 'login']);
91-
Route::post('/register', [AuthController::class, 'register']);
101+
Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:6,1')->name('register');
92102

93103
Route::get('/manifest', function () {
94104
return response()->json(AppManifest::info());
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
namespace Tests\Feature\Settings;
4+
5+
use App\Models\User;
6+
use Illuminate\Foundation\Testing\RefreshDatabase;
7+
use Illuminate\Support\Facades\Hash;
8+
use Tests\TestCase;
9+
10+
class EmailControllerTest extends TestCase {
11+
use RefreshDatabase;
12+
13+
private const CHANGE_EMAIL_ENDPOINT = '/api/settings/email';
14+
15+
public function user_can_update_email_with_correct_password() {
16+
$user = User::factory()->create([
17+
'email' => 'test@eccc.ca',
18+
'password' => Hash::make('old-password'),
19+
]);
20+
21+
$this->actingAs($user);
22+
23+
$response = $this->putJson(self::CHANGE_EMAIL_ENDPOINT, [
24+
'email' => 'test2@eccc.ca',
25+
'password' => 'old-password',
26+
]);
27+
28+
$response->assertNoContent();
29+
$this->assertTrue($user->fresh()->email === 'test2@eccc.ca');
30+
}
31+
32+
public function update_fails_with_incorrect_password() {
33+
$user = User::factory()->create([
34+
'email' => 'test@eccc.ca',
35+
'password' => Hash::make('current-password'),
36+
]);
37+
38+
$this->actingAs($user);
39+
40+
$response = $this->putJson(self::CHANGE_EMAIL_ENDPOINT, [
41+
'email' => fake()->unique()->safeEmail,
42+
'password' => 'wrong-password',
43+
]);
44+
45+
$response->assertStatus(422);
46+
$response->assertJsonValidationErrors('password');
47+
}
48+
49+
public function update_fails_if_email_exists() {
50+
$testUser = $this->getFakeUser();
51+
$otherUser = $this->getFakeUser();
52+
53+
$user = User::factory()->create([
54+
'email' => $testUser['email'],
55+
'password' => Hash::make($testUser['password']),
56+
]);
57+
58+
User::factory()->create([
59+
'email' => $otherUser['email'],
60+
'password' => Hash::make($otherUser['password']),
61+
]);
62+
63+
$this->actingAs($user);
64+
65+
$response = $this->putJson(self::CHANGE_EMAIL_ENDPOINT, [
66+
'email' => $otherUser['email'],
67+
'password' => $testUser['password'],
68+
]);
69+
70+
$response->assertStatus(422);
71+
$response->assertJsonValidationErrors('email');
72+
}
73+
74+
public function update_fails_if_email_is_the_same() {
75+
$testUser = $this->getFakeUser();
76+
77+
$user = User::factory()->create([
78+
'email' => $testUser['email'],
79+
'password' => Hash::make('old-password'),
80+
]);
81+
82+
$this->actingAs($user);
83+
84+
$response = $this->putJson(self::CHANGE_EMAIL_ENDPOINT, [
85+
'email' => $testUser['email'],
86+
'password' => 'old-password',
87+
]);
88+
89+
$response->assertStatus(422);
90+
$response->assertJsonValidationErrors('email');
91+
}
92+
93+
private function getFakeUser(): array {
94+
return ["email" => fake()->unique()->safeEmail, "password" => fake()->password(8)];
95+
}
96+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Tests\Feature\Settings;
4+
5+
use App\Models\User;
6+
use Illuminate\Foundation\Testing\RefreshDatabase;
7+
use Illuminate\Support\Facades\Hash;
8+
use Tests\TestCase;
9+
10+
class PasswordControllerTest extends TestCase {
11+
use RefreshDatabase;
12+
13+
private const CHANGE_PASSWORD_ENDPOINT = '/api/settings/password';
14+
15+
public function user_can_update_password_with_correct_current_password() {
16+
$user = User::factory()->create([
17+
'password' => Hash::make('old-password'),
18+
]);
19+
20+
$this->actingAs($user);
21+
22+
$response = $this->putJson(self::CHANGE_PASSWORD_ENDPOINT, [
23+
'current_password' => 'old-password',
24+
'password' => 'new-secure-password',
25+
'password_confirmation' => 'new-secure-password',
26+
]);
27+
28+
$response->assertNoContent();
29+
$this->assertTrue(Hash::check('new-secure-password', $user->fresh()->password));
30+
}
31+
32+
public function update_fails_with_incorrect_current_password() {
33+
$user = User::factory()->create([
34+
'password' => Hash::make('old-password'),
35+
]);
36+
37+
$this->actingAs($user);
38+
39+
$response = $this->putJson(self::CHANGE_PASSWORD_ENDPOINT, [
40+
'current_password' => 'wrong-password',
41+
'password' => 'new-password',
42+
'password_confirmation' => 'new-password',
43+
]);
44+
45+
$response->assertStatus(422);
46+
$response->assertJsonValidationErrors('current_password');
47+
}
48+
49+
public function update_fails_if_passwords_do_not_match() {
50+
$user = User::factory()->create([
51+
'password' => Hash::make('old-password'),
52+
]);
53+
54+
$this->actingAs($user);
55+
56+
$response = $this->putJson(self::CHANGE_PASSWORD_ENDPOINT, [
57+
'current_password' => 'old-password',
58+
'password' => 'new-password',
59+
'password_confirmation' => 'wrong-confirmation',
60+
]);
61+
62+
$response->assertStatus(422);
63+
$response->assertJsonValidationErrors('password');
64+
}
65+
66+
public function update_fails_if_password_does_not_meet_requirements() {
67+
$user = User::factory()->create([
68+
'password' => Hash::make('old-password'),
69+
]);
70+
71+
$this->actingAs($user);
72+
73+
$response = $this->putJson(self::CHANGE_PASSWORD_ENDPOINT, [
74+
'current_password' => 'old-password',
75+
'password' => 'short', // invalid with Password::defaults() (not long enough)
76+
'password_confirmation' => 'short',
77+
]);
78+
79+
$response->assertStatus(422);
80+
$response->assertJsonValidationErrors('password');
81+
}
82+
}

0 commit comments

Comments
 (0)