feat: ✨ add users password and configure authentication based on them

This commit is contained in:
Alexis Gousseau 2026-04-28 16:38:04 +02:00
parent 4d5afbf9fd
commit 290754b785
No known key found for this signature in database
GPG key ID: 7F3C786EE12DBB72
44 changed files with 1507 additions and 641 deletions

View file

@ -0,0 +1,153 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Uwil\LaravelBackend\Support\UserRoleRegistry;
/**
* Artisan command to create a support admin user.
*/
class MakeUser extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
#[\Override]
protected $signature = 'support:make:user';
/**
* The console command description.
*
* @var string
*/
#[\Override]
protected $description = 'Create a new Support user.';
/**
* Execute the console command.
*
* @return void
*/
public function handle(): void
{
$this->components->info("Creating a new <comment>Support user</comment>...");
$firstName = $this->askValid(
'first_name',
'First name',
'Admin',
['required', 'min:3', 'max:255'],
);
$lastName = $this->askValid(
'last_name',
'Last name',
'Saïtama',
['required', 'min:3', 'max:255'],
);
$email = $this->askValid(
'email',
'Email',
'equipe.web@ldc.fr',
['required', 'unique:users,email', 'email:rfc,strict,dns,spoof,filter'],
);
$role = $this->askRole();
$password = $this->askPassword();
$user = new User([
'first_name' => $firstName,
'last_name' => $lastName,
'email' => $email,
'role' => $role->value,
'published' => true,
'published_at' => Date::now(),
]);
$user->saveOrFail();
$user->userPassword()->create([
'password' => bcrypt($password),
]);
$this->components->success("User <comment>{$firstName} {$lastName}</comment> created successfully.");
}
/**
* Ask for a value and validate it, re-prompting on failure.
*
* @param string $field
* @param string $label
* @param string $default
* @param array<int, string> $rules
* @return string
*/
private function askValid(string $field, string $label, string $default, array $rules): string
{
try {
$value = $this->ask(sprintf("Type the wanted user %s", $label), $default);
$validator = Validator::make([$field => $value], [$field => $rules]);
$validator->validate();
return $value;
} catch (ValidationException $e) {
$this->components->error(
collect($e->errors())->flatten()->first()
);
return $this->askValid($field, $label, $default, $rules);
}
}
/**
* Ask for a role and return the corresponding RoleEnum.
*
* @return object
*/
private function askRole(): object
{
$options = collect(UserRoleRegistry::all())->mapWithKeys(fn(object $role) => [
Str::ucFirst($role->label) => $role
]);
$choice = $this->choice('Role', $options->keys()->all(), Str::ucFirst(UserRoleRegistry::get('client')->label));
$choiceKey = is_string($choice) ? $choice : (string) $choice[0];
return $options->get($choiceKey) ?? UserRoleRegistry::get('client');
}
/**
* Ask for a password and confirmation password.
*
* @return string
*/
private function askPassword(): string
{
$password = $this->secret('Type the wanted user Password');
$passwordConfirmation = $this->secret('Confirm the password');
try {
Validator::make(
[
'password' => $password,
'password_confirmation' => $passwordConfirmation,
],
[
'password' => [
'required',
'confirmed',
'max:255',
Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised(),
],
]
)->validate();
return $password;
} catch (ValidationException $e) {
$this->components->error(collect($e->errors())->flatten()->first());
return $this->askPassword();
}
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Requests\Auth\ForgotPasswordRequest;
use App\Http\Requests\Auth\LoginRequest;
use App\Http\Requests\Auth\ResetPasswordRequest;
use App\Mails\Auth\ResetPassword;
use App\Models\User;
use App\Models\UserPassword;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Inertia\Inertia;
/**
* Manages backend authentication sessions (login/logout).
*/
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*
* @return \Inertia\Response
*/
public function login(): \Inertia\Response
{
return Inertia::render('back/Pages/Auth/Login');
}
/**
* Handle an incoming authentication request.
*
* @param \App\Http\Requests\Auth\LoginRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function postLogin(LoginRequest $request): \Illuminate\Http\RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route(config('uwil-backend.routes.name') . 'dashboard.index', absolute: false));
}
/**
* Destroy an authenticated session.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function logout(Request $request): \Illuminate\Http\RedirectResponse
{
Auth::guard('backend')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route(config('uwil-backend.routes.name') . 'login');
}
/**
* Display the forgotten password view.
*
* @return \Inertia\Response
*/
public function forgotPassword(): \Inertia\Response
{
return Inertia::render('back/Pages/Auth/ForgotPassword');
}
/**
* Handle an incoming forgotten password request.
*
* @param \App\Http\Requests\Auth\ForgotPasswordRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function postForgotPassword(ForgotPasswordRequest $request): \Illuminate\Http\RedirectResponse
{
$validatedData = $request->validated();
$status = Password::broker('backend')->sendResetLink($validatedData);
return $status === Password::RESET_LINK_SENT
? to_route(config('uwil-backend.routes.name') . 'login')
: back();
}
/**
* Display the view.
*
* @param string $token
* @return \Inertia\Response
*/
public function resetPassword(string $token): \Inertia\Response
{
return Inertia::render('back/Pages/Auth/ResetPassword', [
'token' => $token,
'email' => request()->query('email', ''),
]);
}
/**
* Handle an incoming resetting password request.
*
* @param \App\Http\Requests\Auth\ResetPasswordRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function postResetPassword(ResetPasswordRequest $request): \Illuminate\Http\RedirectResponse
{
$status = Password::broker('backend')->reset(
$request->validated(),
function (User $user, string $password): void {
$user->setRememberToken(Str::random(60));
$user->save();
UserPassword::updateOrCreate(
['user_id' => $user->getKey()],
['password' => Hash::make($password)],
);
event(new PasswordReset($user));
Mail::send(new ResetPassword($user));
}
);
return $status === Password::PASSWORD_RESET
? to_route(config('uwil-backend.routes.name') . 'login')
: back();
}
}

View file

@ -21,10 +21,9 @@ class HandleInertiaRequests extends Middleware
...InertiaHelper::errorProps(parent::share($request)),
// More props here.
];
return array_replace_recursive(
$props,
InertiaSharedRegistry::resolve($request)
InertiaSharedRegistry::resolve($request),
);
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
/**
* Handles backend forgotten password form validation.
*/
class ForgotPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'email' => 'required|string|email:rfc,strict,dns,spoof,filter',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'email' => trans('validation.attributes.email'),
];
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Uwil\LaravelBackend\Requests\Auth\LoginRequest as BackendLoginRequest;
/**
* Handles backend login form validation, authentication and rate limiting.
*/
class LoginRequest extends BackendLoginRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
#[\Override]
public function rules(): array
{
return [
'email' => 'required|string|email:rfc,strict,dns,spoof,filter',
'password' => [
'required',
'max:255',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
'remember' => 'boolean',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
];
}
/**
* Attempt to authenticate the request's credentials.
*
* @return void
* @throws \Illuminate\Validation\ValidationException Wrong email.
*/
#[\Override]
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$user = \App\Models\User::query()
->where('email', $this->input('email'))
->first();
if (
!$user
|| !$user->userPassword
|| !Hash::check($this->input('password'), $user->userPassword->password)
) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
Auth::guard('backend')->login($user, $this->boolean('remember'));
RateLimiter::clear($this->throttleKey());
}
/**
* Ensure the login request is not rate limited.
*
* @return void
* @throws \Illuminate\Validation\ValidationException Wrong email.
*/
#[\Override]
public function ensureIsNotRateLimited(): void
{
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
return;
}
event(new Lockout($this));
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
]);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
/**
* Handles backend resetting password form validation.
*/
class ResetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
$passwordRules = Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised();
return [
'token' => 'required|string',
'email' => 'required|string|email:rfc,strict,dns,spoof,filter',
'password' => [
'required',
'required_with:password_confirmation',
'confirmed',
'max:255',
$passwordRules,
],
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'token' => trans('validation.custom.token'),
'email' => trans('validation.attributes.email'),
'password' => trans('validation.attributes.password'),
];
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Mails\Auth;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
class LinkResetPasswordMail extends Mailable
{
/**
* Create a new message instance.
*/
public function __construct(
public string $url,
public mixed $user,
) {}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): \Illuminate\Mail\Mailables\Envelope
{
return new Envelope(
to: $this->user->getEmailForPasswordReset(),
subject: sprintf(
'%s - %s',
trans('auth.email_title_reset_password'),
config('app.name')
),
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content(): \Illuminate\Mail\Mailables\Content
{
return new Content(
markdown: 'back.mails.link-reset-password',
with: [
'url' => $this->url,
'user' => $this->user,
],
);
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace App\Mails\Auth;
use App\Models\User;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
class ResetPassword extends Mailable
{
/**
* @var \App\Models\User $user
*/
private User $user;
/**
* Create a new message instance.
*
* @param \App\Models\User $user
* @return void
*/
public function __construct(User $user)
{
$this->user = clone $user;
}
/**
* Get the message envelope.
*
* @return \Illuminate\Mail\Mailables\Envelope
*/
public function envelope(): \Illuminate\Mail\Mailables\Envelope
{
return new Envelope(
to: $this->user->getEmailForPasswordReset(),
subject: sprintf(
'%s - %s',
trans('auth.email_title_reset_complete'),
config('app.name')
),
);
}
/**
* Get the message content definition.
*
* @return \Illuminate\Mail\Mailables\Content
*/
public function content(): \Illuminate\Mail\Mailables\Content
{
return new Content(
markdown: 'back.mails.reset-password',
with: [
'first_name' => $this->user->first_name,
'last_name' => $this->user->last_name,
],
);
}
}

61
app/Models/User.php Normal file
View file

@ -0,0 +1,61 @@
<?php
namespace App\Models;
use App\Mails\Auth\LinkResetPasswordMail;
use App\Models\UserPassword;
use Illuminate\Support\Facades\Mail;
use Uwil\LaravelBackend\Models\User as BackendUser;
/**
* Extends the package User model with password-based authentication support.
*
* @property-read \App\Models\UserPassword|null $userPassword Associated password.
* @see \Uwil\LaravelBackend\Models\User For all other properties and methods.
*/
class User extends BackendUser
{
/**
* Get the password for authentication.
* Retrieves the hashed password from the related user_passwords table.
*
* @return string
*/
#[\Override]
public function getAuthPassword(): string
{
return $this->userPassword()->value('password') ?? '';
}
/**
* Send the password reset notification to the user.
*
* @param string $token
* @return void
*/
// phpstan-ignore-next-line
// phpcs:ignore
#[\Override]
public function sendPasswordResetNotification($token): void
{
$url = sprintf(
"%s?email=%s",
url(route('back.reset-password', ['token' => $token], false)),
urlencode($this->getEmailForPasswordReset())
);
Mail::send(new LinkResetPasswordMail($url, $this));
}
// * RELATIONSHIPS.
/**
* Get the user's associated password.
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne<\App\Models\UserPassword>
*/
public function userPassword(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(UserPassword::class);
}
}

View file

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property \App\Models\User $user_id User associated.
* @property string $password User password.
* @property-read \Illuminate\Support\Carbon $created_at Date on which the password was created.
* @property-read \Illuminate\Support\Carbon $updated_at Date on which the password was updated.
*/
class UserPassword extends Model
{
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
#[\Override]
protected $fillable = [
'user_id',
'password',
];
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Uwil\LaravelBackend\Support\UserRoleRegistry;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot(): void
{
// Set new role for users.
UserRoleRegistry::add(3, 'project-manager', trans('enums.user-role.project-manager'));
UserRoleRegistry::add(4, 'developper', trans('enums.user-role.developper'));
UserRoleRegistry::add(5, 'client', trans('enums.user-role.client'));
}
}

View file

@ -13,6 +13,7 @@ use Uwil\LaravelCommon\Helpers\InertiaHelper;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
health: '/up',
web: __DIR__ . '/../routes/auth.php',
then: function () {
Route::middleware(['web', 'throttle:5,2'])
->post('/csp-report', function () {
@ -55,13 +56,8 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->preventRequestForgery(except: [
'/csp-report',
]);
$middleware->group('saml', [
\Uwil\LaravelBackend\Middlewares\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
]);
$middleware->alias([
'auth' => \Uwil\LaravelBackend\Middlewares\Authenticate::class,
'auth' => \Uwil\LaravelBackend\Middlewares\Authenticate::class,
'guest' => \Uwil\LaravelBackend\Middlewares\RedirectIfAuthenticated::class,
]);
})
@ -89,4 +85,8 @@ return Application::configure(basePath: dirname(__DIR__))
->withInput($safeInput)
->withErrors($e->errors());
});
})->create();
})
->withCommands([
\App\Console\Commands\MakeUser::class,
])
->create();

