Compare commits
3 commits
b6290c0f8d
...
3e23dad5c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e23dad5c5 | |||
| 16ce3b6324 | |||
| 03fa4b803f |
10 changed files with 168 additions and 54 deletions
|
|
@ -44,7 +44,7 @@ class Onboarding extends Component
|
|||
public int $routePriority = 50;
|
||||
|
||||
// State
|
||||
public array $errors = [];
|
||||
public array $formErrors = [];
|
||||
public bool $isLoading = false;
|
||||
|
||||
protected LemmyAuthService $lemmyAuthService;
|
||||
|
|
@ -96,20 +96,20 @@ public function mount(): void
|
|||
public function goToStep(int $step): void
|
||||
{
|
||||
$this->step = $step;
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
}
|
||||
|
||||
public function nextStep(): void
|
||||
{
|
||||
$this->step++;
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
}
|
||||
|
||||
public function previousStep(): void
|
||||
{
|
||||
if ($this->step > 1) {
|
||||
$this->step--;
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ public function deleteAccount(): void
|
|||
|
||||
public function createPlatformAccount(): void
|
||||
{
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
$this->isLoading = true;
|
||||
|
||||
$this->validate([
|
||||
|
|
@ -183,13 +183,22 @@ public function createPlatformAccount(): void
|
|||
|
||||
$this->nextStep();
|
||||
} catch (\App\Exceptions\PlatformAuthException $e) {
|
||||
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
||||
$this->errors['general'] = $e->getMessage();
|
||||
$message = $e->getMessage();
|
||||
if (str_contains($message, 'Rate limited by')) {
|
||||
$this->formErrors['general'] = $message;
|
||||
} elseif (str_contains($message, 'Connection failed')) {
|
||||
$this->formErrors['general'] = 'Unable to connect to the Lemmy instance. Please check the URL and try again.';
|
||||
} else {
|
||||
$this->errors['general'] = 'Invalid username or password. Please check your credentials and try again.';
|
||||
$this->formErrors['general'] = 'Invalid username or password. Please check your credentials and try again.';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->errors['general'] = 'Unable to connect to the Lemmy instance. Please check the URL and try again.';
|
||||
logger()->error('Lemmy platform account creation failed', [
|
||||
'instance_url' => $fullInstanceUrl,
|
||||
'username' => $this->username,
|
||||
'error' => $e->getMessage(),
|
||||
'class' => get_class($e),
|
||||
]);
|
||||
$this->formErrors['general'] = 'An error occurred while setting up your account. Please try again.';
|
||||
} finally {
|
||||
$this->isLoading = false;
|
||||
}
|
||||
|
|
@ -197,7 +206,7 @@ public function createPlatformAccount(): void
|
|||
|
||||
public function createFeed(): void
|
||||
{
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
$this->isLoading = true;
|
||||
|
||||
$this->validate([
|
||||
|
|
@ -227,7 +236,7 @@ public function createFeed(): void
|
|||
|
||||
$this->nextStep();
|
||||
} catch (\Exception $e) {
|
||||
$this->errors['general'] = 'Failed to create feed. Please try again.';
|
||||
$this->formErrors['general'] = 'Failed to create feed. Please try again.';
|
||||
} finally {
|
||||
$this->isLoading = false;
|
||||
}
|
||||
|
|
@ -235,7 +244,7 @@ public function createFeed(): void
|
|||
|
||||
public function createChannel(): void
|
||||
{
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
$this->isLoading = true;
|
||||
|
||||
$this->validate([
|
||||
|
|
@ -254,7 +263,7 @@ public function createChannel(): void
|
|||
->get();
|
||||
|
||||
if ($activeAccounts->isEmpty()) {
|
||||
$this->errors['general'] = 'No active platform accounts found for this instance. Please create a platform account first.';
|
||||
$this->formErrors['general'] = 'No active platform accounts found for this instance. Please create a platform account first.';
|
||||
$this->isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -279,7 +288,7 @@ public function createChannel(): void
|
|||
|
||||
$this->nextStep();
|
||||
} catch (\Exception $e) {
|
||||
$this->errors['general'] = 'Failed to create channel. Please try again.';
|
||||
$this->formErrors['general'] = 'Failed to create channel. Please try again.';
|
||||
} finally {
|
||||
$this->isLoading = false;
|
||||
}
|
||||
|
|
@ -287,7 +296,7 @@ public function createChannel(): void
|
|||
|
||||
public function createRoute(): void
|
||||
{
|
||||
$this->errors = [];
|
||||
$this->formErrors = [];
|
||||
$this->isLoading = true;
|
||||
|
||||
$this->validate([
|
||||
|
|
@ -309,7 +318,7 @@ public function createRoute(): void
|
|||
|
||||
$this->nextStep();
|
||||
} catch (\Exception $e) {
|
||||
$this->errors['general'] = 'Failed to create route. Please try again.';
|
||||
$this->formErrors['general'] = 'Failed to create route. Please try again.';
|
||||
} finally {
|
||||
$this->isLoading = false;
|
||||
}
|
||||
|
|
@ -344,6 +353,6 @@ public function render()
|
|||
'feeds' => $feeds,
|
||||
'channels' => $channels,
|
||||
'feedProviders' => $feedProviders,
|
||||
])->layout('layouts.guest');
|
||||
])->layout('layouts.onboarding');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ public function login(string $username, string $password): ?string
|
|||
$data = $response->json();
|
||||
return $data['jwt'] ?? null;
|
||||
} catch (Exception $e) {
|
||||
// Re-throw rate limit exceptions immediately
|
||||
if (str_contains($e->getMessage(), 'Rate limited')) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
logger()->error('Lemmy login exception', ['error' => $e->getMessage(), 'scheme' => $scheme]);
|
||||
// If this was the first attempt and HTTPS, try HTTP next
|
||||
if ($idx === 0 && in_array('http', $schemesToTry, true)) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public function up(): void
|
|||
$table->enum('platform', ['lemmy']);
|
||||
$table->string('instance_url');
|
||||
$table->string('username');
|
||||
$table->string('password');
|
||||
$table->text('password');
|
||||
$table->json('settings')->nullable();
|
||||
$table->boolean('is_active')->default(false);
|
||||
$table->timestamp('last_tested_at')->nullable();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
<x-guest-layout>
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Welcome back</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">Sign in to your account</p>
|
||||
</div>
|
||||
|
||||
<!-- Session Status -->
|
||||
<x-auth-session-status class="mb-4" :status="session('status')" />
|
||||
|
||||
|
|
@ -27,21 +32,24 @@
|
|||
<!-- Remember Me -->
|
||||
<div class="block mt-4">
|
||||
<label for="remember_me" class="inline-flex items-center">
|
||||
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
|
||||
<input id="remember_me" type="checkbox" class="rounded border-gray-300 text-blue-600 shadow-sm focus:ring-blue-500" name="remember">
|
||||
<span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
@if (Route::has('password.request'))
|
||||
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('password.request') }}">
|
||||
{{ __('Forgot your password?') }}
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<x-primary-button class="ms-3">
|
||||
{{ __('Log in') }}
|
||||
<div class="mt-6">
|
||||
<x-primary-button class="w-full justify-center py-3">
|
||||
{{ __('Sign in') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
|
||||
@if (Route::has('register'))
|
||||
<div class="mt-6 text-center">
|
||||
<span class="text-sm text-gray-500">Don't have an account?</span>
|
||||
<a class="text-sm text-blue-600 hover:text-blue-800 font-medium ml-1" href="{{ route('register') }}">
|
||||
{{ __('Sign up') }}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
<x-guest-layout>
|
||||
<div class="mb-6 text-center">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Create an account</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">Get started with FFR</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
|
|
@ -19,34 +24,33 @@
|
|||
<!-- Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password" :value="__('Password')" />
|
||||
|
||||
<x-text-input id="password" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password"
|
||||
required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<div class="mt-4">
|
||||
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
|
||||
|
||||
<x-text-input id="password_confirmation" class="block mt-1 w-full"
|
||||
type="password"
|
||||
name="password_confirmation" required autocomplete="new-password" />
|
||||
|
||||
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
|
||||
{{ __('Already registered?') }}
|
||||
</a>
|
||||
|
||||
<x-primary-button class="ms-4">
|
||||
{{ __('Register') }}
|
||||
<div class="mt-6">
|
||||
<x-primary-button class="w-full justify-center py-3">
|
||||
{{ __('Create account') }}
|
||||
</x-primary-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<span class="text-sm text-gray-500">Already have an account?</span>
|
||||
<a class="text-sm text-blue-600 hover:text-blue-800 font-medium ml-1" href="{{ route('login') }}">
|
||||
{{ __('Sign in') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</x-guest-layout>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-lg font-semibold text-sm text-white hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
|
||||
{{ $slot }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
@props(['disabled' => false])
|
||||
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>
|
||||
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-blue-500 focus:ring-blue-500 rounded-lg shadow-sm px-4 py-3']) }}>
|
||||
|
|
|
|||
|
|
@ -5,18 +5,41 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'Laravel') }}</title>
|
||||
<title>{{ config('app.name', 'FFR') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 antialiased">
|
||||
{{ $slot }}
|
||||
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800">
|
||||
<!-- Logo/Brand -->
|
||||
<div class="mb-6">
|
||||
<a href="/" class="flex flex-col items-center">
|
||||
<div class="w-16 h-16 bg-white rounded-2xl shadow-lg flex items-center justify-center mb-3">
|
||||
<svg class="w-10 h-10 text-blue-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-.75a7.5 7.5 0 0 0-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-white">FFR</span>
|
||||
<span class="text-sm text-blue-200">Feed to Fediverse Router</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Card -->
|
||||
<div class="w-full sm:max-w-md px-6 py-8 bg-white shadow-xl rounded-2xl mx-4">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-8 text-center text-sm text-blue-200">
|
||||
<p>Route your feeds to the Fediverse</p>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
65
resources/views/layouts/onboarding.blade.php
Normal file
65
resources/views/layouts/onboarding.blade.php
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
<title>{{ config('app.name', 'FFR') }} - Setup</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
</head>
|
||||
<body class="font-sans text-gray-900 antialiased">
|
||||
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-12 bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 relative">
|
||||
<!-- User Menu -->
|
||||
@auth
|
||||
<div class="absolute top-4 right-4">
|
||||
<div x-data="{ open: false }" class="relative">
|
||||
<button @click="open = !open" class="flex items-center space-x-2 text-white/80 hover:text-white transition">
|
||||
<span class="text-sm">{{ Auth::user()->name }}</span>
|
||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="open" @click.away="open = false" x-transition class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg py-1 z-50">
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
Log out
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
<!-- Logo/Brand -->
|
||||
<div class="mb-6">
|
||||
<a href="/" class="flex flex-col items-center">
|
||||
<div class="w-16 h-16 bg-white rounded-2xl shadow-lg flex items-center justify-center mb-3">
|
||||
<svg class="w-10 h-10 text-blue-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12.75 19.5v-.75a7.5 7.5 0 0 0-7.5-7.5H4.5m0-6.75h.75c7.87 0 14.25 6.38 14.25 14.25v.75M6 18.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-white">FFR</span>
|
||||
<span class="text-sm text-blue-200">Feed to Fediverse Router</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
{{ $slot }}
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-8 mb-8 text-center text-sm text-blue-200">
|
||||
<p>Route your feeds to the Fediverse</p>
|
||||
</div>
|
||||
</div>
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-lg w-full bg-white rounded-lg shadow-md p-8">
|
||||
<div class="w-full max-w-2xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="w-full bg-white rounded-2xl shadow-xl p-8">
|
||||
|
||||
{{-- Step 1: Welcome --}}
|
||||
@if ($step === 1)
|
||||
|
|
@ -59,9 +59,9 @@ class="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 tran
|
|||
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">4</div>
|
||||
</div>
|
||||
|
||||
@if (!empty($errors['general']))
|
||||
@if (!empty($formErrors['general']))
|
||||
<div class="p-3 bg-red-50 border border-red-200 rounded-md mt-6">
|
||||
<p class="text-red-600 text-sm">{{ $errors['general'] }}</p>
|
||||
<p class="text-red-600 text-sm">{{ $formErrors['general'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
@ -190,9 +190,9 @@ class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition
|
|||
</div>
|
||||
|
||||
<form wire:submit="createFeed" class="space-y-6 mt-8 text-left">
|
||||
@if (!empty($errors['general']))
|
||||
@if (!empty($formErrors['general']))
|
||||
<div class="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p class="text-red-600 text-sm">{{ $errors['general'] }}</p>
|
||||
<p class="text-red-600 text-sm">{{ $formErrors['general'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
@ -294,9 +294,9 @@ class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition
|
|||
</div>
|
||||
|
||||
<form wire:submit="createChannel" class="space-y-6 mt-8 text-left">
|
||||
@if (!empty($errors['general']))
|
||||
@if (!empty($formErrors['general']))
|
||||
<div class="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p class="text-red-600 text-sm">{{ $errors['general'] }}</p>
|
||||
<p class="text-red-600 text-sm">{{ $formErrors['general'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
@ -400,9 +400,9 @@ class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition
|
|||
</div>
|
||||
|
||||
<form wire:submit="createRoute" class="space-y-6 mt-8 text-left">
|
||||
@if (!empty($errors['general']))
|
||||
@if (!empty($formErrors['general']))
|
||||
<div class="p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<p class="text-red-600 text-sm">{{ $errors['general'] }}</p>
|
||||
<p class="text-red-600 text-sm">{{ $formErrors['general'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue