PasswordResetController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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. // Verify reset token
  76. $resetRecord = $this->verifyResetToken($request->email, $request->token);
  77. if (!$resetRecord) {
  78. return back()->with('error', 'Token non valido o scaduto.');
  79. }
  80. // Get user from master database
  81. $user = $this->findUserInMasterDatabase($request->email);
  82. if (!$user) {
  83. return back()->with('error', 'Utente non trovato.');
  84. }
  85. // Update password in both databases
  86. $this->updatePasswordInBothDatabases($request->email, $request->password, $user);
  87. // Delete reset token
  88. $this->deleteResetToken($request->email);
  89. // Send password change notification
  90. $this->sendPasswordChangeNotification($request->email, $user->name);
  91. Log::info('Password reset completed with notification', [
  92. 'email' => $request->email
  93. ]);
  94. return redirect('/')->with('success', 'Password aggiornata con successo. Ti abbiamo inviato una email di conferma.');
  95. } catch (\Exception $e) {
  96. Log::error('Password reset failed', [
  97. 'email' => $request->email,
  98. 'error' => $e->getMessage()
  99. ]);
  100. return back()->with('error', 'Errore durante il reset della password. Riprova più tardi.');
  101. }
  102. }
  103. /**
  104. * Find user in master database
  105. */
  106. private function findUserInMasterDatabase($email)
  107. {
  108. try {
  109. $masterConfig = [
  110. 'driver' => 'mysql',
  111. 'host' => env('DB_HOST', '127.0.0.1'),
  112. 'port' => env('DB_PORT', '3306'),
  113. 'database' => env('DB_DATABASE'),
  114. 'username' => env('DB_USERNAME'),
  115. 'password' => env('DB_PASSWORD'),
  116. 'charset' => 'utf8mb4',
  117. 'collation' => 'utf8mb4_unicode_ci',
  118. 'prefix' => '',
  119. 'strict' => true,
  120. 'engine' => null,
  121. ];
  122. config(['database.connections.master_reset' => $masterConfig]);
  123. $user = DB::connection('master_reset')
  124. ->table('users')
  125. ->where('email', $email)
  126. ->first();
  127. DB::purge('master_reset');
  128. return $user;
  129. } catch (\Exception $e) {
  130. Log::error('Failed to find user in master database', [
  131. 'email' => $email,
  132. 'error' => $e->getMessage()
  133. ]);
  134. return null;
  135. }
  136. }
  137. /**
  138. * Store reset token in master database
  139. */
  140. private function storeResetToken($email, $token)
  141. {
  142. try {
  143. $masterConfig = [
  144. 'driver' => 'mysql',
  145. 'host' => env('DB_HOST', '127.0.0.1'),
  146. 'port' => env('DB_PORT', '3306'),
  147. 'database' => env('DB_DATABASE'),
  148. 'username' => env('DB_USERNAME'),
  149. 'password' => env('DB_PASSWORD'),
  150. 'charset' => 'utf8mb4',
  151. 'collation' => 'utf8mb4_unicode_ci',
  152. 'prefix' => '',
  153. 'strict' => true,
  154. 'engine' => null,
  155. ];
  156. config(['database.connections.master_reset_token' => $masterConfig]);
  157. DB::connection('master_reset_token')
  158. ->table('password_resets')
  159. ->where('email', $email)
  160. ->delete();
  161. DB::connection('master_reset_token')
  162. ->table('password_resets')
  163. ->insert([
  164. 'email' => $email,
  165. 'token' => Hash::make($token),
  166. 'created_at' => Carbon::now()
  167. ]);
  168. DB::purge('master_reset_token');
  169. } catch (\Exception $e) {
  170. Log::error('Failed to store reset token', [
  171. 'email' => $email,
  172. 'error' => $e->getMessage()
  173. ]);
  174. throw $e;
  175. }
  176. }
  177. /**
  178. * Verify reset token
  179. */
  180. private function verifyResetToken($email, $token)
  181. {
  182. try {
  183. $masterConfig = [
  184. 'driver' => 'mysql',
  185. 'host' => env('DB_HOST', '127.0.0.1'),
  186. 'port' => env('DB_PORT', '3306'),
  187. 'database' => env('DB_DATABASE'),
  188. 'username' => env('DB_USERNAME'),
  189. 'password' => env('DB_PASSWORD'),
  190. 'charset' => 'utf8mb4',
  191. 'collation' => 'utf8mb4_unicode_ci',
  192. 'prefix' => '',
  193. 'strict' => true,
  194. 'engine' => null,
  195. ];
  196. config(['database.connections.master_verify' => $masterConfig]);
  197. $resetRecord = DB::connection('master_verify')
  198. ->table('password_resets')
  199. ->where('email', $email)
  200. ->first();
  201. DB::purge('master_verify');
  202. if (!$resetRecord) {
  203. return null;
  204. }
  205. $isValidToken = Hash::check($token, $resetRecord->token);
  206. $isNotExpired = Carbon::parse($resetRecord->created_at)->addHours(24)->isFuture();
  207. if ($isValidToken && $isNotExpired) {
  208. return $resetRecord;
  209. }
  210. return null;
  211. } catch (\Exception $e) {
  212. Log::error('Failed to verify reset token', [
  213. 'email' => $email,
  214. 'error' => $e->getMessage()
  215. ]);
  216. return null;
  217. }
  218. }
  219. /**
  220. * Update password in both master and tenant databases
  221. */
  222. private function updatePasswordInBothDatabases($email, $newPassword, $masterUser)
  223. {
  224. $hashedPassword = Hash::make($newPassword);
  225. try {
  226. $masterConfig = [
  227. 'driver' => 'mysql',
  228. 'host' => env('DB_HOST', '127.0.0.1'),
  229. 'port' => env('DB_PORT', '3306'),
  230. 'database' => env('DB_DATABASE'),
  231. 'username' => env('DB_USERNAME'),
  232. 'password' => env('DB_PASSWORD'),
  233. 'charset' => 'utf8mb4',
  234. 'collation' => 'utf8mb4_unicode_ci',
  235. 'prefix' => '',
  236. 'strict' => true,
  237. 'engine' => null,
  238. ];
  239. config(['database.connections.master_update' => $masterConfig]);
  240. DB::connection('master_update')
  241. ->table('users')
  242. ->where('email', $email)
  243. ->update(['password' => $hashedPassword]);
  244. DB::purge('master_update');
  245. Log::info('Password updated in master database', ['email' => $email]);
  246. if ($masterUser->tenant_database) {
  247. $tenantConfig = [
  248. 'driver' => 'mysql',
  249. 'host' => env('DB_HOST', '127.0.0.1'),
  250. 'port' => env('DB_PORT', '3306'),
  251. 'database' => $masterUser->tenant_database,
  252. 'username' => $masterUser->tenant_username,
  253. 'password' => $masterUser->tenant_password,
  254. 'charset' => 'utf8mb4',
  255. 'collation' => 'utf8mb4_unicode_ci',
  256. 'prefix' => '',
  257. 'strict' => true,
  258. 'engine' => null,
  259. ];
  260. config(['database.connections.tenant_update' => $tenantConfig]);
  261. DB::connection('tenant_update')
  262. ->table('users')
  263. ->where('email', $email)
  264. ->update(['password' => $hashedPassword]);
  265. DB::purge('tenant_update');
  266. Log::info('Password updated in tenant database', [
  267. 'email' => $email,
  268. 'tenant_database' => $masterUser->tenant_database
  269. ]);
  270. }
  271. } catch (\Exception $e) {
  272. Log::error('Failed to update password in databases', [
  273. 'email' => $email,
  274. 'error' => $e->getMessage()
  275. ]);
  276. throw $e;
  277. }
  278. }
  279. /**
  280. * Delete reset token
  281. */
  282. private function deleteResetToken($email)
  283. {
  284. try {
  285. $masterConfig = [
  286. 'driver' => 'mysql',
  287. 'host' => env('DB_HOST', '127.0.0.1'),
  288. 'port' => env('DB_PORT', '3306'),
  289. 'database' => env('DB_DATABASE'),
  290. 'username' => env('DB_USERNAME'),
  291. 'password' => env('DB_PASSWORD'),
  292. 'charset' => 'utf8mb4',
  293. 'collation' => 'utf8mb4_unicode_ci',
  294. 'prefix' => '',
  295. 'strict' => true,
  296. 'engine' => null,
  297. ];
  298. config(['database.connections.master_delete_token' => $masterConfig]);
  299. DB::connection('master_delete_token')
  300. ->table('password_resets')
  301. ->where('email', $email)
  302. ->delete();
  303. DB::purge('master_delete_token');
  304. } catch (\Exception $e) {
  305. Log::error('Failed to delete reset token', [
  306. 'email' => $email,
  307. 'error' => $e->getMessage()
  308. ]);
  309. }
  310. }
  311. private function sendPasswordChangeNotification($email, $name)
  312. {
  313. try {
  314. $emailData = [
  315. 'name' => $name,
  316. 'email' => $email,
  317. 'change_time' => now()->format('d/m/Y H:i'),
  318. 'ip_address' => request()->ip()
  319. ];
  320. Mail::send('emails.password-changed', $emailData, function ($message) use ($email, $name) {
  321. $message->to($email, $name)
  322. ->subject('La tua password è stata modificata')
  323. ->from(config('mail.from.address'), config('mail.from.name'));
  324. });
  325. Log::info('Password change notification sent', [
  326. 'email' => $email,
  327. 'name' => $name
  328. ]);
  329. return true;
  330. } catch (\Exception $e) {
  331. Log::error('Failed to send password change notification', [
  332. 'email' => $email,
  333. 'error' => $e->getMessage()
  334. ]);
  335. return false;
  336. }
  337. }
  338. /**
  339. * Send password reset email
  340. */
  341. private function sendPasswordResetEmail($email, $token, $user)
  342. {
  343. try {
  344. $resetUrl = url('/password-reset/' . $token . '?email=' . urlencode($email));
  345. $companyName = 'Leezard';
  346. $emailData = [
  347. 'name' => $user->name,
  348. 'email' => $email,
  349. 'reset_url' => $resetUrl,
  350. 'company' => $companyName,
  351. 'expires_at' => Carbon::now()->addHours(24)->format('d/m/Y H:i')
  352. ];
  353. Mail::send('emails.password-reset', $emailData, function ($message) use ($email, $companyName, $user) {
  354. $message->to($email, $user->name)
  355. ->subject('Reset Password - Leezard')
  356. ->from(config('mail.from.address'), config('mail.from.name'));
  357. });
  358. Log::info('Password reset email sent', ['email' => $email]);
  359. } catch (\Exception $e) {
  360. Log::error('Failed to send password reset email', [
  361. 'email' => $email,
  362. 'error' => $e->getMessage()
  363. ]);
  364. throw $e;
  365. }
  366. }
  367. private function logSecurityEvent($event, $email, $additionalData = [])
  368. {
  369. Log::info('Password Reset Security Event', array_merge([
  370. 'event' => $event,
  371. 'email' => $email,
  372. 'ip' => request()->ip(),
  373. 'user_agent' => request()->userAgent(),
  374. 'timestamp' => now()
  375. ], $additionalData));
  376. }
  377. private function hasRecentResetRequest($email)
  378. {
  379. try {
  380. $masterConfig = [
  381. 'driver' => 'mysql',
  382. 'host' => env('DB_HOST', '127.0.0.1'),
  383. 'port' => env('DB_PORT', '3306'),
  384. 'database' => env('DB_DATABASE'),
  385. 'username' => env('DB_USERNAME'),
  386. 'password' => env('DB_PASSWORD'),
  387. 'charset' => 'utf8mb4',
  388. 'collation' => 'utf8mb4_unicode_ci',
  389. 'prefix' => '',
  390. 'strict' => true,
  391. 'engine' => null,
  392. ];
  393. config(['database.connections.master_check_recent' => $masterConfig]);
  394. $recentRequest = DB::connection('master_check_recent')
  395. ->table('password_resets')
  396. ->where('email', $email)
  397. ->where('created_at', '>', Carbon::now()->subMinutes(5)) // 5 minutes cooldown
  398. ->first();
  399. DB::purge('master_check_recent');
  400. return $recentRequest !== null;
  401. } catch (\Exception $e) {
  402. Log::error('Failed to check recent reset requests', [
  403. 'email' => $email,
  404. 'error' => $e->getMessage()
  405. ]);
  406. return false;
  407. }
  408. }
  409. }