feat(auth): registration and login

This commit is contained in:
2026-02-21 08:29:53 +01:00
parent 795c7a835f
commit 4fb95c872b
16 changed files with 133 additions and 130 deletions

View File

@@ -15,6 +15,7 @@ class AvatarUrl
}
$avatarHash = $hash ?: self::resolveHash($userId);
if (!$avatarHash) {
return self::default();
}
@@ -30,7 +31,9 @@ class AvatarUrl
public static function default(): string
{
return asset('img/default-avatar.webp');
$base = rtrim((string) config('cdn.avatar_url', 'https://files.skinbase.org'), '/');
return sprintf('%s/avatars/default.webp', $base);
}
private static function resolveHash(int $userId): ?string

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

View File

@@ -1,21 +1,18 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Confirm Password</h1>
<p class="mt-2 text-sm text-sb-muted">Please confirm your password before continuing.</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Confirm Password</h2>
<p class="text-sm text-white/60 mb-6">Please confirm your password before continuing.</p>
<form method="POST" action="{{ route('password.confirm') }}" class="mt-4">
@csrf
<div>
<x-input-label for="password" :value="__('Password')" class="text-sb-muted" />
<x-text-input id="password" class="block mt-1 w-full bg-black/20 border-sb-line text-white"
type="password"
name="password"
required autocomplete="current-password" />
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<x-text-input id="password" name="password" type="password" required autocomplete="current-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>

View File

@@ -1,10 +1,11 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Reset Password</h1>
<p class="mt-2 text-sm text-sb-muted">Enter your email and we'll send a link to reset your password.</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Reset Password</h2>
<p class="text-sm text-white/60 mb-6">Enter your email and we'll send a link to reset your password.</p>
<x-auth-session-status class="mt-4 mb-2 text-green-300" :status="session('status')" />
@@ -12,8 +13,8 @@
@csrf
<div>
<x-input-label for="email" :value="__('Email')" class="text-sb-muted" />
<x-text-input id="email" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="email" name="email" :value="old('email')" required autofocus />
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<x-text-input id="email" name="email" type="email" :value="old('email')" required autofocus class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>

View File

@@ -1,49 +1,46 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Log in</h1>
<p class="mt-2 text-sm text-sb-muted">Sign in to continue to your Skinbase account.</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Login</h2>
<x-auth-session-status class="mt-4 mb-2 text-green-300" :status="session('status')" />
<p class="text-sm text-white/60 mb-6">Access your Skinbase account.</p>
<form method="POST" action="{{ route('login') }}" class="mt-4 space-y-4">
@csrf
<x-auth-session-status class="mt-4 mb-2 text-green-300" :status="session('status')" />
<div>
<x-input-label for="email" :value="__('Email')" class="text-sb-muted" />
<x-text-input id="email" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<form method="POST" action="{{ route('login') }}" class="space-y-5">
@csrf
<div>
<x-input-label for="password" :value="__('Password')" class="text-sb-muted" />
<x-text-input id="password" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="password" name="password" required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div>
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<input id="email" name="email" type="email" required placeholder="you@example.com" value="{{ old('email') }}" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="block mt-1">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-sb-line bg-black/20 text-sb-blue shadow-sm focus:ring-sb-blue" name="remember">
<span class="ms-2 text-sm text-sb-muted">{{ __('Remember me') }}</span>
</label>
</div>
<div>
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<input id="password" name="password" type="password" required placeholder="••••••••••" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex items-center justify-between pt-2">
@if (Route::has('password.request'))
<a class="underline text-sm text-sb-muted hover:text-white" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@else
<span></span>
@endif
<div class="flex items-center justify-between text-sm text-white/60">
<label class="flex items-center gap-2">
<input type="checkbox" name="remember" class="rounded bg-slate-800 border-white/20" />
Remember me
</label>
<x-primary-button class="justify-center">
{{ __('Log in') }}
</x-primary-button>
</div>
</form>
@if (Route::has('password.request'))
<a href="{{ route('password.request') }}" class="text-purple-400 hover:underline">Forgot password?</a>
@endif
</div>
<button type="submit" class="w-full rounded-lg py-3 font-medium bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-400 hover:to-pink-400 text-white transition">Sign In</button>
<p class="text-sm text-center text-white/60">Dont have an account? <a href="{{ route('register') }}" class="text-purple-400 hover:underline">Create one</a></p>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -2,9 +2,11 @@
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<div class="mb-4 text-sm text-sb-muted">
<p class="font-medium text-white">Check your inbox</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<div class="mb-4 text-sm text-white/80">
<p class="font-medium text-white">Check your inbox</p>
@if($email !== '')
<p class="mt-1">We sent a verification link to <strong class="text-white">{{ $email }}</strong>.</p>
<p class="mt-1">Click the link in that email to continue setup.</p>
@@ -83,4 +85,4 @@
}, 1000);
});
</script>
@endsection
@endsection

View File

@@ -1,41 +1,32 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Create Account</h1>
<p class="mt-2 text-sm text-sb-muted">Start with your email. You will set your password and username after verification.</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Create Account</h2>
<form method="POST" action="{{ route('register') }}" class="mt-6 space-y-4">
@csrf
<p class="text-sm text-white/60 mb-6">Start with your email. Youll choose a password and username after verification.</p>
<div style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;" aria-hidden="true">
<label for="website">Website</label>
<input id="website" type="text" name="website" tabindex="-1" autocomplete="off" />
</div>
<form method="POST" action="{{ route('register') }}" class="space-y-5">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" class="text-sb-muted" />
<x-text-input id="email" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="email" name="email" :value="old('email', $prefillEmail ?? '')" required autofocus autocomplete="email" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div>
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<input id="email" name="email" type="email" required placeholder="you@example.com" value="{{ old('email', $prefillEmail ?? '') }}" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
@if(config('services.recaptcha.enabled'))
<input type="hidden" name="g-recaptcha-response" value="{{ old('g-recaptcha-response') }}" />
<x-input-error :messages="$errors->get('captcha')" class="mt-2" />
@endif
@if(config('services.recaptcha.enabled'))
<input type="hidden" name="g-recaptcha-response" value="{{ old('g-recaptcha-response') }}" />
<x-input-error :messages="$errors->get('captcha')" class="mt-2" />
@endif
<div class="flex items-center justify-between pt-2">
<a class="underline text-sm text-sb-muted hover:text-white" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<button type="submit" class="w-full rounded-lg py-3 font-medium bg-gradient-to-r from-cyan-500 to-sky-400 hover:from-cyan-400 hover:to-sky-300 text-slate-900 transition">Continue</button>
<x-primary-button class="justify-center">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
<p class="text-sm text-center text-white/60">Already registered? <a href="{{ route('login') }}" class="text-cyan-400 hover:underline">Sign in</a></p>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -1,10 +1,11 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Reset Password</h1>
<p class="mt-2 text-sm text-sb-muted">Choose a new password for your account.</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Reset Password</h2>
<p class="text-sm text-white/60 mb-6">Choose a new password for your account.</p>
<form method="POST" action="{{ route('password.store') }}" class="mt-4">
@csrf
@@ -14,15 +15,15 @@
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" class="text-sb-muted" />
<x-text-input id="email" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<label class="block text-sm mb-1 text-white/80" for="email">Email</label>
<x-text-input id="email" name="email" type="email" :value="old('email', $request->email)" required autofocus autocomplete="username" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" class="text-sb-muted" />
<x-text-input id="password" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="password" name="password" required autocomplete="new-password" />
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<x-text-input id="password" name="password" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
@@ -30,9 +31,7 @@
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" class="text-sb-muted" />
<x-text-input id="password_confirmation" class="block mt-1 w-full bg-black/20 border-sb-line text-white"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-text-input id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>

View File

@@ -1,10 +1,11 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-2xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Set Your Password</h1>
<div class="mt-4 text-white/90">
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Set Your Password</h2>
<div class="mt-4 text-white/90">
@include('auth.partials.onboarding-progress', ['currentStep' => 'verified'])
@if (session('status'))
@@ -13,23 +14,21 @@
</div>
@endif
<p class="mb-4 text-sm text-sb-muted">
{{ __('Create a password for ') }}<strong>{{ $email }}</strong>
</p>
<p class="mb-4 text-sm text-white/60">{{ __('Create a password for ') }}<strong>{{ $email }}</strong></p>
<form method="POST" action="{{ route('setup.password.store') }}">
@csrf
<div>
<x-input-label for="password" :value="__('Password')" class="text-sb-muted" />
<x-text-input id="password" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="password" name="password" required autocomplete="new-password" />
<label class="block text-sm mb-1 text-white/80" for="password">Password</label>
<x-text-input id="password" name="password" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
<p class="mt-2 text-xs text-sb-muted">{{ __('Minimum 10 characters, include at least one number and one symbol.') }}</p>
</div>
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" class="text-sb-muted" />
<x-text-input id="password_confirmation" class="block mt-1 w-full bg-black/20 border-sb-line text-white" type="password" name="password_confirmation" required autocomplete="new-password" />
<label class="block text-sm mb-1 text-white/80" for="password_confirmation">Confirm Password</label>
<x-text-input id="password_confirmation" name="password_confirmation" type="password" required autocomplete="new-password" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
</div>
<div class="mt-6 flex justify-end">

View File

@@ -1,10 +1,11 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-2xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Choose Username</h1>
<div class="mt-4 text-white/90">
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Choose Username</h2>
<div class="mt-4 text-white/90">
@include('auth.partials.onboarding-progress', ['currentStep' => 'password'])
@if (session('status'))
@@ -23,19 +24,8 @@
@csrf
<div>
<x-input-label for="username" :value="__('Username')" class="text-sb-muted" />
<x-text-input
id="username"
class="block mt-1 w-full bg-black/20 border-sb-line text-white"
type="text"
name="username"
:value="old('username', $username)"
required
autocomplete="username"
data-username-field="true"
data-availability-url="{{ route('api.username.availability') }}"
data-availability-target="setup-username-availability"
/>
<label class="block text-sm mb-1 text-white/80" for="username">Username</label>
<x-text-input id="username" name="username" type="text" :value="old('username', $username)" required autocomplete="username" data-username-field="true" data-availability-url="{{ route('api.username.availability') }}" data-availability-target="setup-username-availability" class="w-full rounded-lg bg-slate-950/70 border border-white/10 px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-cyan-500 text-white" />
<p id="setup-username-availability" class="mt-1 text-xs text-sb-muted"></p>
<x-input-error :messages="$errors->get('username')" class="mt-2" />
</div>

View File

@@ -1,10 +1,11 @@
@extends('layouts.nova')
@section('content')
<div class="px-4 py-8 md:px-6 md:py-10">
<div class="mx-auto w-full max-w-xl rounded-2xl border border-sb-line bg-panel-dark shadow-sb p-6 md:p-8">
<h1 class="text-2xl font-semibold text-white">Verify Your Email</h1>
<p class="mt-2 text-sm text-sb-muted">Before getting started, please verify your email address by clicking the link we sent you.</p>
<div class="flex-1 flex items-center justify-center px-6 py-16 min-h-[calc(100vh-4rem)] box-border">
<div class="max-w-5xl w-full">
<div class="rounded-2xl border border-white/10 bg-slate-900/70 backdrop-blur shadow-xl p-8 auth-card">
<h2 class="text-2xl font-semibold mb-2 text-white">Verify Your Email</h2>
<p class="text-sm text-white/60 mb-6">Before getting started, please verify your email address by clicking the link we sent you.</p>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 rounded-md border border-green-700/60 bg-green-900/20 px-3 py-2 text-sm text-green-300">

View File

@@ -42,10 +42,30 @@
/* Card enter animation */
.nova-card-enter { opacity: 0; transform: translateY(10px) scale(0.995); }
.nova-card-enter.nova-card-enter-active { transition: transform 380ms cubic-bezier(.2,.9,.2,1), opacity 380ms ease-out; opacity: 1; transform: none; }
/* Auth card consistency */
.auth-card { max-width: 720px; margin-left: auto; margin-right: auto; }
.auth-card h1 { font-size: 1.25rem; line-height: 1.2; }
.auth-card p { color: rgba(203,213,225,0.9); }
</style>
@stack('head')
</head>
<body class="bg-nova-900 text-white min-h-screen flex flex-col">
@php
$authBgRoutes = [
'login', 'register', 'register.notice', 'password.request', 'password.reset',
'verification.notice', 'registration.verify', 'setup.password.create', 'setup.username.create', 'password.confirm'
];
$useAuthBackground = request()->route() && in_array(request()->route()->getName(), $authBgRoutes);
$authBackgrounds = [
'/gfx/skinbase_back_001.webp',
'/gfx/skinbase_back_002.webp',
'/gfx/skinbase_back_003.webp',
'/gfx/skinbase_back_004.webp',
];
$selectedAuthBg = $useAuthBackground ? $authBackgrounds[array_rand($authBackgrounds)] : null;
@endphp
<body class="bg-nova-900 text-white min-h-screen flex flex-col" @if($selectedAuthBg) style="background: url('{{ $selectedAuthBg }}') center/cover no-repeat; background-attachment: fixed;" @endif>
<!-- React Topbar mount point -->
<div id="topbar-root"></div>

View File

@@ -80,6 +80,9 @@ Route::middleware('auth')->group(function () {
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
Route::get('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout.get');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});