feat: locale
This commit is contained in:
parent
19e65e36f3
commit
8e5a50dd2b
|
|
@ -35,6 +35,11 @@ class UserForm
|
||||||
->required()
|
->required()
|
||||||
->unique(ignoreRecord: true)
|
->unique(ignoreRecord: true)
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
Select::make('locale')
|
||||||
|
->label(__('admin.users.fields.locale'))
|
||||||
|
->options(self::localeOptions())
|
||||||
|
->default('en')
|
||||||
|
->required(),
|
||||||
FileUpload::make('avatar_url')
|
FileUpload::make('avatar_url')
|
||||||
->label(__('admin.users.fields.avatar'))
|
->label(__('admin.users.fields.avatar'))
|
||||||
->image()
|
->image()
|
||||||
|
|
@ -136,4 +141,18 @@ class UserForm
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private static function localeOptions(): array
|
||||||
|
{
|
||||||
|
$labels = __('admin.users.locales');
|
||||||
|
|
||||||
|
return collect(config('app.supported_locales', ['fr', 'en']))
|
||||||
|
->mapWithKeys(fn (string $locale): array => [
|
||||||
|
$locale => is_array($labels) ? ($labels[$locale] ?? $locale) : $locale,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ class AuthController extends Controller
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($data['locale']) && $user->locale !== $data['locale']) {
|
||||||
|
$user->forceFill(['locale' => $data['locale']])->save();
|
||||||
|
}
|
||||||
|
|
||||||
if (! $user->hasVerifiedEmail()) {
|
if (! $user->hasVerifiedEmail()) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => __('api.auth.email_not_verified'),
|
'message' => __('api.auth.email_not_verified'),
|
||||||
|
|
@ -162,9 +166,13 @@ class AuthController extends Controller
|
||||||
|
|
||||||
public function showResetPasswordForm(Request $request, string $token): View
|
public function showResetPasswordForm(Request $request, string $token): View
|
||||||
{
|
{
|
||||||
|
$email = (string) $request->query('email', '');
|
||||||
|
$locale = $this->localeForEmail($email);
|
||||||
|
|
||||||
return view('auth.reset-password', [
|
return view('auth.reset-password', [
|
||||||
'appName' => config('app.name'),
|
'appName' => config('app.name'),
|
||||||
'email' => (string) $request->query('email', ''),
|
'email' => $email,
|
||||||
|
'locale' => $locale,
|
||||||
'passwordReset' => false,
|
'passwordReset' => false,
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
]);
|
]);
|
||||||
|
|
@ -172,6 +180,9 @@ class AuthController extends Controller
|
||||||
|
|
||||||
public function resetPasswordFromView(ResetPasswordRequest $request): RedirectResponse|View
|
public function resetPasswordFromView(ResetPasswordRequest $request): RedirectResponse|View
|
||||||
{
|
{
|
||||||
|
$locale = $this->localeForEmail((string) $request->validated('email'));
|
||||||
|
app()->setLocale($locale);
|
||||||
|
|
||||||
$status = $this->resetPasswordFor($request->safe());
|
$status = $this->resetPasswordFor($request->safe());
|
||||||
|
|
||||||
if ($status !== Password::PASSWORD_RESET) {
|
if ($status !== Password::PASSWORD_RESET) {
|
||||||
|
|
@ -183,6 +194,7 @@ class AuthController extends Controller
|
||||||
return view('auth.reset-password', [
|
return view('auth.reset-password', [
|
||||||
'appName' => config('app.name'),
|
'appName' => config('app.name'),
|
||||||
'email' => strtolower($request->validated('email')),
|
'email' => strtolower($request->validated('email')),
|
||||||
|
'locale' => $locale,
|
||||||
'passwordReset' => true,
|
'passwordReset' => true,
|
||||||
'token' => '',
|
'token' => '',
|
||||||
]);
|
]);
|
||||||
|
|
@ -298,6 +310,11 @@ class AuthController extends Controller
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function localeForEmail(string $email): string
|
||||||
|
{
|
||||||
|
return User::where('email', strtolower($email))->value('locale') ?: 'en';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $data
|
* @param array<string, mixed> $data
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
|
|
@ -307,6 +324,7 @@ class AuthController extends Controller
|
||||||
$attributes = Arr::only($data, [
|
$attributes = Arr::only($data, [
|
||||||
'name',
|
'name',
|
||||||
'avatar_url',
|
'avatar_url',
|
||||||
|
'locale',
|
||||||
'height',
|
'height',
|
||||||
'bio',
|
'bio',
|
||||||
'sex',
|
'sex',
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Http\Requests;
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
class LoginRequest extends FormRequest
|
class LoginRequest extends FormRequest
|
||||||
{
|
{
|
||||||
|
|
@ -23,7 +24,24 @@ class LoginRequest extends FormRequest
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'string'],
|
'email' => ['required', 'string'],
|
||||||
|
'locale' => ['sometimes', 'string', Rule::in(config('app.supported_locales', ['fr', 'en']))],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->has('locale')) {
|
||||||
|
$this->merge([
|
||||||
|
'locale' => $this->normalizeLocale((string) $this->input('locale')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeLocale(string $locale): string
|
||||||
|
{
|
||||||
|
$locale = strtolower(str_replace('_', '-', $locale));
|
||||||
|
|
||||||
|
return explode('-', $locale)[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class RegisterRequest extends FormRequest
|
||||||
'unique:users,name',
|
'unique:users,name',
|
||||||
],
|
],
|
||||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||||
|
'locale' => ['sometimes', 'string', Rule::in(config('app.supported_locales', ['fr', 'en']))],
|
||||||
'password' => ['required', 'string', 'min:8'],
|
'password' => ['required', 'string', 'min:8'],
|
||||||
'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'],
|
'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'],
|
||||||
'bio' => ['nullable', 'string'],
|
'bio' => ['nullable', 'string'],
|
||||||
|
|
@ -56,4 +57,20 @@ class RegisterRequest extends FormRequest
|
||||||
'avatar.max' => __('api.validation.image_max_2mb'),
|
'avatar.max' => __('api.validation.image_max_2mb'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->has('locale')) {
|
||||||
|
$this->merge([
|
||||||
|
'locale' => $this->normalizeLocale((string) $this->input('locale')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeLocale(string $locale): string
|
||||||
|
{
|
||||||
|
$locale = strtolower(str_replace('_', '-', $locale));
|
||||||
|
|
||||||
|
return explode('-', $locale)[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class UpdateUserRequest extends FormRequest
|
||||||
return [
|
return [
|
||||||
'bio' => ['sometimes', 'nullable', 'string'],
|
'bio' => ['sometimes', 'nullable', 'string'],
|
||||||
'height' => ['sometimes', 'nullable', 'integer'],
|
'height' => ['sometimes', 'nullable', 'integer'],
|
||||||
|
'locale' => ['sometimes', 'string', Rule::in(config('app.supported_locales', ['fr', 'en']))],
|
||||||
'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'],
|
'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'],
|
||||||
'nutritionGoals' => ['sometimes', 'array'],
|
'nutritionGoals' => ['sometimes', 'array'],
|
||||||
'nutritionGoals.calories' => ['required_with:nutritionGoals', 'integer', 'min:0', 'max:100000'],
|
'nutritionGoals.calories' => ['required_with:nutritionGoals', 'integer', 'min:0', 'max:100000'],
|
||||||
|
|
@ -46,4 +47,20 @@ class UpdateUserRequest extends FormRequest
|
||||||
'nutritionGoals.*.max' => __('api.validation.nutrition_goals_max'),
|
'nutritionGoals.*.max' => __('api.validation.nutrition_goals_max'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
if ($this->has('locale')) {
|
||||||
|
$this->merge([
|
||||||
|
'locale' => $this->normalizeLocale((string) $this->input('locale')),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeLocale(string $locale): string
|
||||||
|
{
|
||||||
|
$locale = strtolower(str_replace('_', '-', $locale));
|
||||||
|
|
||||||
|
return explode('-', $locale)[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class UserResource extends JsonResource
|
||||||
return [
|
return [
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'email' => $this->email,
|
'email' => $this->email,
|
||||||
|
'locale' => $this->locale,
|
||||||
'emailVerified' => $this->hasVerifiedEmail(),
|
'emailVerified' => $this->hasVerifiedEmail(),
|
||||||
'emailVerifiedAt' => $this->email_verified_at?->toISOString(),
|
'emailVerifiedAt' => $this->email_verified_at?->toISOString(),
|
||||||
'height' => $this->height,
|
'height' => $this->height,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class ResetPasswordMail extends Mailable
|
||||||
public function envelope(): Envelope
|
public function envelope(): Envelope
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return new Envelope(
|
||||||
subject: 'Réinitialise ton mot de passe Bowli',
|
subject: trans('mail.reset_password.subject', [], $this->mailLocale()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,6 +40,7 @@ class ResetPasswordMail extends Mailable
|
||||||
view: 'emails.reset-password',
|
view: 'emails.reset-password',
|
||||||
with: [
|
with: [
|
||||||
'expiresInMinutes' => (int) config('auth.passwords.users.expire', 60),
|
'expiresInMinutes' => (int) config('auth.passwords.users.expire', 60),
|
||||||
|
'locale' => $this->mailLocale(),
|
||||||
'resetUrl' => $this->resetUrl,
|
'resetUrl' => $this->resetUrl,
|
||||||
'userName' => $this->user->name,
|
'userName' => $this->user->name,
|
||||||
],
|
],
|
||||||
|
|
@ -55,4 +56,9 @@ class ResetPasswordMail extends Mailable
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function mailLocale(): string
|
||||||
|
{
|
||||||
|
return $this->user->preferredLocale() ?? 'en';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class VerifyAccount extends Mailable
|
||||||
public function envelope(): Envelope
|
public function envelope(): Envelope
|
||||||
{
|
{
|
||||||
return new Envelope(
|
return new Envelope(
|
||||||
subject: 'Vérifie ton compte Daily Meal',
|
subject: trans('mail.verify_account.subject', [], $this->mailLocale()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,6 +40,7 @@ class VerifyAccount extends Mailable
|
||||||
view: 'emails.verify-account',
|
view: 'emails.verify-account',
|
||||||
with: [
|
with: [
|
||||||
'expiresInMinutes' => (int) config('auth.verification.expire', 60),
|
'expiresInMinutes' => (int) config('auth.verification.expire', 60),
|
||||||
|
'locale' => $this->mailLocale(),
|
||||||
'userName' => $this->user->name,
|
'userName' => $this->user->name,
|
||||||
'verificationUrl' => $this->verificationUrl,
|
'verificationUrl' => $this->verificationUrl,
|
||||||
],
|
],
|
||||||
|
|
@ -55,4 +56,9 @@ class VerifyAccount extends Mailable
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function mailLocale(): string
|
||||||
|
{
|
||||||
|
return $this->user->preferredLocale() ?? 'en';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use App\Enums\WeightGoal;
|
||||||
use Filament\Models\Contracts\FilamentUser;
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
|
use Illuminate\Contracts\Translation\HasLocalePreference;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
|
@ -18,7 +19,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Sanctum\HasApiTokens;
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
|
|
||||||
class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
class User extends Authenticatable implements FilamentUser, HasLocalePreference, MustVerifyEmail
|
||||||
{
|
{
|
||||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||||
use HasApiTokens, HasFactory, HasUlids, Notifiable;
|
use HasApiTokens, HasFactory, HasUlids, Notifiable;
|
||||||
|
|
@ -31,6 +32,7 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'email',
|
'email',
|
||||||
|
'locale',
|
||||||
'email_verified_at',
|
'email_verified_at',
|
||||||
'password',
|
'password',
|
||||||
'avatar_url',
|
'avatar_url',
|
||||||
|
|
@ -115,6 +117,11 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function preferredLocale(): ?string
|
||||||
|
{
|
||||||
|
return $this->locale ?: 'en';
|
||||||
|
}
|
||||||
|
|
||||||
public function isAdmin()
|
public function isAdmin()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class UserFactory extends Factory
|
||||||
return [
|
return [
|
||||||
'name' => fake()->name(),
|
'name' => fake()->name(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
|
'locale' => 'en',
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
'avatar_url' => null,
|
'avatar_url' => null,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('locale', 8)->default('en')->after('email');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('locale');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -18,6 +18,7 @@ return [
|
||||||
'id' => 'ID',
|
'id' => 'ID',
|
||||||
'name' => 'Name',
|
'name' => 'Name',
|
||||||
'email' => 'Email address',
|
'email' => 'Email address',
|
||||||
|
'locale' => 'Language',
|
||||||
'email_verified_at' => 'Email verified at',
|
'email_verified_at' => 'Email verified at',
|
||||||
'account_verified_at' => 'Account verified at',
|
'account_verified_at' => 'Account verified at',
|
||||||
'avatar' => 'Avatar',
|
'avatar' => 'Avatar',
|
||||||
|
|
@ -38,6 +39,10 @@ return [
|
||||||
'created_at' => 'Created at',
|
'created_at' => 'Created at',
|
||||||
'updated_at' => 'Updated at',
|
'updated_at' => 'Updated at',
|
||||||
],
|
],
|
||||||
|
'locales' => [
|
||||||
|
'en' => 'English',
|
||||||
|
'fr' => 'French',
|
||||||
|
],
|
||||||
'filters' => [
|
'filters' => [
|
||||||
'email_verified' => 'Email verified',
|
'email_verified' => 'Email verified',
|
||||||
'account_verified' => 'Account verified',
|
'account_verified' => 'Account verified',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'verify_account' => [
|
||||||
|
'subject' => 'Verify your Bowli account',
|
||||||
|
'title' => 'Verify your account',
|
||||||
|
'greeting' => 'Hi :name,',
|
||||||
|
'default_name' => 'there',
|
||||||
|
'intro' => 'Confirm your email address to activate your account and access Bowli.',
|
||||||
|
'action' => 'Verify my account',
|
||||||
|
'expires' => 'This link expires in :minutes minutes.',
|
||||||
|
'ignore' => 'If you did not create a Bowli account, you can ignore this email.',
|
||||||
|
],
|
||||||
|
'reset_password' => [
|
||||||
|
'subject' => 'Reset your Bowli password',
|
||||||
|
'title' => 'Reset your password',
|
||||||
|
'greeting' => 'Hi :name,',
|
||||||
|
'default_name' => 'there',
|
||||||
|
'intro' => 'Click the button below to create a new password. The link opens a secure Bowli API page.',
|
||||||
|
'action' => 'Choose a new password',
|
||||||
|
'expires' => 'This link expires in :minutes minutes.',
|
||||||
|
'copy_link' => 'If the button does not work, copy this link into your browser:',
|
||||||
|
'ignore' => 'If you did not request a password reset, you can ignore this email.',
|
||||||
|
],
|
||||||
|
'reset_password_form' => [
|
||||||
|
'page_title' => 'New password',
|
||||||
|
'success_title' => 'Password updated',
|
||||||
|
'title' => 'New password',
|
||||||
|
'intro' => 'Choose a new password to secure your account.',
|
||||||
|
'success' => 'Your password has been reset. You can return to the app and sign in.',
|
||||||
|
'email' => 'Email',
|
||||||
|
'password' => 'New password',
|
||||||
|
'password_confirmation' => 'Confirm password',
|
||||||
|
'submit' => 'Update password',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
@ -18,6 +18,7 @@ return [
|
||||||
'id' => 'ID',
|
'id' => 'ID',
|
||||||
'name' => 'Nom',
|
'name' => 'Nom',
|
||||||
'email' => 'Adresse email',
|
'email' => 'Adresse email',
|
||||||
|
'locale' => 'Langue',
|
||||||
'email_verified_at' => 'Email vérifié le',
|
'email_verified_at' => 'Email vérifié le',
|
||||||
'account_verified_at' => 'Compte vérifié le',
|
'account_verified_at' => 'Compte vérifié le',
|
||||||
'avatar' => 'Avatar',
|
'avatar' => 'Avatar',
|
||||||
|
|
@ -38,6 +39,10 @@ return [
|
||||||
'created_at' => 'Créé le',
|
'created_at' => 'Créé le',
|
||||||
'updated_at' => 'Mis à jour le',
|
'updated_at' => 'Mis à jour le',
|
||||||
],
|
],
|
||||||
|
'locales' => [
|
||||||
|
'en' => 'Anglais',
|
||||||
|
'fr' => 'Français',
|
||||||
|
],
|
||||||
'filters' => [
|
'filters' => [
|
||||||
'email_verified' => 'Email vérifié',
|
'email_verified' => 'Email vérifié',
|
||||||
'account_verified' => 'Compte vérifié',
|
'account_verified' => 'Compte vérifié',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'verify_account' => [
|
||||||
|
'subject' => 'Vérifie ton compte Bowli',
|
||||||
|
'title' => 'Vérifie ton compte',
|
||||||
|
'greeting' => 'Bonjour :name,',
|
||||||
|
'default_name' => 'à toi',
|
||||||
|
'intro' => 'Confirme ton adresse email pour activer ton compte et accéder à Bowli.',
|
||||||
|
'action' => 'Vérifier mon compte',
|
||||||
|
'expires' => 'Ce lien expire dans :minutes minutes.',
|
||||||
|
'ignore' => 'Si tu n’as pas créé de compte Bowli, tu peux ignorer cet email.',
|
||||||
|
],
|
||||||
|
'reset_password' => [
|
||||||
|
'subject' => 'Réinitialise ton mot de passe Bowli',
|
||||||
|
'title' => 'Réinitialise ton mot de passe',
|
||||||
|
'greeting' => 'Bonjour :name,',
|
||||||
|
'default_name' => 'à toi',
|
||||||
|
'intro' => 'Clique sur le bouton ci-dessous pour créer un nouveau mot de passe. Le lien ouvre une page sécurisée de l’API Bowli.',
|
||||||
|
'action' => 'Choisir un nouveau mot de passe',
|
||||||
|
'expires' => 'Ce lien expire dans :minutes minutes.',
|
||||||
|
'copy_link' => 'Si le bouton ne fonctionne pas, copie ce lien dans ton navigateur :',
|
||||||
|
'ignore' => 'Si tu n’as pas demandé de réinitialisation, tu peux ignorer cet email.',
|
||||||
|
],
|
||||||
|
'reset_password_form' => [
|
||||||
|
'page_title' => 'Nouveau mot de passe',
|
||||||
|
'success_title' => 'Mot de passe modifié',
|
||||||
|
'title' => 'Nouveau mot de passe',
|
||||||
|
'intro' => 'Choisis un nouveau mot de passe pour sécuriser ton compte.',
|
||||||
|
'success' => 'Ton mot de passe a bien été réinitialisé. Tu peux retourner dans l’application et te connecter.',
|
||||||
|
'email' => 'Email',
|
||||||
|
'password' => 'Nouveau mot de passe',
|
||||||
|
'password_confirmation' => 'Confirmer le mot de passe',
|
||||||
|
'submit' => 'Modifier le mot de passe',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="{{ $locale }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Nouveau mot de passe</title>
|
<title>{{ trans('mail.reset_password_form.page_title', [], $locale) }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
<body style="margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
||||||
<main style="width: min(100% - 32px, 520px); background: #ffffff; border-radius: 8px; padding: 32px; box-sizing: border-box;">
|
<main style="width: min(100% - 32px, 520px); background: #ffffff; border-radius: 8px; padding: 32px; box-sizing: border-box;">
|
||||||
|
|
@ -12,16 +12,16 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
||||||
{{ $passwordReset ? 'Mot de passe modifié' : 'Nouveau mot de passe' }}
|
{{ $passwordReset ? trans('mail.reset_password_form.success_title', [], $locale) : trans('mail.reset_password_form.title', [], $locale) }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@if ($passwordReset)
|
@if ($passwordReset)
|
||||||
<p style="margin: 0; color: #232323; font-size: 16px; line-height: 24px;">
|
<p style="margin: 0; color: #232323; font-size: 16px; line-height: 24px;">
|
||||||
Ton mot de passe a bien été réinitialisé. Tu peux retourner dans l’application et te connecter.
|
{{ trans('mail.reset_password_form.success', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
@else
|
@else
|
||||||
<p style="margin: 0 0 24px; color: #232323; font-size: 16px; line-height: 24px;">
|
<p style="margin: 0 0 24px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||||
Choisis un nouveau mot de passe pour sécuriser ton compte.
|
{{ trans('mail.reset_password_form.intro', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if ($errors->any())
|
@if ($errors->any())
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<input type="hidden" name="token" value="{{ $token }}">
|
<input type="hidden" name="token" value="{{ $token }}">
|
||||||
|
|
||||||
<label style="display: grid; gap: 8px; color: #232323; font-size: 14px; font-weight: 700;">
|
<label style="display: grid; gap: 8px; color: #232323; font-size: 14px; font-weight: 700;">
|
||||||
Email
|
{{ trans('mail.reset_password_form.email', [], $locale) }}
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label style="display: grid; gap: 8px; color: #232323; font-size: 14px; font-weight: 700;">
|
<label style="display: grid; gap: 8px; color: #232323; font-size: 14px; font-weight: 700;">
|
||||||
Nouveau mot de passe
|
{{ trans('mail.reset_password_form.password', [], $locale) }}
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label style="display: grid; gap: 8px; color: #232323; font-size: 14px; font-weight: 700;">
|
<label style="display: grid; gap: 8px; color: #232323; font-size: 14px; font-weight: 700;">
|
||||||
Confirmer le mot de passe
|
{{ trans('mail.reset_password_form.password_confirmation', [], $locale) }}
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
name="password_confirmation"
|
name="password_confirmation"
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="submit" style="min-height: 48px; border: 0; border-radius: 8px; background: #007f3a; color: #ffffff; font-size: 16px; font-weight: 700; cursor: pointer;">
|
<button type="submit" style="min-height: 48px; border: 0; border-radius: 8px; background: #007f3a; color: #ffffff; font-size: 16px; font-weight: 700; cursor: pointer;">
|
||||||
Modifier le mot de passe
|
{{ trans('mail.reset_password_form.submit', [], $locale) }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="{{ $locale }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Réinitialise ton mot de passe Bowli</title>
|
<title>{{ trans('mail.reset_password.subject', [], $locale) }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; padding: 0; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
<body style="margin: 0; padding: 0; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f5efe6; padding: 32px 16px;">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f5efe6; padding: 32px 16px;">
|
||||||
|
|
@ -17,33 +17,33 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
||||||
Réinitialise ton mot de passe
|
{{ trans('mail.reset_password.title', [], $locale) }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p style="margin: 0 0 16px; color: #232323; font-size: 16px; line-height: 24px;">
|
<p style="margin: 0 0 16px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||||
Bonjour {{ $userName ?: 'à toi' }},
|
{{ trans('mail.reset_password.greeting', ['name' => $userName ?: trans('mail.reset_password.default_name', [], $locale)], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 24px; color: #232323; font-size: 16px; line-height: 24px;">
|
<p style="margin: 0 0 24px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||||
Clique sur le bouton ci-dessous pour créer un nouveau mot de passe. Le lien ouvre une page sécurisée de l’API Bowli.
|
{{ trans('mail.reset_password.intro', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 0 24px;">
|
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 0 24px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background: #007f3a; border-radius: 999px;">
|
<td style="background: #007f3a; border-radius: 999px;">
|
||||||
<a href="{{ $resetUrl }}" style="display: inline-block; padding: 14px 24px; color: #ffffff; font-size: 16px; font-weight: 700; text-decoration: none;">
|
<a href="{{ $resetUrl }}" style="display: inline-block; padding: 14px 24px; color: #ffffff; font-size: 16px; font-weight: 700; text-decoration: none;">
|
||||||
Choisir un nouveau mot de passe
|
{{ trans('mail.reset_password.action', [], $locale) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
||||||
Ce lien expire dans {{ $expiresInMinutes }} minutes.
|
{{ trans('mail.reset_password.expires', ['minutes' => $expiresInMinutes], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
||||||
Si le bouton ne fonctionne pas, copie ce lien dans ton navigateur :
|
{{ trans('mail.reset_password.copy_link', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 16px; word-break: break-all; color: #007f3a; font-size: 14px; line-height: 22px;">
|
<p style="margin: 0 0 16px; word-break: break-all; color: #007f3a; font-size: 14px; line-height: 22px;">
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 22px;">
|
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 22px;">
|
||||||
Si tu n’as pas demandé de réinitialisation, tu peux ignorer cet email.
|
{{ trans('mail.reset_password.ignore', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="{{ $locale }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Vérifie ton compte Daily Meal</title>
|
<title>{{ trans('mail.verify_account.subject', [], $locale) }}</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; padding: 0; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
<body style="margin: 0; padding: 0; background: #f5efe6; color: #232323; font-family: Arial, sans-serif;">
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f5efe6; padding: 32px 16px;">
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f5efe6; padding: 32px 16px;">
|
||||||
|
|
@ -13,37 +13,37 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding: 32px;">
|
<td style="padding: 32px;">
|
||||||
<p style="margin: 0 0 8px; color: #426fb3; font-size: 14px; font-weight: 700; text-transform: uppercase;">
|
<p style="margin: 0 0 8px; color: #426fb3; font-size: 14px; font-weight: 700; text-transform: uppercase;">
|
||||||
Daily Meal
|
Bowli
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
<h1 style="margin: 0 0 16px; color: #000000; font-size: 28px; line-height: 34px;">
|
||||||
Vérifie ton compte
|
{{ trans('mail.verify_account.title', [], $locale) }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p style="margin: 0 0 16px; color: #232323; font-size: 16px; line-height: 24px;">
|
<p style="margin: 0 0 16px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||||
Bonjour {{ $userName ?: 'à toi' }},
|
{{ trans('mail.verify_account.greeting', ['name' => $userName ?: trans('mail.verify_account.default_name', [], $locale)], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0 0 24px; color: #232323; font-size: 16px; line-height: 24px;">
|
<p style="margin: 0 0 24px; color: #232323; font-size: 16px; line-height: 24px;">
|
||||||
Confirme ton adresse email pour activer ton compte et accéder à Daily Meal.
|
{{ trans('mail.verify_account.intro', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 0 24px;">
|
<table role="presentation" cellpadding="0" cellspacing="0" style="margin: 0 0 24px;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background: #000000; border-radius: 999px;">
|
<td style="background: #000000; border-radius: 999px;">
|
||||||
<a href="{{ $verificationUrl }}" style="display: inline-block; padding: 14px 24px; color: #ffffff; font-size: 16px; font-weight: 700; text-decoration: none;">
|
<a href="{{ $verificationUrl }}" style="display: inline-block; padding: 14px 24px; color: #ffffff; font-size: 16px; font-weight: 700; text-decoration: none;">
|
||||||
Vérifier mon compte
|
{{ trans('mail.verify_account.action', [], $locale) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
<p style="margin: 0 0 16px; color: #666666; font-size: 14px; line-height: 22px;">
|
||||||
Ce lien expire dans {{ $expiresInMinutes }} minutes.
|
{{ trans('mail.verify_account.expires', ['minutes' => $expiresInMinutes], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 22px;">
|
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 22px;">
|
||||||
Si tu n’as pas créé de compte Daily Meal, tu peux ignorer cet email.
|
{{ trans('mail.verify_account.ignore', [], $locale) }}
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ it('sends a verification email when a user registers', function () {
|
||||||
'name' => 'Leon',
|
'name' => 'Leon',
|
||||||
'email' => 'leon@example.com',
|
'email' => 'leon@example.com',
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
|
'locale' => 'fr-FR',
|
||||||
'physicalActivityLevel' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value,
|
'physicalActivityLevel' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value,
|
||||||
'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value,
|
'weightGoal' => WeightGoal::MAINTAIN_WEIGHT->value,
|
||||||
'pacePreference' => PacePreference::NORMAL->value,
|
'pacePreference' => PacePreference::NORMAL->value,
|
||||||
|
|
@ -29,6 +30,7 @@ it('sends a verification email when a user registers', function () {
|
||||||
])
|
])
|
||||||
->assertCreated()
|
->assertCreated()
|
||||||
->assertJsonPath('user.email', 'leon@example.com')
|
->assertJsonPath('user.email', 'leon@example.com')
|
||||||
|
->assertJsonPath('user.locale', 'fr')
|
||||||
->assertJsonPath('user.emailVerified', false)
|
->assertJsonPath('user.emailVerified', false)
|
||||||
->assertJsonPath('user.physicalActivityLevel', PhysicalActivityLevel::LIGHTLY_ACTIVE->value)
|
->assertJsonPath('user.physicalActivityLevel', PhysicalActivityLevel::LIGHTLY_ACTIVE->value)
|
||||||
->assertJsonPath('user.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value)
|
->assertJsonPath('user.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value)
|
||||||
|
|
@ -42,6 +44,7 @@ it('sends a verification email when a user registers', function () {
|
||||||
|
|
||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
|
'locale' => 'fr',
|
||||||
'physical_activity_level' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value,
|
'physical_activity_level' => PhysicalActivityLevel::LIGHTLY_ACTIVE->value,
|
||||||
'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value,
|
'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value,
|
||||||
'pace_preference' => PacePreference::NORMAL->value,
|
'pace_preference' => PacePreference::NORMAL->value,
|
||||||
|
|
@ -54,7 +57,9 @@ it('sends a verification email when a user registers', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the custom account verification mailable', function () {
|
it('uses the custom account verification mailable', function () {
|
||||||
$user = User::factory()->unverified()->create();
|
$user = User::factory()->unverified()->create([
|
||||||
|
'locale' => 'fr',
|
||||||
|
]);
|
||||||
|
|
||||||
$mailable = (new VerifyEmail)->toMail($user);
|
$mailable = (new VerifyEmail)->toMail($user);
|
||||||
|
|
||||||
|
|
@ -65,6 +70,21 @@ it('uses the custom account verification mailable', function () {
|
||||||
->assertSeeInHtml($user->name);
|
->assertSeeInHtml($user->name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the user locale for account verification emails', function () {
|
||||||
|
$user = User::factory()->unverified()->create([
|
||||||
|
'locale' => 'en',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mailable = (new VerifyEmail)->toMail($user);
|
||||||
|
|
||||||
|
expect($mailable)->toBeInstanceOf(VerifyAccount::class);
|
||||||
|
|
||||||
|
$mailable
|
||||||
|
->assertSeeInHtml('Verify my account')
|
||||||
|
->assertSeeInHtml('Confirm your email address')
|
||||||
|
->assertDontSeeInHtml('Vérifier mon compte');
|
||||||
|
});
|
||||||
|
|
||||||
it('generates verification links with a relative signature', function () {
|
it('generates verification links with a relative signature', function () {
|
||||||
$user = User::factory()->unverified()->create();
|
$user = User::factory()->unverified()->create();
|
||||||
|
|
||||||
|
|
@ -132,17 +152,22 @@ it('blocks login for unverified users', function () {
|
||||||
|
|
||||||
it('allows login for verified users', function () {
|
it('allows login for verified users', function () {
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
|
'locale' => 'en',
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->postJson('/api/auth/login', [
|
$this->postJson('/api/auth/login', [
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
|
'locale' => 'fr-FR',
|
||||||
'password' => 'password',
|
'password' => 'password',
|
||||||
])
|
])
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('user.email', $user->email)
|
->assertJsonPath('user.email', $user->email)
|
||||||
|
->assertJsonPath('user.locale', 'fr')
|
||||||
->assertJsonPath('user.emailVerified', true)
|
->assertJsonPath('user.emailVerified', true)
|
||||||
->assertCookie('token');
|
->assertCookie('token');
|
||||||
|
|
||||||
|
expect($user->fresh()->locale)->toBe('fr');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resends a verification email for an unverified user without authentication', function () {
|
it('resends a verification email for an unverified user without authentication', function () {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ it('sends a password reset email with a clickable api web url', function () {
|
||||||
|
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'email' => 'leon@example.com',
|
'email' => 'leon@example.com',
|
||||||
|
'locale' => 'fr',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this
|
$this
|
||||||
|
|
@ -58,6 +59,38 @@ it('sends a password reset email with a clickable api web url', function () {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the user locale for password reset emails', function () {
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'leon@example.com',
|
||||||
|
'locale' => 'en',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this
|
||||||
|
->postJson('/api/auth/forgot-password', [
|
||||||
|
'email' => $user->email,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
Notification::assertSentTo(
|
||||||
|
$user,
|
||||||
|
ResetPassword::class,
|
||||||
|
function (ResetPassword $notification) use ($user): bool {
|
||||||
|
$mail = $notification->toMail($user);
|
||||||
|
|
||||||
|
expect($mail)->toBeInstanceOf(ResetPasswordMail::class);
|
||||||
|
|
||||||
|
$mail
|
||||||
|
->assertSeeInHtml('Reset your password')
|
||||||
|
->assertSeeInHtml('Choose a new password')
|
||||||
|
->assertDontSeeInHtml('Réinitialise ton mot de passe');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not reveal whether a reset email can be sent', function () {
|
it('does not reveal whether a reset email can be sent', function () {
|
||||||
Notification::fake();
|
Notification::fake();
|
||||||
|
|
||||||
|
|
@ -100,6 +133,7 @@ it('resets the password and invalidates existing api tokens', function () {
|
||||||
it('shows the api password reset form from the email link', function () {
|
it('shows the api password reset form from the email link', function () {
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'email' => 'leon@example.com',
|
'email' => 'leon@example.com',
|
||||||
|
'locale' => 'fr',
|
||||||
]);
|
]);
|
||||||
$token = Password::broker()->createToken($user);
|
$token = Password::broker()->createToken($user);
|
||||||
|
|
||||||
|
|
@ -114,9 +148,29 @@ it('shows the api password reset form from the email link', function () {
|
||||||
->assertSee($user->email);
|
->assertSee($user->email);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the api password reset form in the user locale', function () {
|
||||||
|
$user = User::factory()->create([
|
||||||
|
'email' => 'leon@example.com',
|
||||||
|
'locale' => 'en',
|
||||||
|
]);
|
||||||
|
$token = Password::broker()->createToken($user);
|
||||||
|
|
||||||
|
$this
|
||||||
|
->get(route('password.reset', [
|
||||||
|
'email' => $user->email,
|
||||||
|
'token' => $token,
|
||||||
|
], absolute: false))
|
||||||
|
->assertOk()
|
||||||
|
->assertViewIs('auth.reset-password')
|
||||||
|
->assertSee('New password')
|
||||||
|
->assertSee('Update password')
|
||||||
|
->assertDontSee('Nouveau mot de passe');
|
||||||
|
});
|
||||||
|
|
||||||
it('resets the password from the api web form', function () {
|
it('resets the password from the api web form', function () {
|
||||||
$user = User::factory()->create([
|
$user = User::factory()->create([
|
||||||
'email' => 'leon@example.com',
|
'email' => 'leon@example.com',
|
||||||
|
'locale' => 'fr',
|
||||||
'password' => 'old-password',
|
'password' => 'old-password',
|
||||||
]);
|
]);
|
||||||
$token = Password::broker()->createToken($user);
|
$token = Password::broker()->createToken($user);
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ it('returns nutrition goals with the authenticated user', function () {
|
||||||
|
|
||||||
$this->getJson('/api/auth/me')
|
$this->getJson('/api/auth/me')
|
||||||
->assertOk()
|
->assertOk()
|
||||||
|
->assertJsonPath('data.locale', 'en')
|
||||||
->assertJsonPath('data.nutritionGoals.calories', 2400)
|
->assertJsonPath('data.nutritionGoals.calories', 2400)
|
||||||
->assertJsonPath('data.nutritionGoals.proteins', 140)
|
->assertJsonPath('data.nutritionGoals.proteins', 140)
|
||||||
->assertJsonPath('data.nutritionGoals.carbs', 280)
|
->assertJsonPath('data.nutritionGoals.carbs', 280)
|
||||||
|
|
@ -103,16 +104,19 @@ it('updates profile preferences for the authenticated user', function () {
|
||||||
'pacePreference' => PacePreference::NORMAL->value,
|
'pacePreference' => PacePreference::NORMAL->value,
|
||||||
'sex' => UserSex::MAN->value,
|
'sex' => UserSex::MAN->value,
|
||||||
'dateOfBirth' => '2003-04-24',
|
'dateOfBirth' => '2003-04-24',
|
||||||
|
'locale' => 'fr-FR',
|
||||||
])
|
])
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.physicalActivityLevel', PhysicalActivityLevel::MODERATELY_ACTIVE->value)
|
->assertJsonPath('data.physicalActivityLevel', PhysicalActivityLevel::MODERATELY_ACTIVE->value)
|
||||||
->assertJsonPath('data.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value)
|
->assertJsonPath('data.weightGoal', WeightGoal::MAINTAIN_WEIGHT->value)
|
||||||
->assertJsonPath('data.pacePreference', PacePreference::NORMAL->value)
|
->assertJsonPath('data.pacePreference', PacePreference::NORMAL->value)
|
||||||
->assertJsonPath('data.sex', UserSex::MAN->value)
|
->assertJsonPath('data.sex', UserSex::MAN->value)
|
||||||
|
->assertJsonPath('data.locale', 'fr')
|
||||||
->assertJsonPath('data.dateOfBirth', '2003-04-24');
|
->assertJsonPath('data.dateOfBirth', '2003-04-24');
|
||||||
|
|
||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'id' => $user->id,
|
'id' => $user->id,
|
||||||
|
'locale' => 'fr',
|
||||||
'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE->value,
|
'physical_activity_level' => PhysicalActivityLevel::MODERATELY_ACTIVE->value,
|
||||||
'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value,
|
'weight_goal' => WeightGoal::MAINTAIN_WEIGHT->value,
|
||||||
'pace_preference' => PacePreference::NORMAL->value,
|
'pace_preference' => PacePreference::NORMAL->value,
|
||||||
|
|
@ -130,6 +134,7 @@ it('validates profile preferences', function () {
|
||||||
'weightGoal' => 'bulk',
|
'weightGoal' => 'bulk',
|
||||||
'pacePreference' => 'urgent',
|
'pacePreference' => 'urgent',
|
||||||
'sex' => 'x',
|
'sex' => 'x',
|
||||||
|
'locale' => 'es',
|
||||||
'dateOfBirth' => 'tomorrow',
|
'dateOfBirth' => 'tomorrow',
|
||||||
])
|
])
|
||||||
->assertUnprocessable()
|
->assertUnprocessable()
|
||||||
|
|
@ -138,6 +143,7 @@ it('validates profile preferences', function () {
|
||||||
'weightGoal',
|
'weightGoal',
|
||||||
'pacePreference',
|
'pacePreference',
|
||||||
'sex',
|
'sex',
|
||||||
|
'locale',
|
||||||
'dateOfBirth',
|
'dateOfBirth',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue