Merge pull request 'feat: update user registration to use username instead of name, add user profile model and migration, and implement gender enum' (#4) from feat/user-profile-and-username into dev
Reviewed-on: #4
This commit is contained in:
commit
b8e807d30b
@ -12,11 +12,6 @@ class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
use PasswordValidationRules, ProfileValidationRules;
|
||||
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array<string, string> $input
|
||||
*/
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
@ -25,7 +20,7 @@ public function create(array $input): User
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'username' => $input['username'],
|
||||
'email' => $input['email'],
|
||||
'password' => $input['password'],
|
||||
]);
|
||||
|
||||
@ -3,39 +3,32 @@
|
||||
namespace App\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
trait ProfileValidationRules
|
||||
{
|
||||
/**
|
||||
* Get the validation rules used to validate user profiles.
|
||||
*
|
||||
* @return array<string, array<int, ValidationRule|array<mixed>|string>>
|
||||
*/
|
||||
protected function profileRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->nameRules(),
|
||||
'username' => $this->usernameRules($userId),
|
||||
'email' => $this->emailRules($userId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user names.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function nameRules(): array
|
||||
protected function usernameRules(?int $userId = null): array
|
||||
{
|
||||
return ['required', 'string', 'max:255'];
|
||||
return [
|
||||
'required',
|
||||
'string',
|
||||
'max:20',
|
||||
'min:3',
|
||||
'alpha_dash',
|
||||
$userId === null
|
||||
? Rule::unique(User::class)
|
||||
: Rule::unique(User::class)->ignore($userId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules used to validate user emails.
|
||||
*
|
||||
* @return array<int, ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
protected function emailRules(?int $userId = null): array
|
||||
{
|
||||
return [
|
||||
|
||||
17
app/Enums/Gender.php
Normal file
17
app/Enums/Gender.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum Gender: string
|
||||
{
|
||||
case Male = 'male';
|
||||
case Female = 'female';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Male => 'Laki-laki',
|
||||
self::Female => 'Perempuan',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -2,49 +2,40 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Laravel\Fortify\Contracts\PasskeyUser;
|
||||
use Laravel\Fortify\PasskeyAuthenticatable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
* @property string $password
|
||||
* @property string|null $two_factor_secret
|
||||
* @property string|null $two_factor_recovery_codes
|
||||
* @property Carbon|null $two_factor_confirmed_at
|
||||
* @property string|null $remember_token
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
*/
|
||||
#[Fillable(['name', 'email', 'password'])]
|
||||
#[Hidden(['password', 'two_factor_secret', 'two_factor_recovery_codes', 'remember_token'])]
|
||||
class User extends Authenticatable implements PasskeyUser
|
||||
#[Hidden(['password'])]
|
||||
#[Guarded(['id', 'last_login_at'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable, PasskeyAuthenticatable, TwoFactorAuthenticatable;
|
||||
use HasFactory, Notifiable, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
'last_login_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
protected function fullName(): Attribute
|
||||
{
|
||||
return Attribute::get(function () {
|
||||
return $this->profile?->full_name ?? $this->username;
|
||||
});
|
||||
}
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(UserProfile::class);
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Models/UserProfile.php
Normal file
28
app/Models/UserProfile.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
class UserProfile extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'gender' => Gender::class,
|
||||
'birth_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
@ -16,17 +15,11 @@
|
||||
|
||||
class FortifyServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->configureActions();
|
||||
@ -34,18 +27,11 @@ public function boot(): void
|
||||
$this->configureRateLimiting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify actions.
|
||||
*/
|
||||
private function configureActions(): void
|
||||
{
|
||||
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
|
||||
Fortify::createUsersUsing(CreateNewUser::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Fortify views.
|
||||
*/
|
||||
private function configureViews(): void
|
||||
{
|
||||
Fortify::loginView(fn (Request $request) => Inertia::render('auth/login', [
|
||||
@ -67,18 +53,11 @@ private function configureViews(): void
|
||||
'status' => $request->session()->get('status'),
|
||||
]));
|
||||
|
||||
Fortify::registerView(fn () => Inertia::render('auth/register', [
|
||||
'passwordRules' => Password::defaults()->toPasswordRulesString(),
|
||||
]));
|
||||
|
||||
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
|
||||
|
||||
Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure rate limiting.
|
||||
*/
|
||||
private function configureRateLimiting(): void
|
||||
{
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
@ -90,11 +69,5 @@ private function configureRateLimiting(): void
|
||||
|
||||
return Limit::perMinute(5)->by($throttleKey);
|
||||
});
|
||||
|
||||
RateLimiter::for('passkeys', function (Request $request) {
|
||||
return Limit::perMinute(10)->by(
|
||||
($request->input('credential.id') ?: $request->session()->getId()).'|'.$request->ip(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
'username' => 'email',
|
||||
'username' => 'username',
|
||||
|
||||
'email' => 'email',
|
||||
|
||||
@ -117,7 +117,6 @@
|
||||
'limiters' => [
|
||||
'login' => 'login',
|
||||
'two-factor' => 'two-factor',
|
||||
'passkeys' => 'passkeys',
|
||||
],
|
||||
|
||||
/*
|
||||
@ -133,22 +132,6 @@
|
||||
|
||||
'views' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Passkeys
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These settings configure Fortify's passkey (WebAuthn) support.
|
||||
|
|
||||
*/
|
||||
|
||||
'passkeys' => [
|
||||
'relying_party_id' => parse_url(config('app.url'), PHP_URL_HOST),
|
||||
'allowed_origins' => [config('app.url')],
|
||||
'user_handle_secret' => env('PASSKEYS_USER_HANDLE_SECRET', config('app.key')),
|
||||
'timeout' => 60000,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Features
|
||||
@ -161,16 +144,11 @@
|
||||
*/
|
||||
|
||||
'features' => [
|
||||
Features::registration(),
|
||||
Features::resetPasswords(),
|
||||
Features::emailVerification(),
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
// 'window' => 0
|
||||
]),
|
||||
Features::passkeys([
|
||||
'confirmPassword' => true,
|
||||
]),
|
||||
],
|
||||
|
||||
|
||||
@ -5,40 +5,28 @@
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'username' => fake()->unique()->userName(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
'is_active' => true,
|
||||
'last_login_at' => null,
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
@ -46,9 +34,6 @@ public function unverified(): static
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has two-factor authentication configured.
|
||||
*/
|
||||
public function withTwoFactor(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
|
||||
@ -6,19 +6,17 @@
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('username', 20)->unique();
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('last_login_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
@ -37,9 +35,6 @@ public function up(): void
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_profiles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('full_name', 150);
|
||||
$table->string('phone_number', 20)->nullable();
|
||||
$table->text('address')->nullable();
|
||||
$table->enum('gender', array_values(Gender::cases()))->nullable();
|
||||
$table->string('birth_place', 100)->nullable();
|
||||
$table->date('birth_date')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_profiles');
|
||||
}
|
||||
};
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
@ -10,16 +9,10 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
$this->call([
|
||||
UserSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
78
database/seeders/UserSeeder.php
Normal file
78
database/seeders/UserSeeder.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
|
||||
$users = [
|
||||
[
|
||||
'username' => 'pangestu',
|
||||
'email' => 'pangestu@student.itmpwk.ac.id',
|
||||
'profile' => [
|
||||
'full_name' => 'Yoga Pangestu',
|
||||
'phone_number' => '082121495806',
|
||||
'address' => 'Jl. Merdeka No. 1, Purwakarta',
|
||||
'gender' => 'male',
|
||||
'birth_place' => 'Subang',
|
||||
'birth_date' => '2005-03-13',
|
||||
],
|
||||
],
|
||||
[
|
||||
'username' => 'doni',
|
||||
'email' => 'doni@student.itmpwk.ac.id',
|
||||
'profile' => [
|
||||
'full_name' => 'Doni Setiawan Ramadhan',
|
||||
'phone_number' => '085793462823',
|
||||
'address' => 'Jl. Merdeka No. 1, Purwakarta',
|
||||
'gender' => 'male',
|
||||
'birth_place' => 'Purwakarta',
|
||||
'birth_date' => '2004-10-30',
|
||||
],
|
||||
],
|
||||
[
|
||||
'username' => 'asep',
|
||||
'email' => 'asep@student.itmpwk.ac.id',
|
||||
'profile' => [
|
||||
'full_name' => 'Asep Saepudin',
|
||||
'phone_number' => '081546505033',
|
||||
'address' => 'Jl. Merdeka No. 1, Purwakarta',
|
||||
'gender' => 'male',
|
||||
'birth_place' => 'Purwakarta',
|
||||
'birth_date' => '2005-09-24',
|
||||
],
|
||||
],
|
||||
[
|
||||
'username' => 'komoala',
|
||||
'email' => 'komoala@student.itmpwk.ac.id',
|
||||
'profile' => [
|
||||
'full_name' => 'Komala Dewi',
|
||||
'phone_number' => '085819894938',
|
||||
'address' => 'Jl. Merdeka No. 1, Purwakarta',
|
||||
'gender' => 'female',
|
||||
'birth_place' => 'Purwakarta',
|
||||
'birth_date' => '2005-05-04',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($users as $userData) {
|
||||
$profile = $userData['profile'];
|
||||
unset($userData['profile']);
|
||||
|
||||
$userData['password'] = Hash::make('Minimal8@');
|
||||
|
||||
$user = User::create($userData);
|
||||
$user->profile()->create($profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import AuthLayoutTemplate from '@/layouts/auth/auth-simple-layout';
|
||||
import AuthLayoutTemplate from '@/layouts/auth/auth-split-layout';
|
||||
|
||||
export default function AuthLayout({
|
||||
title = '',
|
||||
|
||||
@ -13,7 +13,14 @@ export default function AuthSplitLayout({
|
||||
return (
|
||||
<div className="relative grid h-dvh flex-col items-center justify-center px-8 sm:px-0 lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div className="relative hidden h-full flex-col bg-muted p-10 text-white lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-zinc-900" />
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url('https://images.pexels.com/photos/267507/pexels-photo-267507.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2')",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/60" />
|
||||
<Link
|
||||
href={home()}
|
||||
className="relative z-20 flex items-center text-lg font-medium"
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import {
|
||||
index as confirmOptions,
|
||||
store as confirmStore,
|
||||
} from '@/actions/Laravel/Passkeys/Http/Controllers/PasskeyConfirmationController';
|
||||
import InputError from '@/components/input-error';
|
||||
import PasskeyVerify from '@/components/passkey-verify';
|
||||
import PasswordInput from '@/components/password-input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -14,17 +9,7 @@ import { store } from '@/routes/password/confirm';
|
||||
export default function ConfirmPassword() {
|
||||
return (
|
||||
<>
|
||||
<Head title="Confirm password" />
|
||||
|
||||
<PasskeyVerify
|
||||
routes={{
|
||||
options: confirmOptions(),
|
||||
submit: confirmStore(),
|
||||
}}
|
||||
label="Confirm with passkey"
|
||||
loadingLabel="Confirming..."
|
||||
separator="Or confirm with password"
|
||||
/>
|
||||
<Head title="Konfirmasi password" />
|
||||
|
||||
<Form {...store.form()} resetOnSuccess={['password']}>
|
||||
{({ processing, errors }) => (
|
||||
@ -49,7 +34,7 @@ export default function ConfirmPassword() {
|
||||
data-test="confirm-password-button"
|
||||
>
|
||||
{processing && <Spinner />}
|
||||
Confirm password
|
||||
Konfirmasi password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,7 +45,7 @@ export default function ConfirmPassword() {
|
||||
}
|
||||
|
||||
ConfirmPassword.layout = {
|
||||
title: 'Confirm password',
|
||||
title: 'Konfirmasi password',
|
||||
description:
|
||||
'This is a secure area of the application. Please confirm your password before continuing.',
|
||||
'Ini adalah area aman dari aplikasi. Silakan konfirmasi password Anda sebelum melanjutkan.',
|
||||
};
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// Components
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import InputError from '@/components/input-error';
|
||||
@ -12,7 +11,7 @@ import { email } from '@/routes/password';
|
||||
export default function ForgotPassword({ status }: { status?: string }) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Forgot password" />
|
||||
<Head title="Lupa password" />
|
||||
|
||||
{status && (
|
||||
<div className="mb-4 text-center text-sm font-medium text-green-600">
|
||||
@ -25,14 +24,14 @@ export default function ForgotPassword({ status }: { status?: string }) {
|
||||
{({ processing, errors }) => (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Label htmlFor="email">Alamat email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
placeholder="email@example.com"
|
||||
placeholder="email@contoh.com"
|
||||
/>
|
||||
|
||||
<InputError message={errors.email} />
|
||||
@ -47,7 +46,7 @@ export default function ForgotPassword({ status }: { status?: string }) {
|
||||
{processing && (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Email password reset link
|
||||
Kirim link reset password
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@ -55,8 +54,8 @@ export default function ForgotPassword({ status }: { status?: string }) {
|
||||
</Form>
|
||||
|
||||
<div className="space-x-1 text-center text-sm text-muted-foreground">
|
||||
<span>Or, return to</span>
|
||||
<TextLink href={login()}>log in</TextLink>
|
||||
<span>Atau kembali ke</span>
|
||||
<TextLink href={login()}>masuk</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -64,6 +63,6 @@ export default function ForgotPassword({ status }: { status?: string }) {
|
||||
}
|
||||
|
||||
ForgotPassword.layout = {
|
||||
title: 'Forgot password',
|
||||
description: 'Enter your email to receive a password reset link',
|
||||
title: 'Lupa password',
|
||||
description: 'Masukkan email Anda untuk menerima link reset password',
|
||||
};
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import InputError from '@/components/input-error';
|
||||
import PasskeyVerify from '@/components/passkey-verify';
|
||||
import PasswordInput from '@/components/password-input';
|
||||
import TextLink from '@/components/text-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { register } from '@/routes';
|
||||
import { store } from '@/routes/login';
|
||||
import { request } from '@/routes/password';
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
|
||||
type Props = {
|
||||
status?: string;
|
||||
@ -20,9 +19,7 @@ type Props = {
|
||||
export default function Login({ status, canResetPassword }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Log in" />
|
||||
|
||||
<PasskeyVerify />
|
||||
<Head title="Masuk" />
|
||||
|
||||
<Form
|
||||
{...store.form()}
|
||||
@ -33,30 +30,28 @@ export default function Login({ status, canResetPassword }: Props) {
|
||||
<>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Label htmlFor="username">Email/Username{' '}<span className="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
autoComplete="email"
|
||||
placeholder="email@example.com"
|
||||
placeholder="Masukkan username Anda"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
<InputError message={errors.username} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Label htmlFor="password">Kata Sandi{' '}<span className="text-destructive">*</span></Label>
|
||||
{canResetPassword && (
|
||||
<TextLink
|
||||
href={request()}
|
||||
className="ml-auto text-sm"
|
||||
tabIndex={5}
|
||||
>
|
||||
Forgot your password?
|
||||
Lupa kata sandi?
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
@ -65,8 +60,7 @@ export default function Login({ status, canResetPassword }: Props) {
|
||||
name="password"
|
||||
required
|
||||
tabIndex={2}
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
placeholder="Kata Sandi"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
@ -77,7 +71,9 @@ export default function Login({ status, canResetPassword }: Props) {
|
||||
name="remember"
|
||||
tabIndex={3}
|
||||
/>
|
||||
<Label htmlFor="remember">Remember me</Label>
|
||||
<Label htmlFor="remember">
|
||||
Ingat saya
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@ -88,15 +84,45 @@ export default function Login({ status, canResetPassword }: Props) {
|
||||
data-test="login-button"
|
||||
>
|
||||
{processing && <Spinner />}
|
||||
Log in
|
||||
Masuk
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<TextLink href={register()} tabIndex={5}>
|
||||
Sign up
|
||||
</TextLink>
|
||||
<div className="relative my-2">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<Separator className="w-full" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Atau
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
|
||||
fill="#4285F4"
|
||||
/>
|
||||
<path
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
fill="#34A853"
|
||||
/>
|
||||
<path
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
fill="#FBBC05"
|
||||
/>
|
||||
<path
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
fill="#EA4335"
|
||||
/>
|
||||
</svg>
|
||||
Masuk dengan Google
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@ -112,6 +138,6 @@ export default function Login({ status, canResetPassword }: Props) {
|
||||
}
|
||||
|
||||
Login.layout = {
|
||||
title: 'Log in to your account',
|
||||
description: 'Enter your email and password below to log in',
|
||||
title: 'Masuk ke akun Anda',
|
||||
description: 'Masukkan username dan password Anda di bawah ini untuk masuk',
|
||||
};
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import InputError from '@/components/input-error';
|
||||
import PasswordInput from '@/components/password-input';
|
||||
import TextLink from '@/components/text-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { login } from '@/routes';
|
||||
import { store } from '@/routes/register';
|
||||
|
||||
type Props = {
|
||||
passwordRules: string;
|
||||
};
|
||||
|
||||
export default function Register({ passwordRules }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Register" />
|
||||
<Form
|
||||
{...store.form()}
|
||||
resetOnSuccess={['password', 'password_confirmation']}
|
||||
disableWhileProcessing
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
{({ processing, errors }) => (
|
||||
<>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
required
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
autoComplete="name"
|
||||
name="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError
|
||||
message={errors.name}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
tabIndex={2}
|
||||
autoComplete="email"
|
||||
name="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
required
|
||||
tabIndex={3}
|
||||
autoComplete="new-password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
passwordrules={passwordRules}
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password_confirmation">
|
||||
Confirm password
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
required
|
||||
tabIndex={4}
|
||||
autoComplete="new-password"
|
||||
name="password_confirmation"
|
||||
placeholder="Confirm password"
|
||||
passwordrules={passwordRules}
|
||||
/>
|
||||
<InputError
|
||||
message={errors.password_confirmation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="mt-2 w-full"
|
||||
tabIndex={5}
|
||||
data-test="register-user-button"
|
||||
>
|
||||
{processing && <Spinner />}
|
||||
Create account
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
Already have an account?{' '}
|
||||
<TextLink href={login()} tabIndex={6}>
|
||||
Log in
|
||||
</TextLink>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Register.layout = {
|
||||
title: 'Create an account',
|
||||
description: 'Enter your details below to create your account',
|
||||
};
|
||||
@ -43,14 +43,14 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Label htmlFor="password">Password baru</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
className="mt-1 block w-full"
|
||||
autoFocus
|
||||
placeholder="Password"
|
||||
placeholder="Password baru"
|
||||
passwordrules={passwordRules}
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
@ -58,14 +58,14 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password_confirmation">
|
||||
Confirm password
|
||||
Konfirmasi password
|
||||
</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
autoComplete="new-password"
|
||||
className="mt-1 block w-full"
|
||||
placeholder="Confirm password"
|
||||
placeholder="Konfirmasi password"
|
||||
passwordrules={passwordRules}
|
||||
/>
|
||||
<InputError
|
||||
@ -92,5 +92,5 @@ export default function ResetPassword({ token, email, passwordRules }: Props) {
|
||||
|
||||
ResetPassword.layout = {
|
||||
title: 'Reset password',
|
||||
description: 'Please enter your new password below',
|
||||
description: 'Masukkan password baru Anda di bawah ini',
|
||||
};
|
||||
|
||||
@ -23,18 +23,18 @@ export default function TwoFactorChallenge() {
|
||||
}>(() => {
|
||||
if (showRecoveryInput) {
|
||||
return {
|
||||
title: 'Recovery code',
|
||||
title: 'Kode pemulihan',
|
||||
description:
|
||||
'Please confirm access to your account by entering one of your emergency recovery codes.',
|
||||
toggleText: 'login using an authentication code',
|
||||
'Silakan konfirmasi akses ke akun Anda dengan memasukkan salah satu kode pemulihan darurat Anda.',
|
||||
toggleText: 'masuk menggunakan kode autentikasi',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Authentication code',
|
||||
title: 'Kode autentikasi',
|
||||
description:
|
||||
'Enter the authentication code provided by your authenticator application.',
|
||||
toggleText: 'login using a recovery code',
|
||||
'Masukkan kode autentikasi yang diberikan oleh aplikasi autentikator Anda.',
|
||||
toggleText: 'masuk menggunakan kode pemulihan',
|
||||
};
|
||||
}, [showRecoveryInput]);
|
||||
|
||||
@ -51,7 +51,7 @@ export default function TwoFactorChallenge() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head title="Two-factor authentication" />
|
||||
<Head title="Autentikasi dua faktor" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<Form
|
||||
@ -67,7 +67,7 @@ export default function TwoFactorChallenge() {
|
||||
<Input
|
||||
name="recovery_code"
|
||||
type="text"
|
||||
placeholder="Enter recovery code"
|
||||
placeholder="Masukkan kode pemulihan"
|
||||
autoFocus={showRecoveryInput}
|
||||
required
|
||||
/>
|
||||
@ -109,11 +109,11 @@ export default function TwoFactorChallenge() {
|
||||
className="w-full"
|
||||
disabled={processing}
|
||||
>
|
||||
Continue
|
||||
Lanjutkan
|
||||
</Button>
|
||||
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<span>or you can </span>
|
||||
<span>atau Anda bisa </span>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// Components
|
||||
import { Form, Head } from '@inertiajs/react';
|
||||
import TextLink from '@/components/text-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -9,12 +8,12 @@ import { send } from '@/routes/verification';
|
||||
export default function VerifyEmail({ status }: { status?: string }) {
|
||||
return (
|
||||
<>
|
||||
<Head title="Email verification" />
|
||||
<Head title="Verifikasi email" />
|
||||
|
||||
{status === 'verification-link-sent' && (
|
||||
<div className="mb-4 text-center text-sm font-medium text-green-600">
|
||||
A new verification link has been sent to the email address
|
||||
you provided during registration.
|
||||
Tautan verifikasi baru telah dikirim ke alamat email
|
||||
yang Anda berikan saat pendaftaran.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -23,14 +22,14 @@ export default function VerifyEmail({ status }: { status?: string }) {
|
||||
<>
|
||||
<Button disabled={processing} variant="secondary">
|
||||
{processing && <Spinner />}
|
||||
Resend verification email
|
||||
Kirim ulang email verifikasi
|
||||
</Button>
|
||||
|
||||
<TextLink
|
||||
href={logout()}
|
||||
className="mx-auto block text-sm"
|
||||
>
|
||||
Log out
|
||||
Keluar
|
||||
</TextLink>
|
||||
</>
|
||||
)}
|
||||
@ -40,7 +39,7 @@ export default function VerifyEmail({ status }: { status?: string }) {
|
||||
}
|
||||
|
||||
VerifyEmail.layout = {
|
||||
title: 'Email verification',
|
||||
title: 'Verifikasi email',
|
||||
description:
|
||||
'Please verify your email address by clicking on the link we just emailed to you.',
|
||||
'Silakan verifikasi alamat email Anda dengan mengklik tautan yang baru saja kami kirimkan.',
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
import { dashboard, login } from '@/routes';
|
||||
import { register } from '@/routes';
|
||||
|
||||
export default function Welcome() {
|
||||
const { auth } = usePage().props;
|
||||
@ -19,20 +18,12 @@ export default function Welcome() {
|
||||
Dashboard
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={login()}
|
||||
className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href={register()}
|
||||
className="inline-block rounded-sm border border-[#19140035] px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#1915014a] dark:border-[#3E3E3A] dark:text-[#EDEDEC] dark:hover:border-[#62605b]"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</>
|
||||
<Link
|
||||
href={login()}
|
||||
className="inline-block rounded-sm border border-transparent px-5 py-1.5 text-sm leading-normal text-[#1b1b18] hover:border-[#19140035] dark:text-[#EDEDEC] dark:hover:border-[#3E3E3A]"
|
||||
>
|
||||
Masuk
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user