Fix nginx error, startup script, add skip
This commit is contained in:
parent
73ba089e46
commit
387920e82b
14 changed files with 600 additions and 71 deletions
|
|
@ -11,6 +11,7 @@
|
|||
use App\Models\PlatformAccount;
|
||||
use App\Models\PlatformChannel;
|
||||
use App\Models\PlatformInstance;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Auth\LemmyAuthService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -32,7 +33,11 @@ public function status(): JsonResponse
|
|||
$hasFeed = Feed::where('is_active', true)->exists();
|
||||
$hasChannel = PlatformChannel::where('is_active', true)->exists();
|
||||
|
||||
$needsOnboarding = !$hasPlatformAccount || !$hasFeed || !$hasChannel;
|
||||
// Check if onboarding was explicitly skipped
|
||||
$onboardingSkipped = Setting::where('key', 'onboarding_skipped')->value('value') === 'true';
|
||||
|
||||
// User needs onboarding if they don't have the required components AND haven't skipped it
|
||||
$needsOnboarding = (!$hasPlatformAccount || !$hasFeed || !$hasChannel) && !$onboardingSkipped;
|
||||
|
||||
// Determine current step
|
||||
$currentStep = null;
|
||||
|
|
@ -52,6 +57,7 @@ public function status(): JsonResponse
|
|||
'has_platform_account' => $hasPlatformAccount,
|
||||
'has_feed' => $hasFeed,
|
||||
'has_channel' => $hasChannel,
|
||||
'onboarding_skipped' => $onboardingSkipped,
|
||||
], 'Onboarding status retrieved successfully.');
|
||||
}
|
||||
|
||||
|
|
@ -80,10 +86,12 @@ public function options(): JsonResponse
|
|||
public function createPlatform(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'instance_url' => 'required|url|max:255',
|
||||
'instance_url' => 'required|string|max:255|regex:/^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$/',
|
||||
'username' => 'required|string|max:255',
|
||||
'password' => 'required|string|min:6',
|
||||
'platform' => 'required|in:lemmy',
|
||||
], [
|
||||
'instance_url.regex' => 'Please enter a valid domain name (e.g., lemmy.world, belgae.social)'
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
|
|
@ -92,19 +100,23 @@ public function createPlatform(Request $request): JsonResponse
|
|||
|
||||
$validated = $validator->validated();
|
||||
|
||||
// Normalize the instance URL - prepend https:// if needed
|
||||
$instanceDomain = $validated['instance_url'];
|
||||
$fullInstanceUrl = 'https://' . $instanceDomain;
|
||||
|
||||
try {
|
||||
// Create or get platform instance
|
||||
$platformInstance = PlatformInstance::firstOrCreate([
|
||||
'url' => $validated['instance_url'],
|
||||
'url' => $fullInstanceUrl,
|
||||
'platform' => $validated['platform'],
|
||||
], [
|
||||
'name' => parse_url($validated['instance_url'], PHP_URL_HOST) ?? 'Lemmy Instance',
|
||||
'name' => ucfirst($instanceDomain),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// Authenticate with Lemmy API
|
||||
// Authenticate with Lemmy API using the full URL
|
||||
$authResponse = $this->lemmyAuthService->authenticate(
|
||||
$validated['instance_url'],
|
||||
$fullInstanceUrl,
|
||||
$validated['username'],
|
||||
$validated['password']
|
||||
);
|
||||
|
|
@ -112,7 +124,7 @@ public function createPlatform(Request $request): JsonResponse
|
|||
// Create platform account with the current schema
|
||||
$platformAccount = PlatformAccount::create([
|
||||
'platform' => $validated['platform'],
|
||||
'instance_url' => $validated['instance_url'],
|
||||
'instance_url' => $fullInstanceUrl,
|
||||
'username' => $validated['username'],
|
||||
'password' => $validated['password'],
|
||||
'api_token' => $authResponse['jwt'] ?? null,
|
||||
|
|
@ -232,4 +244,33 @@ public function complete(): JsonResponse
|
|||
'Onboarding completed successfully.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip onboarding - user can access the app without completing setup
|
||||
*/
|
||||
public function skip(): JsonResponse
|
||||
{
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'onboarding_skipped'],
|
||||
['value' => 'true']
|
||||
);
|
||||
|
||||
return $this->sendResponse(
|
||||
['skipped' => true],
|
||||
'Onboarding skipped successfully.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset onboarding skip status - force user back to onboarding
|
||||
*/
|
||||
public function resetSkip(): JsonResponse
|
||||
{
|
||||
Setting::where('key', 'onboarding_skipped')->delete();
|
||||
|
||||
return $this->sendResponse(
|
||||
['reset' => true],
|
||||
'Onboarding skip status reset successfully.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,25 @@ class LemmyRequest
|
|||
|
||||
public function __construct(string $instance, ?string $token = null)
|
||||
{
|
||||
$this->instance = $instance;
|
||||
// Handle both full URLs and just domain names
|
||||
$this->instance = $this->normalizeInstance($instance);
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize instance URL to just the domain name
|
||||
*/
|
||||
private function normalizeInstance(string $instance): string
|
||||
{
|
||||
// Remove protocol if present
|
||||
$instance = preg_replace('/^https?:\/\//', '', $instance);
|
||||
|
||||
// Remove trailing slash if present
|
||||
$instance = rtrim($instance, '/');
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@
|
|||
Route::post('/onboarding/feed', [OnboardingController::class, 'createFeed'])->name('api.onboarding.feed');
|
||||
Route::post('/onboarding/channel', [OnboardingController::class, 'createChannel'])->name('api.onboarding.channel');
|
||||
Route::post('/onboarding/complete', [OnboardingController::class, 'complete'])->name('api.onboarding.complete');
|
||||
Route::post('/onboarding/skip', [OnboardingController::class, 'skip'])->name('api.onboarding.skip');
|
||||
Route::post('/onboarding/reset-skip', [OnboardingController::class, 'resetSkip'])->name('api.onboarding.reset-skip');
|
||||
|
||||
// Dashboard stats
|
||||
Route::get('/dashboard/stats', [DashboardController::class, 'stats'])->name('api.dashboard.stats');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Models\Feed;
|
||||
use App\Models\Language;
|
||||
use App\Models\PlatformAccount;
|
||||
use App\Models\PlatformChannel;
|
||||
use App\Models\PlatformInstance;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Auth\LemmyAuthService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OnboardingControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Create a language for testing
|
||||
Language::factory()->create([
|
||||
'id' => 1,
|
||||
'short_code' => 'en',
|
||||
'name' => 'English',
|
||||
'native_name' => 'English',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_status_shows_needs_onboarding_when_no_components_exist()
|
||||
{
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'needs_onboarding' => true,
|
||||
'current_step' => 'platform',
|
||||
'has_platform_account' => false,
|
||||
'has_feed' => false,
|
||||
'has_channel' => false,
|
||||
'onboarding_skipped' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_status_shows_feed_step_when_platform_account_exists()
|
||||
{
|
||||
PlatformAccount::factory()->create(['is_active' => true]);
|
||||
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'needs_onboarding' => true,
|
||||
'current_step' => 'feed',
|
||||
'has_platform_account' => true,
|
||||
'has_feed' => false,
|
||||
'has_channel' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_status_shows_channel_step_when_platform_account_and_feed_exist()
|
||||
{
|
||||
PlatformAccount::factory()->create(['is_active' => true]);
|
||||
Feed::factory()->create(['is_active' => true]);
|
||||
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'needs_onboarding' => true,
|
||||
'current_step' => 'channel',
|
||||
'has_platform_account' => true,
|
||||
'has_feed' => true,
|
||||
'has_channel' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_status_shows_no_onboarding_needed_when_all_components_exist()
|
||||
{
|
||||
PlatformAccount::factory()->create(['is_active' => true]);
|
||||
Feed::factory()->create(['is_active' => true]);
|
||||
PlatformChannel::factory()->create(['is_active' => true]);
|
||||
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'needs_onboarding' => false,
|
||||
'current_step' => null,
|
||||
'has_platform_account' => true,
|
||||
'has_feed' => true,
|
||||
'has_channel' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_status_shows_no_onboarding_needed_when_skipped()
|
||||
{
|
||||
// No components exist but onboarding is skipped
|
||||
Setting::create([
|
||||
'key' => 'onboarding_skipped',
|
||||
'value' => 'true',
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'needs_onboarding' => false,
|
||||
'current_step' => null,
|
||||
'has_platform_account' => false,
|
||||
'has_feed' => false,
|
||||
'has_channel' => false,
|
||||
'onboarding_skipped' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_options_returns_languages_and_platform_instances()
|
||||
{
|
||||
PlatformInstance::factory()->create([
|
||||
'platform' => 'lemmy',
|
||||
'url' => 'https://lemmy.world',
|
||||
'name' => 'Lemmy World',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->getJson('/api/v1/onboarding/options');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'data' => [
|
||||
'languages' => [
|
||||
'*' => ['id', 'short_code', 'name', 'native_name', 'is_active']
|
||||
],
|
||||
'platform_instances' => [
|
||||
'*' => ['id', 'platform', 'url', 'name', 'description', 'is_active']
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_feed_validates_required_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'url', 'type', 'language_id']);
|
||||
}
|
||||
|
||||
public function test_create_feed_creates_feed_successfully()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/rss',
|
||||
'type' => 'rss',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/rss',
|
||||
'type' => 'rss',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'Test Feed',
|
||||
'url' => 'https://example.com/rss',
|
||||
'type' => 'rss',
|
||||
'language_id' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_channel_validates_required_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/channel', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'platform_instance_id', 'language_id']);
|
||||
}
|
||||
|
||||
public function test_create_channel_creates_channel_successfully()
|
||||
{
|
||||
$platformInstance = PlatformInstance::factory()->create();
|
||||
|
||||
$channelData = [
|
||||
'name' => 'test_community',
|
||||
'platform_instance_id' => $platformInstance->id,
|
||||
'language_id' => 1,
|
||||
'description' => 'Test community description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/channel', $channelData);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => 'test_community',
|
||||
'display_name' => 'Test_community',
|
||||
'channel_id' => 'test_community',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('platform_channels', [
|
||||
'name' => 'test_community',
|
||||
'channel_id' => 'test_community',
|
||||
'platform_instance_id' => $platformInstance->id,
|
||||
'language_id' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_complete_onboarding_returns_success()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/complete');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => ['completed' => true]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_skip_onboarding_creates_setting()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/skip');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => ['skipped' => true]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'key' => 'onboarding_skipped',
|
||||
'value' => 'true',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_skip_onboarding_updates_existing_setting()
|
||||
{
|
||||
// Create existing setting with false value
|
||||
Setting::create([
|
||||
'key' => 'onboarding_skipped',
|
||||
'value' => 'false',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/skip');
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'key' => 'onboarding_skipped',
|
||||
'value' => 'true',
|
||||
]);
|
||||
|
||||
// Ensure only one setting exists
|
||||
$this->assertEquals(1, Setting::where('key', 'onboarding_skipped')->count());
|
||||
}
|
||||
|
||||
public function test_reset_skip_removes_setting()
|
||||
{
|
||||
// Create skipped setting
|
||||
Setting::create([
|
||||
'key' => 'onboarding_skipped',
|
||||
'value' => 'true',
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/reset-skip');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => ['reset' => true]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('settings', [
|
||||
'key' => 'onboarding_skipped',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_reset_skip_works_when_no_setting_exists()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/reset-skip');
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => ['reset' => true]
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_platform_validates_instance_url_format()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/platform', [
|
||||
'instance_url' => 'invalid.domain.with.spaces and symbols!',
|
||||
'username' => 'testuser',
|
||||
'password' => 'password123',
|
||||
'platform' => 'lemmy',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['instance_url']);
|
||||
}
|
||||
|
||||
public function test_create_platform_validates_required_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/platform', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['instance_url', 'username', 'password', 'platform']);
|
||||
}
|
||||
|
||||
public function test_onboarding_flow_integration()
|
||||
{
|
||||
// 1. Initial status - needs onboarding
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
$response->assertJson(['data' => ['needs_onboarding' => true, 'current_step' => 'platform']]);
|
||||
|
||||
// 2. Skip onboarding
|
||||
$response = $this->postJson('/api/v1/onboarding/skip');
|
||||
$response->assertJson(['data' => ['skipped' => true]]);
|
||||
|
||||
// 3. Status after skip - no longer needs onboarding
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
$response->assertJson(['data' => ['needs_onboarding' => false, 'onboarding_skipped' => true]]);
|
||||
|
||||
// 4. Reset skip
|
||||
$response = $this->postJson('/api/v1/onboarding/reset-skip');
|
||||
$response->assertJson(['data' => ['reset' => true]]);
|
||||
|
||||
// 5. Status after reset - needs onboarding again
|
||||
$response = $this->getJson('/api/v1/onboarding/status');
|
||||
$response->assertJson(['data' => ['needs_onboarding' => true, 'onboarding_skipped' => false]]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
APP_NAME="FFR Development"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:YOUR_APP_KEY_HERE
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://localhost:8000
|
||||
|
|
|
|||
|
|
@ -3,19 +3,17 @@
|
|||
# Copy development environment configuration to backend
|
||||
cp /var/www/html/docker/dev/podman/.env.dev /var/www/html/backend/.env
|
||||
|
||||
# Setup nginx configuration
|
||||
cp /var/www/html/docker/nginx.conf /etc/nginx/sites-available/default
|
||||
# Setup nginx configuration for development
|
||||
cp /var/www/html/docker/dev/podman/nginx.conf /etc/nginx/sites-available/default
|
||||
|
||||
# Install/update dependencies
|
||||
echo "Installing PHP dependencies..."
|
||||
cd /var/www/html/backend
|
||||
composer install --no-interaction
|
||||
|
||||
# Generate app key if not set or empty
|
||||
if grep -q "APP_KEY=base64:YOUR_APP_KEY_HERE" /var/www/html/backend/.env || ! grep -q "APP_KEY=base64:" /var/www/html/backend/.env; then
|
||||
echo "Generating application key..."
|
||||
php artisan key:generate --force
|
||||
fi
|
||||
# Generate application key
|
||||
echo "Generating application key..."
|
||||
php artisan key:generate --force
|
||||
|
||||
# Wait for database to be ready
|
||||
echo "Waiting for database..."
|
||||
|
|
@ -39,6 +37,10 @@ fi
|
|||
# Start services
|
||||
echo "Starting services..."
|
||||
|
||||
# Start React dev server
|
||||
cd /var/www/html/frontend
|
||||
npm run dev -- --host 0.0.0.0 --port 5173 &
|
||||
|
||||
# Start Laravel backend
|
||||
cd /var/www/html/backend
|
||||
php artisan serve --host=127.0.0.1 --port=8000 &
|
||||
|
|
|
|||
87
docker/dev/podman/nginx.conf
Normal file
87
docker/dev/podman/nginx.conf
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Proxy API requests to Laravel backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# Serve Laravel public assets (images, etc.)
|
||||
location /images/ {
|
||||
alias /var/www/html/backend/public/images/;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Proxy Vite dev server assets
|
||||
location /@vite/ {
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# Proxy Vite HMR WebSocket
|
||||
location /@vite/client {
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# Proxy node_modules for Vite deps
|
||||
location /node_modules/ {
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# Proxy /src/ for Vite source files
|
||||
location /src/ {
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# Proxy React dev server for development (catch-all)
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:5173;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support for HMR
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_redirect off;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
}
|
||||
|
|
@ -10,14 +10,12 @@ echo "🚀 Starting FFR development environment with Podman..."
|
|||
if [ ! -f .env ]; then
|
||||
echo "📋 Creating .env file from .env.example..."
|
||||
cp .env.example .env
|
||||
echo "⚠️ Please update your .env file with appropriate values, especially APP_KEY"
|
||||
fi
|
||||
|
||||
# Check if podman-compose is available
|
||||
if ! command -v podman-compose &> /dev/null; then
|
||||
echo "❌ podman-compose not found. Installing..."
|
||||
pip3 install --user podman-compose
|
||||
echo "✅ podman-compose installed"
|
||||
echo "❌ podman-compose not found."
|
||||
exit
|
||||
fi
|
||||
|
||||
# Start services
|
||||
|
|
@ -28,22 +26,32 @@ podman-compose -f docker/dev/podman/docker-compose.yml up -d
|
|||
echo "⏳ Waiting for database to be ready..."
|
||||
sleep 10
|
||||
|
||||
# Check if APP_KEY is set
|
||||
if grep -q "APP_KEY=base64:YOUR_APP_KEY_HERE" .env || grep -q "APP_KEY=$" .env; then
|
||||
echo "🔑 Generating application key..."
|
||||
podman exec ffr-dev-app php artisan key:generate
|
||||
fi
|
||||
# Install/update dependencies if needed
|
||||
echo "📦 Installing dependencies..."
|
||||
podman exec ffr-dev-app bash -c "cd /var/www/html/backend && composer install"
|
||||
podman exec ffr-dev-app bash -c "cd /var/www/html/frontend && npm install"
|
||||
|
||||
# Run migrations and seeders
|
||||
echo "🗃️ Running database migrations..."
|
||||
podman exec ffr-dev-app php artisan migrate --force
|
||||
podman exec ffr-dev-app bash -c "cd /var/www/html/backend && php artisan migrate --force"
|
||||
echo "🌱 Running database seeders..."
|
||||
podman exec ffr-dev-app php artisan db:seed --force
|
||||
podman exec ffr-dev-app bash -c "cd /var/www/html/backend && php artisan db:seed --force"
|
||||
|
||||
# Install/update dependencies if needed
|
||||
echo "📦 Installing dependencies..."
|
||||
podman exec ffr-dev-app composer install
|
||||
podman exec ffr-dev-app npm install
|
||||
# Wait for container services to be fully ready
|
||||
echo "⏳ Waiting for container services to initialize..."
|
||||
sleep 5
|
||||
|
||||
# Start React dev server if not already running
|
||||
echo "🚀 Starting React dev server..."
|
||||
podman exec -d ffr-dev-app bash -c "cd /var/www/html/frontend && npm run dev -- --host 0.0.0.0 --port 5173 > /dev/null 2>&1 &"
|
||||
sleep 5
|
||||
|
||||
# Verify Vite is running
|
||||
if podman exec ffr-dev-app bash -c "curl -s http://localhost:5173 > /dev/null 2>&1"; then
|
||||
echo "✅ Vite dev server is running"
|
||||
else
|
||||
echo "⚠️ Vite dev server may not have started properly"
|
||||
fi
|
||||
|
||||
echo "✅ Development environment is ready!"
|
||||
echo "🌐 Application: http://localhost:8000"
|
||||
|
|
|
|||
|
|
@ -6,22 +6,8 @@ import Articles from './pages/Articles';
|
|||
import Feeds from './pages/Feeds';
|
||||
import Settings from './pages/Settings';
|
||||
import OnboardingWizard from './pages/onboarding/OnboardingWizard';
|
||||
import { OnboardingProvider, useOnboarding } from './contexts/OnboardingContext';
|
||||
|
||||
const AppContent: React.FC = () => {
|
||||
const { isLoading } = useOnboarding();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<Routes>
|
||||
{/* Onboarding routes - outside of main layout */}
|
||||
|
|
@ -44,12 +30,4 @@ const AppContent: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<OnboardingProvider>
|
||||
<AppContent />
|
||||
</OnboardingProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||
<h1 className="text-lg font-medium text-gray-900">FFR</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -28,22 +28,17 @@ export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({ children
|
|||
const needsOnboarding = onboardingStatus?.needs_onboarding ?? false;
|
||||
const isOnOnboardingPage = location.pathname.startsWith('/onboarding');
|
||||
|
||||
// Redirect logic
|
||||
// Redirect logic - only redirect if user explicitly navigates to a protected route
|
||||
React.useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
if (needsOnboarding && !isOnOnboardingPage) {
|
||||
// User needs onboarding but is not on onboarding pages
|
||||
const targetStep = onboardingStatus?.current_step;
|
||||
if (targetStep) {
|
||||
navigate(`/onboarding/${targetStep}`, { replace: true });
|
||||
} else {
|
||||
navigate('/onboarding', { replace: true });
|
||||
}
|
||||
} else if (!needsOnboarding && isOnOnboardingPage) {
|
||||
// User doesn't need onboarding but is on onboarding pages
|
||||
// Only redirect if user doesn't need onboarding but is on onboarding pages
|
||||
if (!needsOnboarding && isOnOnboardingPage) {
|
||||
navigate('/dashboard', { replace: true });
|
||||
}
|
||||
|
||||
// Don't auto-redirect to onboarding - let user navigate manually or via links
|
||||
// This prevents the app from being "stuck" in onboarding mode
|
||||
}, [onboardingStatus, isLoading, needsOnboarding, isOnOnboardingPage, navigate]);
|
||||
|
||||
const value: OnboardingContextValue = {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export interface OnboardingStatus {
|
|||
has_platform_account: boolean;
|
||||
has_feed: boolean;
|
||||
has_channel: boolean;
|
||||
onboarding_skipped: boolean;
|
||||
}
|
||||
|
||||
export interface OnboardingOptions {
|
||||
|
|
@ -288,6 +289,14 @@ class ApiClient {
|
|||
async completeOnboarding(): Promise<void> {
|
||||
await axios.post('/onboarding/complete');
|
||||
}
|
||||
|
||||
async skipOnboarding(): Promise<void> {
|
||||
await axios.post('/onboarding/skip');
|
||||
}
|
||||
|
||||
async resetOnboardingSkip(): Promise<void> {
|
||||
await axios.post('/onboarding/reset-skip');
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
|
|
@ -65,17 +65,18 @@ const PlatformStep: React.FC = () => {
|
|||
|
||||
<div>
|
||||
<label htmlFor="instance_url" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Lemmy Instance URL
|
||||
Lemmy Instance Domain
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
type="text"
|
||||
id="instance_url"
|
||||
value={formData.instance_url}
|
||||
onChange={(e) => handleChange('instance_url', e.target.value)}
|
||||
placeholder="https://lemmy.world"
|
||||
placeholder="lemmy.world"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-1">Enter just the domain name (e.g., lemmy.world, belgae.social)</p>
|
||||
{errors.instance_url && (
|
||||
<p className="text-red-600 text-sm mt-1">{errors.instance_url[0]}</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { apiClient } from '../../../lib/api';
|
||||
|
||||
const WelcomeStep: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const skipMutation = useMutation({
|
||||
mutationFn: () => apiClient.skipOnboarding(),
|
||||
onSuccess: () => {
|
||||
navigate('/dashboard');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSkip = () => {
|
||||
if (confirm('Are you sure you want to skip the setup? You can configure FFR later from the settings page.')) {
|
||||
skipMutation.mutate();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome to FFR</h1>
|
||||
|
|
@ -28,13 +45,21 @@ const WelcomeStep: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<div className="mt-8 space-y-3">
|
||||
<Link
|
||||
to="/onboarding/platform"
|
||||
className="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 transition duration-200 inline-block"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
disabled={skipMutation.isPending}
|
||||
className="w-full text-gray-500 hover:text-gray-700 py-2 px-4 text-sm transition duration-200 disabled:opacity-50"
|
||||
>
|
||||
{skipMutation.isPending ? 'Skipping...' : 'Skip for now'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue