374 lines
13 KiB
PHP
374 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1;
|
|
|
|
use App\Http\Requests\StoreFeedRequest;
|
|
use App\Http\Resources\FeedResource;
|
|
use App\Http\Resources\PlatformAccountResource;
|
|
use App\Http\Resources\PlatformChannelResource;
|
|
use App\Http\Resources\RouteResource;
|
|
use App\Models\Feed;
|
|
use App\Models\Language;
|
|
use App\Models\PlatformAccount;
|
|
use App\Models\PlatformChannel;
|
|
use App\Models\PlatformInstance;
|
|
use App\Models\Route;
|
|
use App\Models\Setting;
|
|
use App\Services\Auth\LemmyAuthService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class OnboardingController extends BaseController
|
|
{
|
|
public function __construct(
|
|
private readonly LemmyAuthService $lemmyAuthService
|
|
) {}
|
|
|
|
/**
|
|
* Get onboarding status - whether user needs onboarding
|
|
*/
|
|
public function status(): JsonResponse
|
|
{
|
|
$hasPlatformAccount = PlatformAccount::where('is_active', true)->exists();
|
|
$hasFeed = Feed::where('is_active', true)->exists();
|
|
$hasChannel = PlatformChannel::where('is_active', true)->exists();
|
|
$hasRoute = Route::where('is_active', true)->exists();
|
|
|
|
// 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 || !$hasRoute) && !$onboardingSkipped;
|
|
|
|
// Determine current step
|
|
$currentStep = null;
|
|
if ($needsOnboarding) {
|
|
if (!$hasPlatformAccount) {
|
|
$currentStep = 'platform';
|
|
} elseif (!$hasFeed) {
|
|
$currentStep = 'feed';
|
|
} elseif (!$hasChannel) {
|
|
$currentStep = 'channel';
|
|
} elseif (!$hasRoute) {
|
|
$currentStep = 'route';
|
|
}
|
|
}
|
|
|
|
return $this->sendResponse([
|
|
'needs_onboarding' => $needsOnboarding,
|
|
'current_step' => $currentStep,
|
|
'has_platform_account' => $hasPlatformAccount,
|
|
'has_feed' => $hasFeed,
|
|
'has_channel' => $hasChannel,
|
|
'has_route' => $hasRoute,
|
|
'onboarding_skipped' => $onboardingSkipped,
|
|
], 'Onboarding status retrieved successfully.');
|
|
}
|
|
|
|
/**
|
|
* Get onboarding options (languages, platform instances)
|
|
*/
|
|
public function options(): JsonResponse
|
|
{
|
|
$languages = Language::where('is_active', true)
|
|
->orderBy('name')
|
|
->get(['id', 'short_code', 'name', 'native_name', 'is_active']);
|
|
|
|
$platformInstances = PlatformInstance::where('is_active', true)
|
|
->orderBy('name')
|
|
->get(['id', 'platform', 'url', 'name', 'description', 'is_active']);
|
|
|
|
// Get existing feeds and channels for route creation
|
|
$feeds = Feed::where('is_active', true)
|
|
->orderBy('name')
|
|
->get(['id', 'name', 'url', 'type']);
|
|
|
|
$platformChannels = PlatformChannel::where('is_active', true)
|
|
->with(['platformInstance:id,name,url'])
|
|
->orderBy('name')
|
|
->get(['id', 'platform_instance_id', 'name', 'display_name', 'description']);
|
|
|
|
// Get feed providers from config
|
|
$feedProviders = collect(config('feed.providers', []))
|
|
->filter(fn($provider) => $provider['is_active'])
|
|
->values();
|
|
|
|
return $this->sendResponse([
|
|
'languages' => $languages,
|
|
'platform_instances' => $platformInstances,
|
|
'feeds' => $feeds,
|
|
'platform_channels' => $platformChannels,
|
|
'feed_providers' => $feedProviders,
|
|
], 'Onboarding options retrieved successfully.');
|
|
}
|
|
|
|
/**
|
|
* Create platform account for onboarding
|
|
*/
|
|
public function createPlatform(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'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()) {
|
|
throw new ValidationException($validator);
|
|
}
|
|
|
|
$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' => $fullInstanceUrl,
|
|
'platform' => $validated['platform'],
|
|
], [
|
|
'name' => ucfirst($instanceDomain),
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// Authenticate with Lemmy API using the full URL
|
|
$authResponse = $this->lemmyAuthService->authenticate(
|
|
$fullInstanceUrl,
|
|
$validated['username'],
|
|
$validated['password']
|
|
);
|
|
|
|
// Create platform account with the current schema
|
|
$platformAccount = PlatformAccount::create([
|
|
'platform' => $validated['platform'],
|
|
'instance_url' => $fullInstanceUrl,
|
|
'username' => $validated['username'],
|
|
'password' => $validated['password'],
|
|
'settings' => [
|
|
'display_name' => $authResponse['person_view']['person']['display_name'] ?? null,
|
|
'description' => $authResponse['person_view']['person']['bio'] ?? null,
|
|
'person_id' => $authResponse['person_view']['person']['id'] ?? null,
|
|
'platform_instance_id' => $platformInstance->id,
|
|
'api_token' => $authResponse['jwt'] ?? null, // Store JWT in settings for now
|
|
],
|
|
'is_active' => true,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
return $this->sendResponse(
|
|
new PlatformAccountResource($platformAccount),
|
|
'Platform account created successfully.'
|
|
);
|
|
|
|
} catch (\App\Exceptions\PlatformAuthException $e) {
|
|
// Check if it's a rate limit error
|
|
if (str_contains($e->getMessage(), 'Rate limited by')) {
|
|
return $this->sendError($e->getMessage(), [], 429);
|
|
}
|
|
|
|
return $this->sendError('Invalid username or password. Please check your credentials and try again.', [], 422);
|
|
} catch (\Exception $e) {
|
|
|
|
$message = 'Unable to connect to the Lemmy instance. Please check the URL and try again.';
|
|
|
|
// If it's a network/connection issue, provide a more specific message
|
|
if (str_contains(strtolower($e->getMessage()), 'connection') ||
|
|
str_contains(strtolower($e->getMessage()), 'network') ||
|
|
str_contains(strtolower($e->getMessage()), 'timeout')) {
|
|
$message = 'Connection failed. Please check the instance URL and your internet connection.';
|
|
}
|
|
|
|
return $this->sendError($message, [], 422);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create feed for onboarding
|
|
*/
|
|
public function createFeed(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'name' => 'required|string|max:255',
|
|
'provider' => 'required|in:belga,vrt',
|
|
'language_id' => 'required|exists:languages,id',
|
|
'description' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new ValidationException($validator);
|
|
}
|
|
|
|
$validated = $validator->validated();
|
|
|
|
// Map provider to preset URL and type as required by onboarding tests
|
|
$provider = $validated['provider'];
|
|
$url = null;
|
|
$type = 'website';
|
|
if ($provider === 'vrt') {
|
|
$url = 'https://www.vrt.be/vrtnws/en/';
|
|
} elseif ($provider === 'belga') {
|
|
$url = 'https://www.belganewsagency.eu/';
|
|
}
|
|
|
|
$feed = Feed::firstOrCreate(
|
|
['url' => $url],
|
|
[
|
|
'name' => $validated['name'],
|
|
'type' => $type,
|
|
'provider' => $provider,
|
|
'language_id' => $validated['language_id'],
|
|
'description' => $validated['description'] ?? null,
|
|
'is_active' => true,
|
|
]
|
|
);
|
|
|
|
return $this->sendResponse(
|
|
new FeedResource($feed->load('language')),
|
|
'Feed created successfully.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create channel for onboarding
|
|
* @throws ValidationException
|
|
*/
|
|
public function createChannel(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'name' => 'required|string|max:255',
|
|
'platform_instance_id' => 'required|exists:platform_instances,id',
|
|
'language_id' => 'required|exists:languages,id',
|
|
'description' => 'nullable|string|max:1000',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new ValidationException($validator);
|
|
}
|
|
|
|
$validated = $validator->validated();
|
|
|
|
// Get the platform instance to check for active accounts
|
|
$platformInstance = PlatformInstance::findOrFail($validated['platform_instance_id']);
|
|
|
|
// Check if there are active platform accounts for this instance
|
|
$activeAccounts = PlatformAccount::where('instance_url', $platformInstance->url)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
if ($activeAccounts->isEmpty()) {
|
|
return $this->sendError(
|
|
'Cannot create channel: No active platform accounts found for this instance. Please create a platform account first.',
|
|
[],
|
|
422
|
|
);
|
|
}
|
|
|
|
$channel = PlatformChannel::create([
|
|
'platform_instance_id' => $validated['platform_instance_id'],
|
|
'channel_id' => $validated['name'], // For Lemmy, this is the community name
|
|
'name' => $validated['name'],
|
|
'display_name' => ucfirst($validated['name']),
|
|
'description' => $validated['description'] ?? null,
|
|
'language_id' => $validated['language_id'],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
// Automatically attach the first active account to the channel
|
|
$firstAccount = $activeAccounts->first();
|
|
$channel->platformAccounts()->attach($firstAccount->id, [
|
|
'is_active' => true,
|
|
'priority' => 1,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
return $this->sendResponse(
|
|
new PlatformChannelResource($channel->load(['platformInstance', 'language', 'platformAccounts'])),
|
|
'Channel created successfully and linked to platform account.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create route for onboarding
|
|
*
|
|
* @throws ValidationException
|
|
*/
|
|
public function createRoute(Request $request): JsonResponse
|
|
{
|
|
$validator = Validator::make($request->all(), [
|
|
'feed_id' => 'required|exists:feeds,id',
|
|
'platform_channel_id' => 'required|exists:platform_channels,id',
|
|
'priority' => 'nullable|integer|min:1|max:100',
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
throw new ValidationException($validator);
|
|
}
|
|
|
|
$validated = $validator->validated();
|
|
|
|
$route = Route::create([
|
|
'feed_id' => $validated['feed_id'],
|
|
'platform_channel_id' => $validated['platform_channel_id'],
|
|
'priority' => $validated['priority'] ?? 50,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
return $this->sendResponse(
|
|
new RouteResource($route->load(['feed', 'platformChannel'])),
|
|
'Route created successfully.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Mark onboarding as complete
|
|
*/
|
|
public function complete(): JsonResponse
|
|
{
|
|
// In a real implementation, we might want to update a user preference
|
|
// or create a setting that tracks onboarding completion
|
|
// For now, we'll just return success since the onboarding status
|
|
// is determined by the existence of platform accounts, feeds, and channels
|
|
|
|
return $this->sendResponse(
|
|
['completed' => true],
|
|
'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.'
|
|
);
|
|
}
|
|
}
|