View file

@ -4,4 +4,5 @@ return [
Uwil\LaravelCommon\Providers\AppServiceProvider::class,
Uwil\LaravelCommon\Providers\TranslationServiceProvider::class,
Uwil\K8S\Laravel\K8SServiceProvider::class,
App\Providers\AppServiceProvider::class,
];

View file

@ -14,7 +14,6 @@
"laravel/tinker": "^3.0",
"monolog/monolog": "^3.0",
"psr/log": "^3.0",
"scaler-tech/laravel-saml2": "^2.7",
"tightenco/ziggy": "^2.5",
"uwil/admin-secure-laravel": "^5.1",
"uwil/laravel-backend": "^5.0",

346
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ced98c57935b85c05a2f58f91f5347de",
"content-hash": "c7008e1b25e55e3dd4f28ec944534e12",
"packages": [
{
"name": "brick/math",
@ -1468,16 +1468,16 @@
},
{
"name": "laravel/framework",
"version": "v13.6.0",
"version": "v13.7.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1"
"reference": "f13b85b2cce7ef5e8f3bcdf2b6c6364bbdedae0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/416a93ea9c53161e0d4b8a44045f447b65a7d2f1",
"reference": "416a93ea9c53161e0d4b8a44045f447b65a7d2f1",
"url": "https://api.github.com/repos/laravel/framework/zipball/f13b85b2cce7ef5e8f3bcdf2b6c6364bbdedae0b",
"reference": "f13b85b2cce7ef5e8f3bcdf2b6c6364bbdedae0b",
"shasum": ""
},
"require": {
@ -1520,6 +1520,7 @@
"symfony/mime": "^7.4.0 || ^8.0.0",
"symfony/polyfill-php84": "^1.33",
"symfony/polyfill-php85": "^1.33",
"symfony/polyfill-php86": "^1.36",
"symfony/process": "^7.4.5 || ^8.0.5",
"symfony/routing": "^7.4.0 || ^8.0.0",
"symfony/uid": "^7.4.0 || ^8.0.0",
@ -1687,7 +1688,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-04-21T13:32:11+00:00"
"time": "2026-04-28T17:18:25+00:00"
},
{
"name": "laravel/prompts",
@ -1806,16 +1807,16 @@
},
{
"name": "laravel/serializable-closure",
"version": "v2.0.12",
"version": "v2.0.13",
"source": {
"type": "git",
"url": "https://github.com/laravel/serializable-closure.git",
"reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919"
"reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/a6abb4e54f6fcd3138120b9ad497f0bd146f9919",
"reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919",
"url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
"reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
"shasum": ""
},
"require": {
@ -1863,7 +1864,7 @@
"issues": "https://github.com/laravel/serializable-closure/issues",
"source": "https://github.com/laravel/serializable-closure"
},
"time": "2026-04-14T13:33:34+00:00"
"time": "2026-04-16T14:03:50+00:00"
},
{
"name": "laravel/telescope",
@ -3446,16 +3447,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "2.0.53",
"version": "2.0.54",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "2d1a664b940b9b8f367185307dc010d11a2790f3"
"reference": "a96a835067c39ee7a709329fe70869817da18081"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2d1a664b940b9b8f367185307dc010d11a2790f3",
"reference": "2d1a664b940b9b8f367185307dc010d11a2790f3",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/a96a835067c39ee7a709329fe70869817da18081",
"reference": "a96a835067c39ee7a709329fe70869817da18081",
"shasum": ""
},
"require": {
@ -3536,7 +3537,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/2.0.53"
"source": "https://github.com/phpseclib/phpseclib/tree/2.0.54"
},
"funding": [
{
@ -3552,7 +3553,7 @@
"type": "tidelift"
}
],
"time": "2026-04-10T01:30:02+00:00"
"time": "2026-04-27T06:59:24+00:00"
},
{
"name": "psr/clock",
@ -5663,20 +5664,20 @@
},
{
"name": "symfony/options-resolver",
"version": "v7.4.8",
"version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4"
"reference": "b48bce0a70b914f6953dafbd10474df232ed4de8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
"reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8",
"reference": "b48bce0a70b914f6953dafbd10474df232ed4de8",
"shasum": ""
},
"require": {
"php": ">=8.2",
"php": ">=8.4",
"symfony/deprecation-contracts": "^2.5|^3"
},
"type": "library",
@ -5710,7 +5711,7 @@
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.4.8"
"source": "https://github.com/symfony/options-resolver/tree/v8.0.8"
},
"funding": [
{
@ -5730,11 +5731,11 @@
"type": "tidelift"
}
],
"time": "2026-03-24T13:12:05+00:00"
"time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@ -5793,7 +5794,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
},
"funding": [
{
@ -5817,16 +5818,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df"
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e",
"reference": "4864388bfbd3001ce88e234fab652acd91fdc57e",
"shasum": ""
},
"require": {
@ -5875,7 +5876,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0"
},
"funding": [
{
@ -5895,11 +5896,11 @@
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
"time": "2026-04-26T13:13:48+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@ -5962,7 +5963,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0"
},
"funding": [
{
@ -5986,7 +5987,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@ -6047,7 +6048,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0"
},
"funding": [
{
@ -6071,7 +6072,7 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@ -6132,7 +6133,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
},
"funding": [
{
@ -6156,7 +6157,7 @@
},
{
"name": "symfony/polyfill-php80",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
@ -6216,7 +6217,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
},
"funding": [
{
@ -6240,7 +6241,7 @@
},
{
"name": "symfony/polyfill-php84",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php84.git",
@ -6296,7 +6297,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0"
},
"funding": [
{
@ -6320,16 +6321,16 @@
},
{
"name": "symfony/polyfill-php85",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php85.git",
"reference": "2c408a6bb0313e6001a83628dc5506100474254e"
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e",
"reference": "2c408a6bb0313e6001a83628dc5506100474254e",
"url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee",
"reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee",
"shasum": ""
},
"require": {
@ -6376,7 +6377,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0"
},
"funding": [
{
@ -6396,11 +6397,91 @@
"type": "tidelift"
}
],
"time": "2026-04-10T16:50:15+00:00"
"time": "2026-04-26T13:10:57+00:00"
},
{
"name": "symfony/polyfill-php86",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php86.git",
"reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/33d8fc5a705481e21fe3a81212b26f9b1f61749c",
"reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php86\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php86/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-26T13:13:48+00:00"
},
{
"name": "symfony/polyfill-uuid",
"version": "v1.36.0",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-uuid.git",
@ -6459,7 +6540,7 @@
"uuid"
],
"support": {
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0"
"source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0"
},
"funding": [
{
@ -7357,16 +7438,16 @@
},
{
"name": "uwil/admin-secure-laravel",
"version": "5.1.0",
"version": "5.1.1",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/ldc-web-admin-secure-laravel.git",
"reference": "0299d8397111b5a80ea3200a9fd387b94c34f208"
"reference": "c54dad85ee051e103f50dc8f19d457703bc5ff03"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/20/packages/composer/archives/uwil/admin-secure-laravel.zip?sha=0299d8397111b5a80ea3200a9fd387b94c34f208",
"reference": "0299d8397111b5a80ea3200a9fd387b94c34f208",
"url": "https://lab.s-web.fr/api/v4/projects/20/packages/composer/archives/uwil/admin-secure-laravel.zip?sha=c54dad85ee051e103f50dc8f19d457703bc5ff03",
"reference": "c54dad85ee051e103f50dc8f19d457703bc5ff03",
"shasum": ""
},
"require": {
@ -7374,7 +7455,7 @@
"illuminate/support": "~11|~12|~13",
"laravel/framework": "~11|~12|~13",
"php": "^8.3",
"symfony/options-resolver": "^7.0"
"symfony/options-resolver": "^7.0|^8.0"
},
"require-dev": {
"larastan/larastan": "^2.9",
@ -7482,17 +7563,11 @@
},
{
"name": "uwil/laravel-activity-logs",
"version": "6.0.0",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/laravel-activity-logs.git",
"reference": "765bf7aab42af11e79a64ce1b2245a09abae6fe1"
},
"version": "6.0.1",
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/229/packages/composer/archives/uwil/laravel-activity-logs.zip?sha=765bf7aab42af11e79a64ce1b2245a09abae6fe1",
"reference": "765bf7aab42af11e79a64ce1b2245a09abae6fe1",
"shasum": ""
"type": "path",
"url": "packages/laravel-activity-logs",
"reference": "988e15d19ea29ad3f9b077057330f226dc963d29"
},
"require": {
"illuminate/support": "~13",
@ -7502,7 +7577,10 @@
"uwil/laravel-common": "^6.0"
},
"require-dev": {
"driftingly/rector-laravel": "^2.3",
"larastan/larastan": "^3.9",
"laravel/pint": "^1.29",
"orchestra/testbench": "^11.0",
"slevomat/coding-standard": "^8.3",
"squizlabs/php_codesniffer": "^3.7"
},
@ -7519,6 +7597,20 @@
"Uwil\\LaravelActivityLogs\\": "src/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
],
"phpcs": [
"./vendor/bin/phpcs"
],
"phpstan": [
"./vendor/bin/phpstan"
],
"rector": [
"./vendor/bin/rector"
]
},
"license": [
"MIT"
],
@ -7531,21 +7623,19 @@
"support": {
"email": "digital@ldc.fr",
"issues": "https://lab.s-web.fr/ldc-web/projets/laravel-activity-logs/-/issues"
},
"transport-options": {
"symlink": true,
"relative": true
}
},
{
"name": "uwil/laravel-backend",
"version": "5.0.1",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/laravel-backend.git",
"reference": "9ec385e80de62ebcb0362911c56a9e7d917bcc80"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/228/packages/composer/archives/uwil/laravel-backend.zip?sha=9ec385e80de62ebcb0362911c56a9e7d917bcc80",
"reference": "9ec385e80de62ebcb0362911c56a9e7d917bcc80",
"shasum": ""
"type": "path",
"url": "packages/laravel-backend",
"reference": "cabc9caa919d4fdcb509558f631416fbdafcc7d2"
},
"require": {
"diglactic/laravel-breadcrumbs": "^10.0",
@ -7554,15 +7644,16 @@
"laravel/framework": "~13",
"laravel/telescope": "^5.7",
"php": "^8.5",
"scaler-tech/laravel-saml2": "^2.7",
"sentry/sentry-laravel": "^4.10",
"uwil/laravel-activity-logs": "^6.0",
"uwil/laravel-common": "^6.0"
},
"require-dev": {
"driftingly/rector-laravel": "^2.3",
"larastan/larastan": "^3.9",
"orchestra/testbench": "^11.0",
"phpunit/php-code-coverage": "^11.0",
"phpunit/phpunit": "^11.0",
"laravel/pint": "^1.29",
"orchestra/testbench": "^11.1",
"slevomat/coding-standard": "^8.3",
"squizlabs/php_codesniffer": "^3.7",
"uwil/admin-secure-laravel": "^5.1"
@ -7586,8 +7677,8 @@
}
},
"scripts": {
"test": [
"vendor/bin/phpunit"
"post-autoload-dump": [
"@php ./vendor/bin/testbench package:discover --ansi"
],
"phpcs": [
"./vendor/bin/phpcs"
@ -7595,8 +7686,8 @@
"phpstan": [
"./vendor/bin/phpstan"
],
"post-autoload-dump": [
"@php ./vendor/bin/testbench package:discover --ansi"
"rector": [
"./vendor/bin/rector"
]
},
"license": [
@ -7611,21 +7702,19 @@
"support": {
"email": "digital@ldc.fr",
"issues": "https://lab.s-web.fr/ldc-web/projets/laravel-backend/-/issues"
},
"transport-options": {
"symlink": true,
"relative": true
}
},
{
"name": "uwil/laravel-common",
"version": "6.0.0",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/laravel-common.git",
"reference": "dbaeb3f980a1c7a17c92601c090d8687c6bae8e6"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/215/packages/composer/archives/uwil/laravel-common.zip?sha=dbaeb3f980a1c7a17c92601c090d8687c6bae8e6",
"reference": "dbaeb3f980a1c7a17c92601c090d8687c6bae8e6",
"shasum": ""
"type": "path",
"url": "packages/laravel-common",
"reference": "db3afe457554ee103cac4a92cf601dc6a39780b1"
},
"require": {
"diglactic/laravel-breadcrumbs": "^10.0",
@ -7639,10 +7728,10 @@
"uwil/laravel-i18n": "^3.0"
},
"require-dev": {
"driftingly/rector-laravel": "^2.3",
"larastan/larastan": "^3.9",
"orchestra/testbench": "^11.0",
"phpunit/php-code-coverage": "^11.0",
"phpunit/phpunit": "^11.0",
"laravel/pint": "^1.29",
"orchestra/testbench": "^11.1",
"slevomat/coding-standard": "^8.3",
"squizlabs/php_codesniffer": "^3.7"
},
@ -7655,12 +7744,12 @@
}
},
"autoload": {
"files": [
"src/Helpers/methods.php"
],
"psr-4": {
"Uwil\\LaravelCommon\\": "src/"
}
},
"files": [
"src/Helpers/methods.php"
]
},
"autoload-dev": {
"psr-4": {
@ -7668,11 +7757,17 @@
}
},
"scripts": {
"test": [
"vendor/bin/phpunit"
],
"post-autoload-dump": [
"@php ./vendor/bin/testbench package:discover --ansi"
],
"phpcs": [
"./vendor/bin/phpcs"
],
"phpstan": [
"./vendor/bin/phpstan"
],
"rector": [
"./vendor/bin/rector"
]
},
"license": [
@ -7687,21 +7782,19 @@
"support": {
"email": "digital@ldc.fr",
"issues": "https://lab.s-web.fr/ldc-web/projets/laravel-common/-/issues"
},
"transport-options": {
"symlink": true,
"relative": true
}
},
{
"name": "uwil/laravel-i18n",
"version": "3.0.0",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/laravel-i18n.git",
"reference": "a87ec12228efd4b13c4a7298631c6dc0f601be47"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/252/packages/composer/archives/uwil/laravel-i18n.zip?sha=a87ec12228efd4b13c4a7298631c6dc0f601be47",
"reference": "a87ec12228efd4b13c4a7298631c6dc0f601be47",
"shasum": ""
"type": "path",
"url": "packages/laravel-i18n",
"reference": "1d14e78522ec29cf95897fbeb1bd0ffd5279c93a"
},
"require": {
"illuminate/support": "~13",
@ -7712,10 +7805,10 @@
"uwil/laravel-common": "^6.0"
},
"require-dev": {
"driftingly/rector-laravel": "^2.3",
"larastan/larastan": "^3.9",
"laravel/pint": "^1.29",
"orchestra/testbench": "^11.0",
"phpunit/php-code-coverage": "^11.0",
"phpunit/phpunit": "^11.0",
"slevomat/coding-standard": "^8.3",
"squizlabs/php_codesniffer": "^3.7"
},
@ -7733,14 +7826,17 @@
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
],
"phpcs": [
"./vendor/bin/phpcs"
],
"phpstan": [
"./vendor/bin/phpstan"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump"
"rector": [
"./vendor/bin/rector"
]
},
"license": [
@ -7755,6 +7851,10 @@
"support": {
"email": "digital@ldc.fr",
"issues": "https://lab.s-web.fr/ldc-web/projets/laravel-i18n/-/issues"
},
"transport-options": {
"symlink": true,
"relative": true
}
},
{
@ -7843,16 +7943,16 @@
},
{
"name": "voku/portable-ascii",
"version": "2.1.0",
"version": "2.1.1",
"source": {
"type": "git",
"url": "https://github.com/voku/portable-ascii.git",
"reference": "d870a33f0f79d2b4579740b0620200221ee44aeb"
"reference": "8e1051fe39379367aecf014f41744ce7539a856f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/d870a33f0f79d2b4579740b0620200221ee44aeb",
"reference": "d870a33f0f79d2b4579740b0620200221ee44aeb",
"url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f",
"reference": "8e1051fe39379367aecf014f41744ce7539a856f",
"shasum": ""
},
"require": {
@ -7889,7 +7989,7 @@
],
"support": {
"issues": "https://github.com/voku/portable-ascii/issues",
"source": "https://github.com/voku/portable-ascii/tree/2.1.0"
"source": "https://github.com/voku/portable-ascii/tree/2.1.1"
},
"funding": [
{
@ -7913,7 +8013,7 @@
"type": "tidelift"
}
],
"time": "2026-04-16T23:10:39+00:00"
"time": "2026-04-26T05:33:54+00:00"
}
],
"packages-dev": [
@ -8944,11 +9044,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.51",
"version": "2.1.53",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
"reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/ef67586798c003274797b288a68b221e4270dca7",
"reference": "ef67586798c003274797b288a68b221e4270dca7",
"shasum": ""
},
"require": {
@ -8993,7 +9093,7 @@
"type": "github"
}
],
"time": "2026-04-21T18:22:01+00:00"
"time": "2026-04-28T16:09:00+00:00"
},
{
"name": "phpunit/php-code-coverage",

View file

@ -40,11 +40,6 @@ return [
'driver' => 'session',
'provider' => 'users',
],
'frontend' => [
'driver' => 'session',
'provider' => 'customers',
],
],
/**
@ -67,7 +62,7 @@ return [
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', Uwil\LaravelBackend\Models\User::class),
'model' => env('AUTH_MODEL', App\Models\User::class),
],
],
@ -91,10 +86,10 @@ return [
*/
'passwords' => [
'users' => [
'backend' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],

View file

@ -1,397 +0,0 @@
<?php
return [
/**
*--------------------------------------------------------------------------
* Tenant Model
*--------------------------------------------------------------------------
*
* This will allow you to override the tenant model with your own.
*
*/
'tenantModel' => \Slides\Saml2\Models\Tenant::class,
/**
*--------------------------------------------------------------------------
* Use built-in routes
*--------------------------------------------------------------------------
*
* If "useRoutes" set to true, the package defines five new routes:
*
* Method | URI | Name
* -------|---------------------------------|------------------
* POST | {routesPrefix}/{uuid}/acs | saml.acs
* GET | {routesPrefix}/{uuid}/login | saml.login
* GET | {routesPrefix}/{uuid}/logout | saml.logout
* GET | {routesPrefix}/{uuid}/metadata | saml.metadata
* GET | {routesPrefix}/{uuid}/sls | saml.sls
*
*/
'useRoutes' => true,
/**
*--------------------------------------------------------------------------
* Built-in routes prefix
*--------------------------------------------------------------------------
*
* Here you may define the prefix for built-in routes.
*
*/
'routesPrefix' => '/saml2',
/**
*--------------------------------------------------------------------------
* Middle groups to use for the SAML routes
*--------------------------------------------------------------------------
*
* Note, Laravel 5.2 requires a group which includes StartSession
*
*/
'routesMiddleware' => ['saml'],
/**
*--------------------------------------------------------------------------
* Signature validation
*--------------------------------------------------------------------------
*
* Set to true if you want to use parameters from $_SERVER to validate the signature.
*
*/
'retrieveParametersFromServer' => false,
/**
*--------------------------------------------------------------------------
* Login redirection URL.
*--------------------------------------------------------------------------
*
* The redirection URL after successful login.
*
*/
'loginRoute' => '/admin/login',
/**
*--------------------------------------------------------------------------
* Logout redirection URL.
*--------------------------------------------------------------------------
*
* The redirection URL after successful logout.
*
*/
'logoutRoute' => '/admin/login',
/**
*--------------------------------------------------------------------------
* Login error redirection URL.
*--------------------------------------------------------------------------
*
* The redirection URL after login failing.
*
*/
'errorRoute' => '/admin/login',
/**
*--------------------------------------------------------------------------
* Strict mode.
*--------------------------------------------------------------------------
*
* If 'strict' is True, then the PHP Toolkit will reject unsigned
* or unencrypted messages if it expects them signed or encrypted
* Also will reject the messages if not strictly follow the SAML
* standard: Destination, NameId, Conditions... are validated too.
*
*/
'strict' => true,
/**
*--------------------------------------------------------------------------
* Debug mode.
*--------------------------------------------------------------------------
*
* When enabled, errors must be printed.
*
*/
'debug' => env('SAML2_DEBUG', env('APP_DEBUG', false)),
/**
*--------------------------------------------------------------------------
* Whether to use `X-Forwarded-*` headers to determine port/domain/protocol.
*--------------------------------------------------------------------------
*
* If 'proxyVars' is True, then the Saml lib will trust proxy headers
* e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if
* your application is running behind a load balancer which terminates SSL.
*
*/
'proxyVars' => true,
/**
*--------------------------------------------------------------------------
* Service Provider configuration.
*--------------------------------------------------------------------------
*
* General setting of the service provider.
*
*/
'sp' => [
/**
*--------------------------------------------------------------------------
* NameID format.
*--------------------------------------------------------------------------
*
* Specifies constraints on the name identifier to be used to
* represent the requested subject.
*
*/
'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
/**
*--------------------------------------------------------------------------
* SP Certificates.
*--------------------------------------------------------------------------
*
* Usually x509cert and privateKey of the SP are provided by files placed at
* the certs folder. But we can also provide them with the following parameters.
*
*/
'x509cert' => env('SAML2_SP_CERT_x509', ''),
'privateKey' => env('SAML2_SP_CERT_PRIVATEKEY', ''),
/**
*--------------------------------------------------------------------------
* Identifier (URI) of the SP entity.
*--------------------------------------------------------------------------
*
* Leave blank to use the 'saml.metadata' route.
*
*/
'entityId' => env('SAML2_SP_ENTITYID', ''),
/**
*--------------------------------------------------------------------------
* The Assertion Consumer Service (ACS) URL.
*--------------------------------------------------------------------------
*
* URL Location where the <Response> from the IdP will be returned, using HTTP-POST binding.
* Leave blank to use the 'saml.acs' route.
*
*/
'assertionConsumerService' => [
'url' => '',
],
/**
*--------------------------------------------------------------------------
* The Single Logout Service URL.
*--------------------------------------------------------------------------
*
* Specifies info about where and how the <Logout Response> message MUST be
* returned to the requester, in this case our SP.
*
* URL Location where the <Response> from the IdP will be returned, using HTTP-Redirect binding.
* Leave blank to use the 'saml.sls' route.
*
*/
'singleLogoutService' => [
'url' => ''
],
],
/**
*--------------------------------------------------------------------------
* OneLogin security settings.
*--------------------------------------------------------------------------
*/
'security' => [
'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256',
/**
*--------------------------------------------------------------------------
* NameId encryption
*--------------------------------------------------------------------------
*
* Indicates that the nameID of the <samlp:logoutRequest> sent by this SP
* will be encrypted.
*
*/
'nameIdEncrypted' => true,
/**
*--------------------------------------------------------------------------
* AuthnRequest signage
*--------------------------------------------------------------------------
*
* Indicates whether the <samlp:AuthnRequest> messages sent by
* this SP will be signed. The Metadata of the SP will offer this info
*
*/
'authnRequestsSigned' => true,
/**
*--------------------------------------------------------------------------
* Logout request signage
*--------------------------------------------------------------------------
*
* Indicates whether the <samlp:logoutRequest> messages sent by this SP
* will be signed.
*
*/
'logoutRequestSigned' => true,
/**
*--------------------------------------------------------------------------
* Logout response signage
*--------------------------------------------------------------------------
*
* Indicates whether the <samlp:logoutResponse> messages sent by this SP
* will be signed.
*
*/
'logoutResponseSigned' => true,
/**
*--------------------------------------------------------------------------
* Whether need to sign metadata.
*--------------------------------------------------------------------------
*
* The possible values:
* - false
* - true (use certs)
* - array:
* ```
* [
* 'keyFileName' => 'metadata.key',
* 'certFileName' => 'metadata.crt'
* ]
* ```
*
*/
'signMetadata' => true,
/**
*--------------------------------------------------------------------------
* Requirement to sign messages.
*--------------------------------------------------------------------------
*
* Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest> and
* <samlp:LogoutResponse> elements received by this SP to be signed.
*
*/
'wantMessagesSigned' => true,
/**
*--------------------------------------------------------------------------
* Requirement to sign assertion elements.
*--------------------------------------------------------------------------
*
* Indicates a requirement for the <saml:Assertion> elements received by
* this SP to be signed.
*
*/
'wantAssertionsSigned' => true,
/**
*--------------------------------------------------------------------------
* Requirement to encrypt NameID.
*--------------------------------------------------------------------------
*
* Indicates a requirement for the NameID received by this SP to be encrypted.
*
*/
'wantNameIdEncrypted' => false,
/**
*--------------------------------------------------------------------------
* Authentication context.
*--------------------------------------------------------------------------
*
* Set to false and no AuthContext will be sent in the AuthNRequest,
*
* Set true or don't present this parameter and you will get an
* AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
*
* Set an array with the possible auth context values:
* ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509']
*
*/
'requestedAuthnContext' => true,
],
/**
*--------------------------------------------------------------------------
* Contact information.
*--------------------------------------------------------------------------
*
* It is recommended to supply a technical and support contacts.
*
*/
'contactPerson' => [
'technical' => [
'givenName' => env('SAML2_CONTACT_TECHNICAL_NAME', env('APP_NAME', 'Technical')),
'emailAddress' => env('SAML2_CONTACT_TECHNICAL_EMAIL', 'no@reply.com')
],
'support' => [
'givenName' => env('SAML2_CONTACT_SUPPORT_NAME', env('APP_NAME', 'Support')),
'emailAddress' => env('SAML2_CONTACT_SUPPORT_EMAIL', 'no@reply.com')
],
],
/**
*--------------------------------------------------------------------------
* Organization information.
*--------------------------------------------------------------------------
*
* The info in en_US lang is recommended, add more if required.
*
*/
'organization' => [
'en-US' => [
'name' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')),
'displayname' => env('SAML2_ORGANIZATION_NAME', env('APP_NAME', 'Name')),
'url' => env('SAML2_ORGANIZATION_URL', env('APP_URL', 'http://url'))
],
],
/**
* Default tenant UUID.
*/
'default_tenant_uuid' => env('SAML2_DEFAULT_TENANT_UUID', null),
/**
* Target domain.
*/
'domain' => env('SAML2_DOMAIN', "")
];

View file

@ -42,7 +42,7 @@ return [
*
*/
'path' => env('TELESCOPE_PATH', 'admin/telescope'),
'path' => env('TELESCOPE_PATH', config('uwil-backend.routes.prefix') . '/telescope'),
/**
*--------------------------------------------------------------------------

32
config/uwil-backend.php Normal file
View file

@ -0,0 +1,32 @@
<?php
return [
/**
*--------------------------------------------------------------------------
* Routes
*--------------------------------------------------------------------------
*
* This option may be used to edit routes informations.
* - prefix: string.
* - name: string.
*/
'routes' => [
'prefix' => '/',
'name' => 'back.',
],
/**
*--------------------------------------------------------------------------
* Saml
*--------------------------------------------------------------------------
*
* This option may be used to active or not the saml connection.
* - enabled: boolean.
*/
'saml' => [
'enabled' => false,
],
];

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('user_passwords', function (Blueprint $table): void {
$table->id()->comment('Id of the user password.');;
$table->foreignId('user_id')->unique()->comment('Associated user.')
->constrained('users')->cascadeOnDelete();
$table->string('password')->comment('Password of the user.');
$table->timestamp('created_at')->nullable()->comment('Date the user password was created.');
$table->timestamp('updated_at')->nullable()->comment('Date the user password was updated.');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('user_passwords');
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('password_reset_tokens', function (Blueprint $table): void {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
}
};

44
lang/fr/auth.php Normal file
View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
return [
'failed' => 'Ces identifiants ne correspondent pas à nos enregistrements.',
'password' => 'Le mot de passe fourni est incorrect.',
'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez réessayer dans :seconds secondes.',
"login" => "Se connecter",
"remember" => "Se souvenir de moi",
"forgot_password" => "Mot de passe oublié",
"forgot_password_desc" => "Renseignez votre email pour recevoir un lien de réinitialisation.",
"description" => "Connectez-vous afin d'accéder à notre outil <strong>:appName</strong> !",
"connected" => "Connecté !",
"connected_desc" => "Vous êtes à présent authentifié au Support UWIL.",
"send_link" => "Envoyer le lien",
"mail_sent" => "Mail envoyé !",
"mail_sent_desc" => "Si un compte est associé à l'adresse :email, un e-mail de réinitialisation du mot de passe
vous sera envoyé dans quelques instants.",
"reset_password" => "Réinitialisation du mot de passe",
"reset_password_desc" => "Choisissez un nouveau mot de passe pour votre compte.",
"reset_your_password" => "Réinitialiser votre mot de passe",
"reset_sent" => "Réinitialisation du mot de passe effectué !",
"reset_sent_desc" => "Mot de passe mis à jour avec succès. Vous pouvez dès à présent vous connecter
avec vos nouveaux identifiants.",
'email_hello' => 'Bonjour !',
'email_get_reset_password' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation
du mot de passe de votre compte.',
'email_reset_link_expire' => 'Ce lien expirera dans :count minutes.',
'email_button_redirect' => 'Cliquez sur le bouton suivant afin de procéder au changement:',
'email_reset_password' => 'Réinitialiser le mot de passe',
'email_wrong_destination' => 'Si vous n\'avez pas demandé la réinitialisation de votre mot de passe, aucune
autre action n\'est requise.',
'email_cant_visualise' => 'Si vous avez des difficultés à cliquer sur le bouton \':actionText\' ou si vous ne
le voyez pas, copiez ou cliquez sur le lien suivant:',
'email_reset_complete' => 'La réinitialisation du mot de passe associé à votre compte a bien été
effectuée.',
'email_new_connection' => 'Vous pouvez désormais vour connecter avec votre nouvel identifiant et
mot de passe en cliquant sur le bouton suivant:',
'email_title_reset_password' => 'Réinitialisation du mot de passe',
'email_title_reset_complete' => 'Mot de passe modifié',
'reset_password' => 'Réinitialiser mot de passe',
'reset_password_desc' => 'Formulaire de réinitialisation du mot de passe de votre compte au back office.',
];

View file

@ -3,5 +3,5 @@
declare(strict_types=1);
return [
// "dashboard" => "Tableau de bord",
"your_field" => "Votre :field",
];

View file

@ -6,5 +6,6 @@ return [
"user-role" => [
"project-manager" => "Chef de projet",
"developper" => "Développeur",
"client" => "Client",
],
];

View file

@ -1,6 +0,0 @@
<?php
return [
"homepage" => "Bonjour Monde",
"back_to_your_website" => "Revenir à la page d'accueil",
];

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
return [
"custom" => [
// "published" => "publié",
"token" => "token",
"new_password" => "nouveau mot de passe",
],
];

View file

@ -38,11 +38,6 @@ data:
SCOUT_QUEUE: "${APP_SCOUT_QUEUE}"
MEILISEARCH_HOST: "meilisearch.common-services:7700"
MEILISEARCH_KEY: "${APP_MEILISEARCH_KEY}"
SAML2_SP_CERT_x509: "${APP_SAML2_SP_CERT_x509}"
SAML2_SP_CERT_PRIVATEKEY: "${APP_SAML2_SP_CERT_PRIVATEKEY}"
SAML2_CONTACT_TECHNICAL_EMAIL: "${APP_SAML2_CONTACT_TECHNICAL_EMAIL}"
SAML2_CONTACT_SUPPORT_EMAIL: "${APP_SAML2_CONTACT_SUPPORT_EMAIL}"
SAML2_DEFAULT_TENANT_UUID: "${APP_SAML2_DEFAULT_TENANT_UUID}"
UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}"
---
apiVersion: v1

View file

@ -9,6 +9,7 @@ import InputError from "@/back/Components/InputError";
import { cn } from "@/lib/utils";
type SimpleSwitchProps = {
id?: string;
name: string;
checked?: boolean;
label?: ReactNode | string;
@ -24,6 +25,7 @@ type SimpleSwitchProps = {
export default function SimpleSwitch({
name,
id = name,
checked,
label,
leftHelper,
@ -48,7 +50,7 @@ export default function SimpleSwitch({
<div className="flex items-center gap-3">
{leftHelper && <small>{leftHelper}</small>}
<Switch
id={name}
id={id}
name={name}
onCheckedChange={(value) => setData(name, value)}
required={required}

View file

@ -10,9 +10,9 @@ export default function AppFooter() {
const appName = (usePage().props.appEnv as AppEnv).name;
const year = new Date().getFullYear();
return (
<section className="relative flex flex-col justify-center items-center w-full pt-4 z-2">
<section className="relative flex flex-col justify-center items-center w-full py-4 z-2">
<Separator />
<p className="text-sm pt-4">
<p className="text-sm text-muted-foreground pt-4">
uwil/{str(appName).slug().value()} - UWIL © {year}
</p>
</section>

View file

@ -46,8 +46,8 @@ import { SidebarRegistry } from "@/back/Components/Layout/Sidebar/SidebarRegistr
import Views from "@/back/lib/actions";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
import { AppEnv, LaravelEnum } from "@/types/global";
import { AuthProp } from "@/back/types/global";
import { AppEnv } from "@/types/global";
import { AuthProp, UserModel } from "@/back/types/global";
import { GetTranslation } from "@/lib/utils";
export type NavListLinksProps = {
@ -124,10 +124,11 @@ export default function AppSidebar() {
const { appEnv, auth, enumUserRole } = usePage().props as unknown as {
appEnv: AppEnv;
auth: AuthProp;
enumUserRole: LaravelEnum;
enumUserRole: UserModel["role"][];
};
const { state } = useSidebar();
const prevStateRef = useRef(state);
const isConceptor = enumUserRole.find((role) => role.name === "conceptor");
const isCollapsed = state === "collapsed";
// Translations.
@ -165,7 +166,7 @@ export default function AppSidebar() {
},
{
title: str(transAdmin).plural().value(),
show: auth.user.role === enumUserRole.conceptor.value,
show: auth.user.role.value === isConceptor!.value,
links: [
...SidebarRegistry.getAll(auth, "admin"),
{

View file

@ -51,7 +51,7 @@ function LayoutContent({
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
style={{ maxWidth: `calc(100% - ${sidebarVar})` }}
className="relative flex flex-col justify-between transition-[max-width] duration-200 ease-linear w-full px-8 pb-4"
className="relative flex flex-col justify-between transition-[max-width] duration-200 ease-linear w-full px-8"
>
<div className="container flex flex-col justify-between mx-auto">
<AppHeader />

View file

@ -2,20 +2,26 @@
import { ReactNode } from "react";
// * Components shadcn
import { Card } from "@/components/ui/card";
import { Toaster } from "@/components/ui/toaster";
// * Other
import ApplicationLogo from "@/back/Components/ApplicationLogo";
import AppSettings from "@/back/Components/Layout/Settings/AppSettings";
import { ThemeProvider } from "@/back/providers/ThemeProvider";
import AppFooter from "@/back/Components/Layout/AppFooter";
export default function GuestLayout({ children }: { children: ReactNode }) {
return (
<ThemeProvider defaultTheme="light" storageKey="vite-ui-theme">
<AppSettings className="container absolute top-0 start-1/2 transform -translate-x-1/2 text-right my-4" />
<div className="flex min-h-screen flex-col justify-center items-center pt-6">
<ApplicationLogo />
<Card className="text-center shadow-md sm:max-w-md sm:rounded-lg bg-secondary w-full mt-6">
{children}
</Card>
<div className="flex flex-col min-h-screen">
<AppSettings className="container absolute top-0 start-1/2 transform -translate-x-1/2 text-right my-4" />
<section className="flex flex-1 flex-col justify-center items-center pt-4">
<ApplicationLogo />
<Card className="text-center shadow-md sm:max-w-md sm:rounded-lg bg-secondary w-full mt-6">
{children}
</Card>
</section>
<Toaster />
<AppFooter />
</div>
</ThemeProvider>
);

View file

@ -0,0 +1,87 @@
// * Packages.
import { Head, Link, useForm } from "@inertiajs/react";
import { route } from "ziggy-js";
import { LoaderCircleIcon, MoveLeftIcon, SendIcon } from "lucide-react";
import { SubmitEvent } from "react";
// * Components shadcn.
import { Button } from "@/components/ui/button";
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
// * Other.
import GuestLayout from "@/back/Layouts/GuestLayout";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import useTrans from "@/hooks/use-translations";
import { toast } from "@/hooks/use-toast";
export default function ForgotPassword() {
const { post, setData, data, processing } = useForm({
email: "",
});
const mailSent = useTrans("auth.mail_sent");
const mailSentDesc = useTrans("auth.mail_sent_desc", {
params: { email: data.email },
});
const submit = (event: SubmitEvent) => {
event.preventDefault();
post(route("back.forgot-password.post"), {
onSuccess: () => {
toast({
title: mailSent,
description: mailSentDesc,
});
},
});
};
return (
<GuestLayout>
<CardHeader>
<Head title={useTrans("auth.forgot_password")} />
<CardTitle>
<h1 className="text-2xl text-center font-extrabold tracking-tight">
{useTrans("auth.forgot_password")}
</h1>
</CardTitle>
<CardDescription>
{useTrans("auth.forgot_password_desc")}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={submit} className="space-y-3">
<SimpleString
id="email"
name="email"
type="email"
label="Votre email"
placeholder="ex: john.doe@ldc.fr"
wordCounter={false}
setData={setData}
/>
<div className="flex flex-row items-center justify-between gap-y-3">
<Link
href={route("back.login")}
className="flex justify-center items-center gap-1 rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
<MoveLeftIcon size={15} />
{useTrans("auth.login")}
</Link>
<Button type="submit" disabled={processing}>
{useTrans("auth.send_link")}
{processing ? (
<LoaderCircleIcon className="animate-spin" />
) : (
<SendIcon />
)}
</Button>
</div>
</form>
</CardContent>
</GuestLayout>
);
}

View file

@ -1,8 +1,9 @@
// * Packages
import { Head, usePage } from "@inertiajs/react";
import { SquareArrowOutUpRightIcon } from "lucide-react";
// * Packages.
import { Head, Link, useForm, usePage } from "@inertiajs/react";
import { route } from "ziggy-js";
// * Components shadcn
import { SubmitEvent } from "react";
import { LoaderCircleIcon, LogInIcon, MoveRightIcon } from "lucide-react";
// * Components shadcn.
import { Button } from "@/components/ui/button";
import {
CardContent,
@ -10,14 +11,40 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
// * Other
// * Other.
import GuestLayout from "@/back/Layouts/GuestLayout";
import InputError from "@/back/Components/InputError";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import useTrans from "@/hooks/use-translations";
import { AppEnv } from "@/types/global";
import { toast } from "@/hooks/use-toast";
import SimpleSwitch from "@/back/Components/Inputs/SimpleSwitch";
export default function Login({ tenantUUID }: { tenantUUID: string }) {
export default function Login() {
const appName = (usePage().props.appEnv as AppEnv).name;
/** Translations. */
const transConnected = useTrans("auth.connected");
const transConnectedDesc = useTrans("auth.connected_desc");
/** Form section. */
const { post, setData, data, processing } = useForm({
email: null,
password: null,
remember: false,
});
const submit = (event: SubmitEvent) => {
event.preventDefault();
post(route("back.login.post"), {
onSuccess: () => {
toast({
title: transConnected,
description: transConnectedDesc,
});
},
});
};
return (
<GuestLayout>
<CardHeader>
@ -27,25 +54,72 @@ export default function Login({ tenantUUID }: { tenantUUID: string }) {
{useTrans("uwil-backend::back.login_title")}
</h1>
</CardTitle>
<CardDescription>
<p
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: useTrans("uwil-backend::back.login_desc", {
params: { appName: String(appName) },
}),
}}
/>
</CardDescription>
<CardDescription
dangerouslySetInnerHTML={{
__html: useTrans("auth.description", {
params: { appName: String(appName) },
}),
}}
/>
</CardHeader>
<CardContent>
<InputError all />
<Button type="button" asChild>
<a href={route("saml.login", { uuid: tenantUUID })}>
{useTrans("uwil-backend::back.login_btn")}
<SquareArrowOutUpRightIcon />
</a>
</Button>
<form onSubmit={submit} className="space-y-3">
<SimpleString
id="email"
name="email"
type="email"
label={useTrans("back.your_field", {
params: { field: useTrans("validation.attributes.email") },
})}
placeholder="ex: john.doe@ldc.fr"
wordCounter={false}
required
setData={setData}
/>
<SimpleString
id="password"
name="password"
type="password"
label={useTrans("back.your_field", {
params: { field: useTrans("validation.attributes.password") },
})}
placeholder="********"
wordCounter={false}
required
setData={setData}
/>
<div className="flex flex-row justify-start items-center space-x-2 space-y-0">
<SimpleSwitch
id="remember-switch"
name="remember"
checked={data.remember}
setData={setData}
/>
<label
htmlFor="remember-switch"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{useTrans("auth.remember")}
</label>
</div>
<div className="flex flex-row items-center justify-between gap-y-3">
<Link
href={route("back.forgot-password")}
className="flex justify-center items-center gap-1 rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
{useTrans("auth.forgot_password")}
<MoveRightIcon size={15} />
</Link>
<Button type="submit" disabled={processing}>
{useTrans("auth.login")}
{processing ? (
<LoaderCircleIcon className="animate-spin" />
) : (
<LogInIcon />
)}
</Button>
</div>
</form>
</CardContent>
</GuestLayout>
);

View file

@ -0,0 +1,114 @@
// * Packages.
import { SubmitEvent } from "react";
import { Head, useForm } from "@inertiajs/react";
import { route } from "ziggy-js";
import { LoaderCircleIcon, ShieldCheckIcon } from "lucide-react";
// * Components shadcn.
import { Button } from "@/components/ui/button";
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
// * Other.
import GuestLayout from "@/back/Layouts/GuestLayout";
import SimpleString from "@/back/Components/Inputs/SimpleString";
import useTrans from "@/hooks/use-translations";
import { toast } from "@/hooks/use-toast";
type ResetPasswordProps = {
token: string;
email: string;
};
export default function ResetPassword({ token, email }: ResetPasswordProps) {
const resetSent = useTrans("auth.reset_sent");
const resetSentDesc = useTrans("auth.reset_sent_desc");
const { post, setData, processing } = useForm({
token,
email,
password: "",
password_confirmation: "",
});
const submit = (event: SubmitEvent) => {
event.preventDefault();
post(route("back.reset-password.post"), {
onSuccess: () => {
toast({
title: resetSent,
description: resetSentDesc,
});
},
});
};
return (
<GuestLayout>
<CardHeader>
<Head title={useTrans("auth.reset_password")} />
<CardTitle>
<h1 className="text-2xl text-center font-extrabold tracking-tight">
{useTrans("auth.reset_password")}
</h1>
</CardTitle>
<CardDescription>
{useTrans("auth.reset_password_desc")}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={submit} className="space-y-3">
<SimpleString
id="email"
name="email"
type="email"
label={useTrans("back.your_field", {
params: { field: useTrans("validation.attributes.email") },
})}
placeholder="ex: john.doe@ldc.fr"
wordCounter={false}
value={email}
setData={() => {}}
disabled={true}
/>
<SimpleString
id="password"
name="password"
type="password"
label={useTrans("back.your_field", {
params: { field: useTrans("validation.custom.new_password") },
})}
placeholder="********"
wordCounter={false}
setData={setData}
/>
<SimpleString
id="password_confirmation"
name="password_confirmation"
type="password"
label={useTrans("back.your_field", {
params: {
field: useTrans("validation.attributes.password_confirmation"),
},
})}
placeholder="********"
wordCounter={false}
setData={setData}
/>
<div className="flex justify-center">
<Button type="submit" disabled={processing}>
{useTrans("auth.reset_your_password")}
{processing ? (
<LoaderCircleIcon className="animate-spin" />
) : (
<ShieldCheckIcon />
)}
</Button>
</div>
</form>
</CardContent>
</GuestLayout>
);
}

View file

@ -15,7 +15,7 @@ export default function Default({ status }: { status: number }) {
<ThemeProvider defaultTheme="light" storageKey="vite-ui-theme">
<TooltipProvider>
<Head title={String(status)} />
<div className="flex min-h-screen flex-col justify-center items-center px-8 pb-4">
<div className="flex min-h-screen flex-col justify-center items-center px-8">
<div className="container grid grid-cols-1 gap-6 text-center mx-auto my-auto">
<ApplicationLogo />
<h1 className="text-4xl font-extrabold tracking-tight lg:text-5xl">
@ -30,9 +30,6 @@ export default function Default({ status }: { status: number }) {
{useTrans("uwil-backend::back.dashboard")}
</Link>
</Button>
<Button variant="outline" asChild>
<a href="/">{useTrans("uwil-backend::back.your_website")}</a>
</Button>
</div>
</div>
<AppFooter />

View file

@ -9,7 +9,6 @@ import FieldsetWrapper from "@/back/Components/FieldsetWrapper";
import useTrans from "@/hooks/use-translations";
import str from "@/hooks/use-string";
import InputError from "@/back/Components/InputError";
import { LaravelEnum } from "@/types/global";
import { AuthProp, UserModel } from "@/back/types/global";
import SubmitButton from "@/back/Components/SubmitButton";
import useCrudSubmit from "@/back/hooks/use-crud-submit";
@ -28,8 +27,8 @@ export default function Form({
method: number;
}) {
const userModel = (usePage().props.auth as AuthProp).user;
const enumUserRole = usePage().props.enumUserRole as LaravelEnum;
const isConceptor = userModel.role === enumUserRole.conceptor.value;
const enumUserRole = usePage().props.enumUserRole as UserModel["role"][];
const isConceptor = enumUserRole.find((role) => role.name === "conceptor");
const filterRoles = (value: number) => {
return (
Object.entries(enumUserRole)
@ -46,7 +45,7 @@ export default function Form({
email: model?.email,
published: model?.published ?? false,
published_at: model?.published_at,
role: model?.role,
role: model && model.role ? model.role.value : undefined,
});
const submit = useCrudSubmit({
@ -141,26 +140,26 @@ export default function Form({
.ucFirst()
.value()}
placeholder="ex: john.doe@gmail.com"
disabled={!isConceptor}
wordCounter={isConceptor}
value={!isConceptor ? model?.email : data.email}
disabled={!isConceptor!.value}
wordCounter={Boolean(isConceptor!.value)}
value={!isConceptor!.value ? model?.email : data.email}
setData={setData}
/>
{isConceptor && (
{Boolean(isConceptor!.value) && (
<EnumSelect
name="role"
options={
model?.role && userModel.role !== enumUserRole.conceptor.value
? filterRoles(model?.role)
model?.role && userModel.role.value !== isConceptor!.value
? filterRoles(model?.role.value)
: enumUserRole
}
label={transAssociateToRole}
selected={data.role ? String(data.role) : undefined}
setData={setData}
selected={data.role ? data.role : undefined}
setData={(name, value) => setData(name, value?.value ?? value)}
/>
)}
</FieldsetWrapper>
{isConceptor && (
{userModel.role.value === isConceptor!.value && (
<FieldsetWrapper
className="gap-4"
title={transFormTitleVisibility}

View file

@ -13,12 +13,11 @@ import DataTableFilterButton from "@/back/Components/Table/DataTableFilterButton
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import Views from "@/back/lib/actions";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
import { LaravelEnum } from "@/types/global";
import { AuthProp, Resource, UserModel } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
const getColumns = (enumUserRole: LaravelEnum) => {
const getColumns = () => {
return [
{
accessorKey: "first_name",
@ -80,19 +79,14 @@ const getColumns = (enumUserRole: LaravelEnum) => {
field={column.id}
/>
),
cell: ({ row }: { row: Row<UserModel> }) => (
<Badge variant="outline">
{str(
String(
Object.values(enumUserRole).find(
(role) => role.value === row.getValue("role"),
)?.label,
),
)
.ucFirst()
.value()}
</Badge>
),
cell: ({ row }: { row: Row<UserModel> }) => {
const rowRole = row.getValue("role") as UserModel["role"];
return (
<Badge variant="outline">
{str(String(rowRole?.label ?? '')).ucFirst().value()}
</Badge>
);
},
},
{
accessorKey: "published",
@ -160,9 +154,8 @@ export default function Index({
userModels: Resource;
searchFields: string;
}) {
const { auth, enumUserRole } = usePage().props as unknown as {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
enumUserRole: LaravelEnum;
};
const { create } = auth.policies.users;
const modelName = usePage().props.modelName as string;
@ -173,7 +166,7 @@ export default function Index({
Views.DELETION,
];
const columns = getColumns(enumUserRole);
const columns = getColumns();
const transCrudAddEntry = useTrans("uwil-backend::back.crud_add_entry");
const afterNode = () => {
return (

View file

@ -11,12 +11,11 @@ import ModelActions from "@/back/Components/ModelActions";
import BtnOptionnalLinkWithTooltip from "@/back/Components/BtnOptionnalLinkWithTooltip";
import Views from "@/back/lib/actions";
import { GetFormatedDate, GetTranslation } from "@/lib/utils";
import { LaravelEnum } from "@/types/global";
import { AuthProp, SingleResource, UserModel } from "@/back/types/global";
import str from "@/hooks/use-string";
import useTrans from "@/hooks/use-translations";
const getColumns = (enumUserRole: LaravelEnum) => {
const getColumns = () => {
return [
{
accessorKey: "first_name",
@ -54,16 +53,9 @@ const getColumns = (enumUserRole: LaravelEnum) => {
header: str(GetTranslation(`uwil-backend::validation.attributes.role`))
.ucFirst()
.value(),
cell: ({ value }: { value: number }) => (
cell: ({ value }: { value: UserModel["role"] }) => (
<Badge variant="outline">
{str(
String(
Object.values(enumUserRole).find((role) => role.value === value)
?.label,
),
)
.ucFirst()
.value()}
{str(String(value?.label ?? '')).ucFirst().value()}
</Badge>
),
},
@ -133,15 +125,14 @@ const getColumns = (enumUserRole: LaravelEnum) => {
};
export default function Show({ data }: { data: SingleResource }) {
const { auth, enumUserRole } = usePage().props as unknown as {
const { auth } = usePage().props as unknown as {
auth: AuthProp;
enumUserRole: LaravelEnum;
};
const { viewAny } = auth.policies.users;
const userModel = data.data as UserModel;
const actionModels = [Views.EDITION, Views.DUPLICATION, Views.DELETION];
const columns = getColumns(enumUserRole);
const columns = getColumns();
const transCrudBackList = useTrans("uwil-backend::back.crud_back_to_list");
const beforeNode = () => {

View file

@ -25,7 +25,7 @@ type UserModel = {
first_name: string;
last_name: string;
email: string;
role: number;
role: { value: number; label: string; name: string };
published: boolean;
published_at: Date;
created_at: Date;

View file

@ -0,0 +1,23 @@
{{-- blade-formatter-disable --}}
@component('mail::message')
# {{ __('auth.email_hello') }}
{{ __('auth.email_get_reset_password') }}
{{ __('auth.email_reset_link_expire', ['count' => config('auth.passwords.users.expire')]) }}
{{ __('auth.email_button_redirect') }}
@component('mail::button', ['url' => $url])
<strong>{{ __('auth.email_reset_password') }}</strong>
@endcomponent
{{ __('auth.email_wrong_destination') }}
{{ config('app.name') }}.
<hr>
<small>{{ __('auth.email_cant_visualise', ['actionText' => __('auth.email_reset_password')]) }}
<a href="{{ $url }}" target="_blank">{{ $url }}</a></small>
@endcomponent

View file

@ -0,0 +1,20 @@
{{-- blade-formatter-disable --}}
@component('mail::message')
# {{ __('auth.email_hello') }}
{{ __('auth.email_reset_complete') }}
{{ __('auth.email_new_connection') }}
@component('mail::button', ['url' => route('back.login')])
<strong>{{ __('auth.login') }}</strong>
@endcomponent
{{ config('app.name') }}.
<hr>
<small>{{ __('auth.email_cant_visualise', ['actionText' => __('auth.login')]) }}
<a href="{{ route('back.login') }}" target="_blank">{{ route('back.login') }}</a></small>
@endcomponent

30
routes/auth.php Normal file
View file

@ -0,0 +1,30 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use Illuminate\Support\Facades\Route;
Route::prefix(config('uwil-backend.routes.prefix'))
->name(config('uwil-backend.routes.name'))
->middleware(['web', 'admin-secure'])
->group(function (): void {
Route::middleware('guest:backend')->group(function (): void {
Route::get('login', [AuthenticatedSessionController::class, 'login'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'postLogin'])
->name('login.post');
Route::get('forgot-password', [AuthenticatedSessionController::class, 'forgotPassword'])
->name('forgot-password');
Route::post('forgot-password', [AuthenticatedSessionController::class, 'postForgotPassword'])
->name('forgot-password.post');
Route::get('reset-password/{token}', [AuthenticatedSessionController::class, 'resetPassword'])
->name('reset-password');
Route::post('reset-password', [AuthenticatedSessionController::class, 'postResetPassword'])
->name('reset-password.post');
});
Route::middleware(['auth:backend', 'verified'])->group(function (): void {
Route::get('logout', [AuthenticatedSessionController::class, 'logout'])
->name('logout');
});
});