1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6declare(strict_types=1);
7
8namespace App\Http\Controllers;
9
10use App\Exceptions\User\PasswordResetFailException;
11use App\Libraries\User\PasswordResetData;
12use App\Models\User;
13use App\Models\UserAccountHistory;
14use Carbon\CarbonImmutable;
15
16class PasswordResetController extends Controller
17{
18 private static function getUser(?string $username): ?User
19 {
20 return present($username) ? User::findForLogin($username, true) : null;
21 }
22
23 public function __construct()
24 {
25 parent::__construct();
26
27 $this->middleware('guest');
28 $this->middleware('throttle:60,10');
29 $this->middleware('throttle:20,1440,password-reset:');
30 }
31
32 public function create()
33 {
34 $username = get_string(\Request::input('username'));
35 $user = static::getUser($username);
36 $error = PasswordResetData::create($user, $username);
37
38 if ($error === null) {
39 \Session::flash('popup', osu_trans('password_reset.notice.sent'));
40
41 return ujs_redirect(route('password-reset.reset', [
42 'username' => $username,
43 ]));
44 } else {
45 return response(['form_error' => [
46 'username' => [$error],
47 ]], 422);
48 }
49 }
50
51 public function index()
52 {
53 return ext_view('password_reset.index');
54 }
55
56 public function resendMail()
57 {
58 $username = get_string(\Request::input('username'));
59 $user = static::getUser($username) ?? abort(422);
60 $data = PasswordResetData::find($user, $username);
61
62 if ($data === null) {
63 \Session::flash('popup', osu_trans('password_reset.error.expired'));
64
65 return ujs_redirect(route('password-reset'));
66 } elseif ($data->sendMail()) {
67 $data->save();
68 }
69
70 return ['message' => osu_trans('password_reset.notice.sent')];
71 }
72
73 public function reset()
74 {
75 $username = presence(get_string(\Request::input('username'))) ?? abort(422);
76
77 return ext_view('password_reset.reset', compact('username'));
78 }
79
80 public function update()
81 {
82 $params = get_params(\Request::all(), null, [
83 'key',
84 'user.password',
85 'user.password_confirmation',
86 'username',
87 ], ['null_missing' => true]);
88
89 try {
90 $user = static::getUser($params['username'])
91 ?? throw new PasswordResetFailException('invalid');
92 $data = PasswordResetData::find($user, $params['username'])
93 ?? throw new PasswordResetFailException('invalid');
94
95 if (!$data->isActive()) {
96 throw new PasswordResetFailException('expired');
97 }
98
99 $params['key'] = strtr($params['key'] ?? '', [' ' => '']);
100 if (!present($params['key'])) {
101 return response(['form_error' => [
102 'key' => [osu_trans('password_reset.error.missing_key')],
103 ]], 422);
104 }
105
106 if (!$data->isValidKey($params['key'])) {
107 if (!$data->hasMoreTries()) {
108 throw new PasswordResetFailException('too_many_tries');
109 }
110
111 $data->save();
112
113 return response(['form_error' => [
114 'key' => [osu_trans('password_reset.error.wrong_key')],
115 ]], 422);
116 }
117 } catch (PasswordResetFailException $e) {
118 if (isset($data)) {
119 $data->delete();
120 }
121
122 \Session::flash('popup', osu_trans("password_reset.error.{$e->getMessage()}"));
123
124 return ujs_redirect(route('password-reset'));
125 }
126
127 $user->validatePasswordConfirmation();
128 $params['user']['user_lastvisit'] = CarbonImmutable::now();
129 if ($user->update($params['user'])) {
130 $user->resetSessions();
131 $this->login($user);
132
133 UserAccountHistory::logUserResetPassword($user);
134 $data->delete();
135
136 \Session::flash('popup', osu_trans('password_reset.notice.saved'));
137
138 return ujs_redirect(route('home'));
139 } else {
140 return response(['form_error' => [
141 'user' => $user->validationErrors()->all(),
142 ]], 422);
143 }
144 }
145}