Release v1.1.0 #79
3 changed files with 0 additions and 441 deletions
|
|
@ -2,12 +2,6 @@
|
|||
|
||||
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\Jobs\ArticleDiscoveryJob;
|
||||
use App\Models\Feed;
|
||||
use App\Models\Language;
|
||||
use App\Models\PlatformAccount;
|
||||
|
|
@ -15,18 +9,10 @@
|
|||
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
|
||||
*/
|
||||
|
|
@ -111,233 +97,6 @@ public function options(): JsonResponse
|
|||
], '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,
|
||||
]);
|
||||
|
||||
// Trigger article discovery when the first route is created during onboarding
|
||||
// This ensures articles start being fetched immediately after setup
|
||||
ArticleDiscoveryJob::dispatch();
|
||||
|
||||
return $this->sendResponse(
|
||||
new RouteResource($route->load(['feed', 'platformChannel'])),
|
||||
'Route created successfully.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark onboarding as complete
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -38,10 +38,6 @@
|
|||
// Onboarding
|
||||
Route::get('/onboarding/status', [OnboardingController::class, 'status'])->name('api.onboarding.status');
|
||||
Route::get('/onboarding/options', [OnboardingController::class, 'options'])->name('api.onboarding.options');
|
||||
Route::post('/onboarding/platform', [OnboardingController::class, 'createPlatform'])->name('api.onboarding.platform');
|
||||
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/route', [OnboardingController::class, 'createRoute'])->name('api.onboarding.route');
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
use App\Models\PlatformInstance;
|
||||
use App\Models\Route;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Auth\LemmyAuthService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
|
@ -189,180 +188,6 @@ public function test_options_returns_languages_and_platform_instances()
|
|||
]);
|
||||
}
|
||||
|
||||
public function test_create_feed_validates_required_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['name', 'provider', 'language_id']);
|
||||
}
|
||||
|
||||
public function test_create_feed_creates_vrt_feed_successfully()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'VRT Test Feed',
|
||||
'provider' => 'vrt',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => 'VRT Test Feed',
|
||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||
'type' => 'website',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'VRT Test Feed',
|
||||
'url' => 'https://www.vrt.be/vrtnws/en/',
|
||||
'type' => 'website',
|
||||
'language_id' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_feed_creates_belga_feed_successfully()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'Belga Test Feed',
|
||||
'provider' => 'belga',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'name' => 'Belga Test Feed',
|
||||
'url' => 'https://www.belganewsagency.eu/',
|
||||
'type' => 'website',
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('feeds', [
|
||||
'name' => 'Belga Test Feed',
|
||||
'url' => 'https://www.belganewsagency.eu/',
|
||||
'type' => 'website',
|
||||
'language_id' => 1,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_feed_rejects_invalid_provider()
|
||||
{
|
||||
$feedData = [
|
||||
'name' => 'Invalid Feed',
|
||||
'provider' => 'invalid',
|
||||
'language_id' => 1,
|
||||
'description' => 'Test description',
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/feed', $feedData);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['provider']);
|
||||
}
|
||||
|
||||
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();
|
||||
$language = Language::factory()->create();
|
||||
|
||||
// Create a platform account for this instance first
|
||||
PlatformAccount::factory()->create([
|
||||
'instance_url' => $platformInstance->url,
|
||||
'is_active' => true
|
||||
]);
|
||||
|
||||
$channelData = [
|
||||
'name' => 'test_community',
|
||||
'platform_instance_id' => $platformInstance->id,
|
||||
'language_id' => $language->id,
|
||||
'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' => $language->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_create_route_validates_required_fields()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/route', []);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['feed_id', 'platform_channel_id']);
|
||||
}
|
||||
|
||||
public function test_create_route_creates_route_successfully()
|
||||
{
|
||||
$language = Language::first();
|
||||
$feed = Feed::factory()->language($language)->create();
|
||||
$platformChannel = PlatformChannel::factory()->create();
|
||||
|
||||
$routeData = [
|
||||
'feed_id' => $feed->id,
|
||||
'platform_channel_id' => $platformChannel->id,
|
||||
'priority' => 75,
|
||||
];
|
||||
|
||||
$response = $this->postJson('/api/v1/onboarding/route', $routeData);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'feed_id' => $feed->id,
|
||||
'platform_channel_id' => $platformChannel->id,
|
||||
'priority' => 75,
|
||||
'is_active' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('routes', [
|
||||
'feed_id' => $feed->id,
|
||||
'platform_channel_id' => $platformChannel->id,
|
||||
'priority' => 75,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_complete_onboarding_returns_success()
|
||||
{
|
||||
$response = $this->postJson('/api/v1/onboarding/complete');
|
||||
|
|
@ -443,27 +268,6 @@ public function test_reset_skip_works_when_no_setting_exists()
|
|||
]);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue