Fix FLEXIAPI-228 Add reset password email flow

This commit is contained in:
Timothée Jaussoin 2024-12-03 17:06:30 +01:00
parent 93c98ae73f
commit 3d715afc23
25 changed files with 464 additions and 133 deletions

View file

@ -1,7 +1,7 @@
variables:
ROCKY_8_IMAGE_VERSION: 20241113_143521_update_php_82
ROCKY_9_IMAGE_VERSION: 20241114_161138_remove_redis
DEBIAN_12_IMAGE_VERSION: 20241112_113948_update_package_and_dependencies
DEBIAN_12_IMAGE_VERSION: 20241204_162237_update_download_linphone_org
PHP_REDIS_REMI_VERSION: php-pecl-redis6-6.1.0-1
PHP_IGBINARY_REMI_VERSION: php-pecl-igbinary-3.2.16-2
PHP_MSGPACK_REMI_VERSION: php-pecl-msgpack-2.2.0-3

View file

@ -45,6 +45,7 @@ APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES=10
APP_RECOVERY_CODE_EXPIRATION_MINUTES=10
APP_PROVISIONING_TOKEN_EXPIRATION_MINUTES=0
APP_API_KEY_EXPIRATION_MINUTES=60 # Number of minutes the unused API Keys are valid
APP_RESET_PASSWORD_EMAIL_TOKEN_EXPIRATION_MINUTES=1440 # 24h
# Account creation and authentication
ACCOUNT_EMAIL_UNIQUE=false # Emails are unique between all the accounts

View file

@ -256,6 +256,11 @@ class Account extends Authenticatable
return $this->hasMany(AuthToken::class);
}
public function resetPasswordEmailTokens()
{
return $this->hasMany(ResetPasswordEmailToken::class)->latest();
}
/**
* Attributes
*/

View file

@ -27,6 +27,11 @@ abstract class Consommable extends Model
$this->user_agent = $request->userAgent();
}
public function offed(): bool
{
return $this->consumed() || $this->expired();
}
public function consumed(): bool
{
return $this->{$this->consommableAttribute} == null;

View file

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Account;
use App\ResetPasswordEmailToken;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ResetPasswordEmailController extends Controller
{
public function change(string $token)
{
$token = ResetPasswordEmailToken::where('token', $token)->firstOrFail();
return view('account.password_reset', [
'token' => $token
]);
}
public function reset(Request $request)
{
$request->validate([
'token' => 'required|size:16',
'password' => 'required|min:8|confirmed',
'h-captcha-response' => captchaConfigured() ? 'required|HCaptcha' : ''
]);
$token = ResetPasswordEmailToken::where('token', $request->get('token'))->firstOrFail();
if ($token->offed()) abort(403);
$token->account->updatePassword($request->get('password'));
$token->consume();
return view('account.password_changed');
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Account;
use App\ResetPasswordEmailToken;
use App\Http\Controllers\Controller;
use App\Mail\ResetPassword;
use Illuminate\Support\Str;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
class ResetPasswordEmailController extends Controller
{
public function create(int $accountId)
{
$account = Account::findOrFail($accountId);
return view('admin.account.reset_password_email.create', [
'account' => $account
]);
}
public function send(int $accountId)
{
$account = Account::findOrFail($accountId);
$resetPasswordEmail = new ResetPasswordEmailToken;
$resetPasswordEmail->account_id = $account->id;
$resetPasswordEmail->token = Str::random(16);
$resetPasswordEmail->email = $account->email;
$resetPasswordEmail->save();
Mail::to($account)->send(new ResetPassword($resetPasswordEmail));
return redirect()->route('admin.account.activity.index', $account);
}
}

View file

@ -35,8 +35,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\Space::class
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class
];
/**
@ -59,8 +58,7 @@ class Kernel extends HttpKernel
'throttle:600,1', // move to 600 instead of 60
'bindings',
'validate_json',
'localization',
'space'
'localization'
],
];
@ -89,7 +87,6 @@ class Kernel extends HttpKernel
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'space' => \App\Http\Middleware\Space::class,
'space.expired' => \App\Http\Middleware\IsSpaceExpired::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,

View file

@ -4,16 +4,34 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;
class IsSpaceExpired
{
public function handle(Request $request, Closure $next): Response
{
if ($request->user() && !$request->user()->superAdmin && $request->get('resolvedSpace')?->isExpired()) {
abort(403, 'The related Space has expired');
if (empty(config('app.root_domain'))) {
return abort(503, 'APP_ROOT_DOMAIN is not configured');
}
return $next($request);
$space = \App\Space::where('host', $request->header('host'))->first();
if ($space) {
if (!str_ends_with($space->host, config('app.root_domain'))) {
return abort(503, 'The APP_ROOT_DOMAIN configured does not match with the current root domain');
}
Config::set('app.url', '://' . $space->host);
Config::set('app.sip_domain', $space->domain);
if ($request->user() && !$request->user()->superAdmin && $space?->isExpired()) {
abort(403, 'The related Space has expired');
}
return $next($request);
}
return abort(404, 'Host not configured');
}
}

View file

@ -1,35 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;
class Space
{
public function handle(Request $request, Closure $next): Response
{
if (empty(config('app.root_domain'))) {
return abort(503, 'APP_ROOT_DOMAIN is not configured');
}
$space = \App\Space::where('host', $request->header('host'))->first();
if ($space) {
if (!str_ends_with($space->host, config('app.root_domain'))) {
return abort(503, 'The APP_ROOT_DOMAIN configured does not match with the current root domain');
}
Config::set('app.url', '://' . $space->host);
Config::set('app.sip_domain', $space->domain);
$request->request->set('resolvedSpace', $space);
return $next($request);
}
return abort(404, 'Host not configured');
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2024 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Account;
use App\ResetPasswordEmailToken;
class ResetPassword extends Mailable
{
use Queueable, SerializesModels;
private $token;
public function __construct(ResetPasswordEmailToken $token)
{
$this->token = $token;
}
public function build()
{
return $this->view('mails.reset_password')
->text('mails.reset_password')
->with([
'token' => $this->token
]);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ResetPasswordEmailToken extends Consommable
{
use HasFactory;
protected ?string $configExpirationMinutesKey = 'reset_password_email_token_expiration_minutes';
public function account()
{
return $this->belongsTo(Account::class);
}
public function consume()
{
$this->used = true;
$this->save();
}
public function consumed(): bool
{
return $this->used == true;
}
}

144
flexiapi/composer.lock generated
View file

@ -1325,16 +1325,16 @@
},
{
"name": "giggsey/libphonenumber-for-php-lite",
"version": "8.13.50",
"version": "8.13.51",
"source": {
"type": "git",
"url": "https://github.com/giggsey/libphonenumber-for-php-lite.git",
"reference": "57bb2bfd8d4a9896ed961c584141247f2a35bc04"
"reference": "34e43f33e21a8cdeebc36e9de57157ae821ef56b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/57bb2bfd8d4a9896ed961c584141247f2a35bc04",
"reference": "57bb2bfd8d4a9896ed961c584141247f2a35bc04",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/34e43f33e21a8cdeebc36e9de57157ae821ef56b",
"reference": "34e43f33e21a8cdeebc36e9de57157ae821ef56b",
"shasum": ""
},
"require": {
@ -1404,7 +1404,7 @@
"issues": "https://github.com/giggsey/libphonenumber-for-php-lite/issues",
"source": "https://github.com/giggsey/libphonenumber-for-php-lite"
},
"time": "2024-11-18T09:58:30+00:00"
"time": "2024-12-02T09:22:48+00:00"
},
{
"name": "graham-campbell/result-type",
@ -4790,16 +4790,16 @@
},
{
"name": "psy/psysh",
"version": "v0.12.4",
"version": "v0.12.5",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "2fd717afa05341b4f8152547f142cd2f130f6818"
"reference": "36a03ff27986682c22985e56aabaf840dd173cb5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818",
"reference": "2fd717afa05341b4f8152547f142cd2f130f6818",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/36a03ff27986682c22985e56aabaf840dd173cb5",
"reference": "36a03ff27986682c22985e56aabaf840dd173cb5",
"shasum": ""
},
"require": {
@ -4826,12 +4826,12 @@
],
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "0.12.x-dev"
},
"bamarni-bin": {
"bin-links": false,
"forward-command": false
},
"branch-alias": {
"dev-main": "0.12.x-dev"
}
},
"autoload": {
@ -4863,9 +4863,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.4"
"source": "https://github.com/bobthecow/psysh/tree/v0.12.5"
},
"time": "2024-06-10T01:18:23+00:00"
"time": "2024-11-29T06:14:30+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -5599,16 +5599,16 @@
},
{
"name": "respect/validation",
"version": "2.3.8",
"version": "2.3.9",
"source": {
"type": "git",
"url": "https://github.com/Respect/Validation.git",
"reference": "25ce44c7ee9613d260c7c0e44e27daa2131f383a"
"reference": "c96758eb27339c97486f311f25fbc797df2f6736"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Respect/Validation/zipball/25ce44c7ee9613d260c7c0e44e27daa2131f383a",
"reference": "25ce44c7ee9613d260c7c0e44e27daa2131f383a",
"url": "https://api.github.com/repos/Respect/Validation/zipball/c96758eb27339c97486f311f25fbc797df2f6736",
"reference": "c96758eb27339c97486f311f25fbc797df2f6736",
"shasum": ""
},
"require": {
@ -5661,9 +5661,9 @@
],
"support": {
"issues": "https://github.com/Respect/Validation/issues",
"source": "https://github.com/Respect/Validation/tree/2.3.8"
"source": "https://github.com/Respect/Validation/tree/2.3.9"
},
"time": "2024-11-26T09:14:36+00:00"
"time": "2024-11-28T09:44:01+00:00"
},
{
"name": "sabre/uri",
@ -5922,12 +5922,12 @@
"type": "library",
"extra": {
"laravel": {
"providers": [
"Scyllaly\\HCaptcha\\HCaptchaServiceProvider"
],
"aliases": {
"HCaptcha": "Scyllaly\\HCaptcha\\Facades\\HCaptcha"
}
},
"providers": [
"Scyllaly\\HCaptcha\\HCaptchaServiceProvider"
]
}
},
"autoload": {
@ -7034,16 +7034,16 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
"shasum": ""
},
"require": {
@ -7081,7 +7081,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
},
"funding": [
{
@ -7097,7 +7097,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/error-handler",
@ -7256,16 +7256,16 @@
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
"reference": "8f93aec25d41b72493c6ddff14e916177c9efc50"
"reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50",
"reference": "8f93aec25d41b72493c6ddff14e916177c9efc50",
"url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f",
"reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f",
"shasum": ""
},
"require": {
@ -7312,7 +7312,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0"
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1"
},
"funding": [
{
@ -7328,7 +7328,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/finder",
@ -7396,16 +7396,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v6.4.15",
"version": "v6.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "9b3165eb2f04aeaa1a5a2cfef73e63fe3b22dff6"
"reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/9b3165eb2f04aeaa1a5a2cfef73e63fe3b22dff6",
"reference": "9b3165eb2f04aeaa1a5a2cfef73e63fe3b22dff6",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/431771b7a6f662f1575b3cfc8fd7617aa9864d57",
"reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57",
"shasum": ""
},
"require": {
@ -7453,7 +7453,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v6.4.15"
"source": "https://github.com/symfony/http-foundation/tree/v6.4.16"
},
"funding": [
{
@ -7469,20 +7469,20 @@
"type": "tidelift"
}
],
"time": "2024-11-08T16:09:24+00:00"
"time": "2024-11-13T18:58:10+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v6.4.15",
"version": "v6.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "b002a5b3947653c5aee3adac2a024ea615fd3ff5"
"reference": "8838b5b21d807923b893ccbfc2cbeda0f1bc00f0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/b002a5b3947653c5aee3adac2a024ea615fd3ff5",
"reference": "b002a5b3947653c5aee3adac2a024ea615fd3ff5",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/8838b5b21d807923b893ccbfc2cbeda0f1bc00f0",
"reference": "8838b5b21d807923b893ccbfc2cbeda0f1bc00f0",
"shasum": ""
},
"require": {
@ -7567,7 +7567,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v6.4.15"
"source": "https://github.com/symfony/http-kernel/tree/v6.4.16"
},
"funding": [
{
@ -7583,7 +7583,7 @@
"type": "tidelift"
}
],
"time": "2024-11-13T13:57:37+00:00"
"time": "2024-11-27T12:49:36+00:00"
},
{
"name": "symfony/mailer",
@ -8449,16 +8449,16 @@
},
{
"name": "symfony/routing",
"version": "v6.4.13",
"version": "v6.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "640a74250d13f9c30d5ca045b6aaaabcc8215278"
"reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/640a74250d13f9c30d5ca045b6aaaabcc8215278",
"reference": "640a74250d13f9c30d5ca045b6aaaabcc8215278",
"url": "https://api.github.com/repos/symfony/routing/zipball/91e02e606b4b705c2f4fb42f7e7708b7923a3220",
"reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220",
"shasum": ""
},
"require": {
@ -8512,7 +8512,7 @@
"url"
],
"support": {
"source": "https://github.com/symfony/routing/tree/v6.4.13"
"source": "https://github.com/symfony/routing/tree/v6.4.16"
},
"funding": [
{
@ -8528,20 +8528,20 @@
"type": "tidelift"
}
],
"time": "2024-10-01T08:30:56+00:00"
"time": "2024-11-13T15:31:34+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f"
"reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
"reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
"shasum": ""
},
"require": {
@ -8595,7 +8595,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v3.5.0"
"source": "https://github.com/symfony/service-contracts/tree/v3.5.1"
},
"funding": [
{
@ -8611,7 +8611,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/string",
@ -8796,16 +8796,16 @@
},
{
"name": "symfony/translation-contracts",
"version": "v3.5.0",
"version": "v3.5.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a"
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
"reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
"reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
"shasum": ""
},
"require": {
@ -8854,7 +8854,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.0"
"source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
},
"funding": [
{
@ -8870,7 +8870,7 @@
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
"time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/uid",
@ -10101,16 +10101,16 @@
},
{
"name": "symfony/dependency-injection",
"version": "v6.4.15",
"version": "v6.4.16",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "70ab1f65a4516ef741e519ea938e6aa465e6aa36"
"reference": "7a379d8871f6a36f01559c14e11141cc02eb8dc8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/70ab1f65a4516ef741e519ea938e6aa465e6aa36",
"reference": "70ab1f65a4516ef741e519ea938e6aa465e6aa36",
"url": "https://api.github.com/repos/symfony/dependency-injection/zipball/7a379d8871f6a36f01559c14e11141cc02eb8dc8",
"reference": "7a379d8871f6a36f01559c14e11141cc02eb8dc8",
"shasum": ""
},
"require": {
@ -10162,7 +10162,7 @@
"description": "Allows you to standardize and centralize the way objects are constructed in your application",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dependency-injection/tree/v6.4.15"
"source": "https://github.com/symfony/dependency-injection/tree/v6.4.16"
},
"funding": [
{
@ -10178,7 +10178,7 @@
"type": "tidelift"
}
],
"time": "2024-11-09T06:56:25+00:00"
"time": "2024-11-25T14:52:46+00:00"
},
{
"name": "symfony/filesystem",

View file

@ -52,6 +52,7 @@ return [
'phone_change_code_expiration_minutes' => env('APP_PHONE_CHANGE_CODE_EXPIRATION_MINUTES', 10),
'recovery_code_expiration_minutes' => env('APP_RECOVERY_CODE_EXPIRATION_MINUTES', 10),
'provisioning_token_expiration_minutes' => env('APP_PROVISIONING_TOKEN_EXPIRATION_MINUTES', 0),
'reset_password_email_token_expiration_minutes' => env('APP_RESET_PASSWORD_EMAIL_TOKEN_EXPIRATION_MINUTES', 1440),
/**
* Amount of minutes before re-authorizing the generation of a new account creation token
*/

View file

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('reset_password_email_tokens', function (Blueprint $table) {
$table->id();
$table->string('token', 16);
$table->boolean('used')->default(false);
$table->string('email');
$table->integer('account_id')->unsigned();
$table->string('ip')->nullable();
$table->string('user_agent')->nullable();
$table->timestamps();
$table->foreign('account_id')->references('id')
->on('accounts')->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('reset_password_email_tokens');
}
};

View file

@ -619,6 +619,7 @@ table tr td a {
table tr td,
table tr th {
width: 20rem;
padding: 1rem;
font-size: 1.5rem;
}

View file

@ -0,0 +1,18 @@
@extends('layouts.main', ['welcome' => true])
@section('content')
<section>
<header>
<h1><i class="ph">lock</i> Reset password</h1>
</header>
<p>Your password was updated properly.</p>
<p>
<a class="btn" href="{{ route('account.login')}}">Authenticate</a>
</p>
</section>
<section class="on_desktop">
<img src="{{ asset('img/lock.svg') }}">
</section>
@endsection

View file

@ -0,0 +1,39 @@
@extends('layouts.main', ['welcome' => true])
@section('content')
<section>
<header>
<h1><i class="ph">lock</i> Reset password</h1>
</header>
@if ($token->offed())
<p>This link is not available anymore.</p>
@else
<form id="password_update" method="POST" action="{{ route('account.reset_password_email.reset') }}" accept-charset="UTF-8">
@csrf
<input type="hidden" name="token" value="{{ $token->token }}">
<div class="large">
<input type="password" name="password" required>
<label for="password">Password</label>
@include('parts.errors', ['name' => 'password'])
</div>
<div class="large">
<input type="password" name="password_confirmation" required>
<label for="password_confirmation">Password confirmation</label>
@include('parts.errors', ['name' => 'password_confirmation'])
</div>
@include('parts.captcha')
<div class="large">
<input class="btn" type="submit" value="Reset">
</div>
</form>
@endif
</section>
<section class="on_desktop">
<img src="{{ asset('img/lock.svg') }}">
</section>
@endsection

View file

@ -60,7 +60,7 @@
</tr>
</thead>
<tbody>
<tr @if ($account->accountCreationToken->consumed()) class="disabled crossed" @endif>
<tr @if ($account->accountCreationToken->offed()) class="disabled crossed" @endif>
<td>****</td>
<td>
{{ $account->accountCreationToken->created_at }}
@ -88,8 +88,8 @@
</tr>
</thead>
<tbody>
@foreach ($account->recoveryCodes as $recoveryCode)
<tr @if ($recoveryCode->consumed()) class="disabled crossed" @endif>
@foreach ($account->recoveryCodes as $key => $recoveryCode)
<tr @if ($recoveryCode->offed() || $key > 0) class="disabled crossed" @endif>
<td>****</td>
<td>
{{ $recoveryCode->created_at }}
@ -119,8 +119,8 @@
</tr>
</thead>
<tbody>
@foreach ($account->phoneChangeCodes as $phoneChangeCode)
<tr @if ($phoneChangeCode->consumed()) class="disabled crossed" @endif>
@foreach ($account->phoneChangeCodes as $key => $phoneChangeCode)
<tr @if ($phoneChangeCode->offed() || $key > 0) class="disabled crossed" @endif>
<td>{{ $phoneChangeCode->phone }}</td>
<td>{{ $phoneChangeCode->code ?? '-' }}</td>
<td>
@ -151,8 +151,8 @@
</tr>
</thead>
<tbody>
@foreach ($account->emailChangeCodes as $emailChangeCode)
<tr @if ($emailChangeCode->consumed()) class="disabled crossed" @endif>
@foreach ($account->emailChangeCodes as $key => $emailChangeCode)
<tr @if ($emailChangeCode->offed() || $key > 0) class="disabled crossed" @endif>
<td>{{ $emailChangeCode->email }}</td>
<td>{{ $emailChangeCode->code ?? '-' }}</td>
<td>
@ -182,8 +182,8 @@
</tr>
</thead>
<tbody>
@foreach ($account->provisioningTokens as $provisioningToken)
<tr @if ($provisioningToken->consumed()) class="disabled crossed" @endif>
@foreach ($account->provisioningTokens as $key => $provisioningToken)
<tr @if ($provisioningToken->offed() || $key > 0) class="disabled crossed" @endif>
<td>{{ $provisioningToken->token }}</td>
<td>
{{ $provisioningToken->created_at }}
@ -200,4 +200,34 @@
</table>
@endif
@if ($account->resetPasswordEmailTokens->isNotEmpty())
<h3>Set Password Emails</h3>
<table>
<thead>
<tr>
<th>Token</th>
<th>Created</th>
<th>Used</th>
<th>Email</th>
</tr>
</thead>
<tbody>
@foreach ($account->resetPasswordEmailTokens as $key => $resetPasswordEmailToken)
<tr @if ($resetPasswordEmailToken->offed() || $key > 0) class="disabled crossed" @endif>
<td>{{ $resetPasswordEmailToken->token }}</td>
<td>
{{ $resetPasswordEmailToken->created_at }}
</td>
<td>
{{ $resetPasswordEmailToken->consumed() ? $resetPasswordEmailToken->updated_at : '-' }}
</td>
<td>
{{ $resetPasswordEmailToken->email }}
</td>
</tr>
@endforeach
</tbody>
</table>
@endif
@endsection

View file

@ -83,6 +83,14 @@
value="@if($account->id){{ $account->email }}@else{{ old('email') }}@endif">
<label for="email">Email</label>
@include('parts.errors', ['name' => 'email'])
@if (!empty($account->email))
<p class="oppose">
<a href="{{ route('admin.account.reset_password_email.create', $account) }}">
Send an email to the user to reset the password
</a>
</p>
@endif
</div>
<div>

View file

@ -0,0 +1,25 @@
@extends('layouts.main')
@section('breadcrumb')
@include('admin.account.parts.breadcrumb_accounts_index')
@include('admin.account.parts.breadcrumb_accounts_edit', ['account' => $account])
<li class="breadcrumb-item active" aria-current="page">Reset Password emails</li>
@endsection
@section('content')
<header>
<h1><i class="ph">envelope</i> Send a Reset Password email</h1>
</header>
<p>An email will be sent to <b>{{ $account->email }}</b> with a unique link allowing the user to reset its password.</p>
<p>This link will be available for {{ config('app.reset_password_email_token_expiration_minutes')/60 }} hours.</p>
<p>
<a class="btn" href="{{ route('admin.account.reset_password_email.send', $account) }}">
<i class="ph">paper-plane-right</i> Send
</a>
</p>
@endsection

View file

@ -0,0 +1,21 @@
<html>
<head>
<title>Reset your password on {{ config('app.name') }}</title>
</head>
<body>
<p>Hello,</p>
<p>
You are invited to reset your {{ $token->account->identifier }} account password on {{ config('app.name') }} via your email account.
</p>
<p>The following link will be valid for {{ config('app.reset_password_email_token_expiration_minutes')/60 }} hours.</p>
<p>
<a href="{{ route('account.reset_password_email.change', $token->token) }}">
{{ route('account.reset_password_email.change', $token->token) }}
</a>
</p>
<p>
Regards,<br />
{{ config('mail.signature') }}
</p>
</body>
</html>

View file

@ -0,0 +1,10 @@
Hello,
You are invited to reset your {{ $token->account->identifier }} account password on {{ config('app.name') }} via your email account.
The following link will be valid for {{ config('app.reset_password_email_token_expiration_minutes')/60 }} hours.
{{ route('account.reset_password_email.change', $token->token) }}
Regards,
{{ config('mail.signature') }}

View file

@ -39,6 +39,7 @@ use App\Http\Controllers\Admin\AccountStatisticsController;
use App\Http\Controllers\Admin\ContactsListController;
use App\Http\Controllers\Admin\ContactsListContactController;
use App\Http\Controllers\Admin\PhoneCountryController;
use App\Http\Controllers\Admin\ResetPasswordEmailController;
use App\Http\Controllers\Admin\SpaceController;
use App\Http\Controllers\Admin\StatisticsController;
use Illuminate\Support\Facades\Route;
@ -47,10 +48,14 @@ Route::redirect('/', 'login')->name('account.home');
Route::get('documentation', 'Account\AccountController@documentation')->name('account.documentation');
Route::get('about', 'AboutController@about')->name('about');
Route::group(['middleware' => 'web_panel_enabled'], function () {
Route::middleware(['web_panel_enabled', 'space.expired'])->group(function () {
Route::get('login', 'Account\AuthenticateController@login')->name('account.login');
Route::post('authenticate', 'Account\AuthenticateController@authenticate')->name('account.authenticate');
Route::get('authenticate/qrcode/{token?}', 'Account\AuthenticateController@loginAuthToken')->name('account.authenticate.auth_token');
Route::get('logout', 'Account\AuthenticateController@logout')->name('account.logout');
Route::get('reset_password/{token}', 'Account\ResetPasswordEmailController@change')->name('account.reset_password_email.change');
Route::post('reset_password', 'Account\ResetPasswordEmailController@reset')->name('account.reset_password_email.reset');
// Deprecated
Route::get('authenticate/email/{code}', 'Account\AuthenticateController@validateEmail')->name('account.authenticate.email_confirm');
@ -100,10 +105,6 @@ Route::middleware(['web_panel_enabled', 'space.expired'])->group(function () {
Route::post('confirm', 'confirm')->name('account.recovery.confirm');
});
Route::middleware(['auth'])->group(function () {
Route::get('logout', 'Account\AuthenticateController@logout')->name('account.logout');
});
Route::name('account.')->middleware(['auth', 'auth.check_blocked'])->group(function () {
Route::get('blocked', 'Account\AccountController@blocked')->name('blocked');
@ -201,6 +202,11 @@ Route::middleware(['web_panel_enabled', 'space.expired'])->group(function () {
Route::post('{account_id}/contacts_lists', 'contactsListAdd')->name('contacts_lists.attach');
});
Route::name('reset_password_email.')->controller(ResetPasswordEmailController::class)->prefix('{account_id}/reset_password_emails')->group(function () {
Route::get('create', 'create')->name('create');
Route::get('send', 'send')->name('send');
});
Route::name('import.')->prefix('import')->controller(AccountImportController::class)->group(function () {
Route::get('/', 'create')->name('create');
Route::post('/', 'store')->name('store');

View file

@ -20,7 +20,7 @@
namespace Tests;
use App\PhoneCountry;
use App\Http\Middleware\Space;
use App\Http\Middleware\IsSpaceExpired;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -37,7 +37,7 @@ abstract class TestCase extends BaseTestCase
{
parent::setUp();
$this->withoutMiddleware([Space::class]);
$this->withoutMiddleware([IsSpaceExpired::class]);
PhoneCountry::truncate();
PhoneCountry::factory()->france()->activated()->create();

View file

@ -19,7 +19,6 @@
namespace Tests;
use App\Http\Middleware\Space;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;