PasswordResetController.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Illuminate\Http\Request;
  4. use Illuminate\Support\Facades\DB;
  5. use Illuminate\Support\Facades\Hash;
  6. use Illuminate\Support\Facades\Mail;
  7. use Illuminate\Support\Facades\Log;
  8. use Illuminate\Support\Str;
  9. use Carbon\Carbon;
  10. class PasswordResetController extends Controller
  11. {
  12. /**
  13. * Show password reset request form
  14. */
  15. public function showResetRequestForm()
  16. {
  17. return view('auth.password-reset-request');
  18. }
  19. /**
  20. * Send password reset email
  21. */
  22. public function sendResetEmail(Request $request)
  23. {
  24. $request->validate([
  25. 'email' => 'required|email'
  26. ], [
  27. 'email.required' => 'La mail è obbligatoria',
  28. 'email.email' => 'Inserisci un indirizzo email valido'
  29. ]);
  30. $this->logSecurityEvent('reset_requested', $request->email);
  31. try {
  32. $user = $this->findUserInMasterDatabase($request->email);
  33. if (!$user) {
  34. $this->logSecurityEvent('reset_requested_invalid_email', $request->email);
  35. return back()->with('success', 'Se l\'email esiste nel sistema, riceverai le istruzioni per il reset della password.');
  36. }
  37. if ($this->hasRecentResetRequest($request->email)) {
  38. $this->logSecurityEvent('reset_requested_too_soon', $request->email);
  39. return back()->with('error', 'È già stata inviata una richiesta di reset per questa email. Controlla la tua casella di posta o riprova più tardi.');
  40. }
  41. $token = Str::random(64);
  42. $this->storeResetToken($request->email, $token);
  43. $this->sendPasswordResetEmail($request->email, $token, $user);
  44. $this->logSecurityEvent('reset_email_sent', $request->email);
  45. return back()->with('success', 'Se l\'email esiste nel sistema, riceverai le istruzioni per il reset della password.');
  46. } catch (\Exception $e) {
  47. $this->logSecurityEvent('reset_request_failed', $request->email, ['error' => $e->getMessage()]);
  48. return back()->with('error', 'Errore durante l\'invio dell\'email. Riprova più tardi.');
  49. }
  50. }
  51. /**
  52. * Show password reset form
  53. */
  54. public function showResetForm($token)
  55. {
  56. return view('auth.password-reset-form', ['token' => $token]);
  57. }
  58. /**
  59. * Reset password
  60. */
  61. public function resetPassword(Request $request)
  62. {
  63. $request->validate([
  64. 'token' => 'required',
  65. 'email' => 'required|email',
  66. 'password' => 'required|min:6|confirmed'
  67. ], [
  68. 'email.required' => 'La mail è obbligatoria',
  69. 'email.email' => 'Inserisci un indirizzo email valido',
  70. 'password.required' => 'La password è obbligatoria',
  71. 'password.min' => 'La password deve essere di almeno 6 caratteri',
  72. 'password.confirmed' => 'Le password non coincidono'
  73. ]);
  74. try {
  75. $resetRecord = $this->verifyResetToken($request->email, $request->token);
  76. if (!$resetRecord) {
  77. return back()->with('error', 'Token non valido o scaduto.');
  78. }
  79. $user = $this->findUserInMasterDatabase($request->email);
  80. if (!$user) {
  81. return back()->with('error', 'Utente non trovato.');
  82. }
  83. $this->updatePasswordInBothDatabases($request->email, $request->password, $user);
  84. $this->deleteResetToken($request->email);
  85. Log::info('Password reset completed', [
  86. 'email' => $request->email
  87. ]);
  88. return redirect('/')->with('success', 'Password aggiornata con successo. Puoi ora effettuare il login.');
  89. } catch (\Exception $e) {
  90. Log::error('Password reset failed', [
  91. 'email' => $request->email,
  92. 'error' => $e->getMessage()
  93. ]);
  94. return back()->with('error', 'Errore durante il reset della password. Riprova più tardi.');
  95. }
  96. }
  97. /**
  98. * Find user in master database
  99. */
  100. private function findUserInMasterDatabase($email)
  101. {
  102. try {
  103. $masterConfig = [
  104. 'driver' => 'mysql',
  105. 'host' => env('DB_HOST', '127.0.0.1'),
  106. 'port' => env('DB_PORT', '3306'),
  107. 'database' => env('DB_DATABASE'),
  108. 'username' => env('DB_USERNAME'),
  109. 'password' => env('DB_PASSWORD'),
  110. 'charset' => 'utf8mb4',
  111. 'collation' => 'utf8mb4_unicode_ci',
  112. 'prefix' => '',
  113. 'strict' => true,
  114. 'engine' => null,
  115. ];
  116. config(['database.connections.master_reset' => $masterConfig]);
  117. $user = DB::connection('master_reset')
  118. ->table('users')
  119. ->where('email', $email)
  120. ->first();
  121. DB::purge('master_reset');
  122. return $user;
  123. } catch (\Exception $e) {
  124. Log::error('Failed to find user in master database', [
  125. 'email' => $email,
  126. 'error' => $e->getMessage()
  127. ]);
  128. return null;
  129. }
  130. }
  131. /**
  132. * Store reset token in master database
  133. */
  134. private function storeResetToken($email, $token)
  135. {
  136. try {
  137. $masterConfig = [
  138. 'driver' => 'mysql',
  139. 'host' => env('DB_HOST', '127.0.0.1'),
  140. 'port' => env('DB_PORT', '3306'),
  141. 'database' => env('DB_DATABASE'),
  142. 'username' => env('DB_USERNAME'),
  143. 'password' => env('DB_PASSWORD'),
  144. 'charset' => 'utf8mb4',
  145. 'collation' => 'utf8mb4_unicode_ci',
  146. 'prefix' => '',
  147. 'strict' => true,
  148. 'engine' => null,
  149. ];
  150. config(['database.connections.master_reset_token' => $masterConfig]);
  151. DB::connection('master_reset_token')
  152. ->table('password_resets')
  153. ->where('email', $email)
  154. ->delete();
  155. DB::connection('master_reset_token')
  156. ->table('password_resets')
  157. ->insert([
  158. 'email' => $email,
  159. 'token' => Hash::make($token),
  160. 'created_at' => Carbon::now()
  161. ]);
  162. DB::purge('master_reset_token');
  163. } catch (\Exception $e) {
  164. Log::error('Failed to store reset token', [
  165. 'email' => $email,
  166. 'error' => $e->getMessage()
  167. ]);
  168. throw $e;
  169. }
  170. }
  171. /**
  172. * Verify reset token
  173. */
  174. private function verifyResetToken($email, $token)
  175. {
  176. try {
  177. $masterConfig = [
  178. 'driver' => 'mysql',
  179. 'host' => env('DB_HOST', '127.0.0.1'),
  180. 'port' => env('DB_PORT', '3306'),
  181. 'database' => env('DB_DATABASE'),
  182. 'username' => env('DB_USERNAME'),
  183. 'password' => env('DB_PASSWORD'),
  184. 'charset' => 'utf8mb4',
  185. 'collation' => 'utf8mb4_unicode_ci',
  186. 'prefix' => '',
  187. 'strict' => true,
  188. 'engine' => null,
  189. ];
  190. config(['database.connections.master_verify' => $masterConfig]);
  191. $resetRecord = DB::connection('master_verify')
  192. ->table('password_resets')
  193. ->where('email', $email)
  194. ->first();
  195. DB::purge('master_verify');
  196. if (!$resetRecord) {
  197. return null;
  198. }
  199. $isValidToken = Hash::check($token, $resetRecord->token);
  200. $isNotExpired = Carbon::parse($resetRecord->created_at)->addHours(24)->isFuture();
  201. if ($isValidToken && $isNotExpired) {
  202. return $resetRecord;
  203. }
  204. return null;
  205. } catch (\Exception $e) {
  206. Log::error('Failed to verify reset token', [
  207. 'email' => $email,
  208. 'error' => $e->getMessage()
  209. ]);
  210. return null;
  211. }
  212. }
  213. /**
  214. * Update password in both master and tenant databases
  215. */
  216. private function updatePasswordInBothDatabases($email, $newPassword, $masterUser)
  217. {
  218. $hashedPassword = Hash::make($newPassword);
  219. try {
  220. $masterConfig = [
  221. 'driver' => 'mysql',
  222. 'host' => env('DB_HOST', '127.0.0.1'),
  223. 'port' => env('DB_PORT', '3306'),
  224. 'database' => env('DB_DATABASE'),
  225. 'username' => env('DB_USERNAME'),
  226. 'password' => env('DB_PASSWORD'),
  227. 'charset' => 'utf8mb4',
  228. 'collation' => 'utf8mb4_unicode_ci',
  229. 'prefix' => '',
  230. 'strict' => true,
  231. 'engine' => null,
  232. ];
  233. config(['database.connections.master_update' => $masterConfig]);
  234. DB::connection('master_update')
  235. ->table('users')
  236. ->where('email', $email)
  237. ->update(['password' => $hashedPassword]);
  238. DB::purge('master_update');
  239. Log::info('Password updated in master database', ['email' => $email]);
  240. if ($masterUser->tenant_database) {
  241. $tenantConfig = [
  242. 'driver' => 'mysql',
  243. 'host' => env('DB_HOST', '127.0.0.1'),
  244. 'port' => env('DB_PORT', '3306'),
  245. 'database' => $masterUser->tenant_database,
  246. 'username' => $masterUser->tenant_username,
  247. 'password' => $masterUser->tenant_password,
  248. 'charset' => 'utf8mb4',
  249. 'collation' => 'utf8mb4_unicode_ci',
  250. 'prefix' => '',
  251. 'strict' => true,
  252. 'engine' => null,
  253. ];
  254. config(['database.connections.tenant_update' => $tenantConfig]);
  255. DB::connection('tenant_update')
  256. ->table('users')
  257. ->where('email', $email)
  258. ->update(['password' => $hashedPassword]);
  259. DB::purge('tenant_update');
  260. Log::info('Password updated in tenant database', [
  261. 'email' => $email,
  262. 'tenant_database' => $masterUser->tenant_database
  263. ]);
  264. }
  265. } catch (\Exception $e) {
  266. Log::error('Failed to update password in databases', [
  267. 'email' => $email,
  268. 'error' => $e->getMessage()
  269. ]);
  270. throw $e;
  271. }
  272. }
  273. /**
  274. * Delete reset token
  275. */
  276. private function deleteResetToken($email)
  277. {
  278. try {
  279. $masterConfig = [
  280. 'driver' => 'mysql',
  281. 'host' => env('DB_HOST', '127.0.0.1'),
  282. 'port' => env('DB_PORT', '3306'),
  283. 'database' => env('DB_DATABASE'),
  284. 'username' => env('DB_USERNAME'),
  285. 'password' => env('DB_PASSWORD'),
  286. 'charset' => 'utf8mb4',
  287. 'collation' => 'utf8mb4_unicode_ci',
  288. 'prefix' => '',
  289. 'strict' => true,
  290. 'engine' => null,
  291. ];
  292. config(['database.connections.master_delete_token' => $masterConfig]);
  293. DB::connection('master_delete_token')
  294. ->table('password_resets')
  295. ->where('email', $email)
  296. ->delete();
  297. DB::purge('master_delete_token');
  298. } catch (\Exception $e) {
  299. Log::error('Failed to delete reset token', [
  300. 'email' => $email,
  301. 'error' => $e->getMessage()
  302. ]);
  303. }
  304. }
  305. /**
  306. * Send password reset email
  307. */
  308. private function sendPasswordResetEmail($email, $token, $user)
  309. {
  310. try {
  311. $resetUrl = url('/password-reset/' . $token . '?email=' . urlencode($email));
  312. $companyName = 'Leezard';
  313. $emailData = [
  314. 'name' => $user->name,
  315. 'email' => $email,
  316. 'reset_url' => $resetUrl,
  317. 'company' => $companyName,
  318. 'expires_at' => Carbon::now()->addHours(24)->format('d/m/Y H:i')
  319. ];
  320. Mail::send('emails.password-reset', $emailData, function ($message) use ($email, $companyName, $user) {
  321. $message->to($email, $user->name)
  322. ->subject('Reset Password - Leezard')
  323. ->from(config('mail.from.address'), config('mail.from.name'));
  324. });
  325. Log::info('Password reset email sent', ['email' => $email]);
  326. } catch (\Exception $e) {
  327. Log::error('Failed to send password reset email', [
  328. 'email' => $email,
  329. 'error' => $e->getMessage()
  330. ]);
  331. throw $e;
  332. }
  333. }
  334. private function logSecurityEvent($event, $email, $additionalData = [])
  335. {
  336. Log::info('Password Reset Security Event', array_merge([
  337. 'event' => $event,
  338. 'email' => $email,
  339. 'ip' => request()->ip(),
  340. 'user_agent' => request()->userAgent(),
  341. 'timestamp' => now()
  342. ], $additionalData));
  343. }
  344. private function hasRecentResetRequest($email)
  345. {
  346. try {
  347. $masterConfig = [
  348. 'driver' => 'mysql',
  349. 'host' => env('DB_HOST', '127.0.0.1'),
  350. 'port' => env('DB_PORT', '3306'),
  351. 'database' => env('DB_DATABASE'),
  352. 'username' => env('DB_USERNAME'),
  353. 'password' => env('DB_PASSWORD'),
  354. 'charset' => 'utf8mb4',
  355. 'collation' => 'utf8mb4_unicode_ci',
  356. 'prefix' => '',
  357. 'strict' => true,
  358. 'engine' => null,
  359. ];
  360. config(['database.connections.master_check_recent' => $masterConfig]);
  361. $recentRequest = DB::connection('master_check_recent')
  362. ->table('password_resets')
  363. ->where('email', $email)
  364. ->where('created_at', '>', Carbon::now()->subMinutes(5)) // 5 minutes cooldown
  365. ->first();
  366. DB::purge('master_check_recent');
  367. return $recentRequest !== null;
  368. } catch (\Exception $e) {
  369. Log::error('Failed to check recent reset requests', [
  370. 'email' => $email,
  371. 'error' => $e->getMessage()
  372. ]);
  373. return false;
  374. }
  375. }
  376. }