Files
SkinbaseNova/app/Services/Academy/AcademyAccessService.php

326 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Academy;
use App\Models\AcademyAiComparisonResult;
use App\Models\AcademyChallenge;
use App\Models\AcademyLesson;
use App\Models\AcademyLessonBlock;
use App\Models\AcademyPromptPack;
use App\Models\AcademyPromptTemplate;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
final class AcademyAccessService
{
public function canAccessContent(?User $user, string $accessLevel): bool
{
if ($accessLevel === 'free') {
return true;
}
if ($user === null) {
return false;
}
if ($user->isAdmin()) {
return true;
}
return $this->rankForUser($user) >= $this->rankForLevel($accessLevel);
}
public function canAccessLesson(?User $user, AcademyLesson $lesson): bool
{
return $this->canAccessContent($user, (string) $lesson->access_level);
}
public function canAccessPrompt(?User $user, AcademyPromptTemplate $prompt): bool
{
return $this->canAccessContent($user, (string) $prompt->access_level);
}
public function canAccessPack(?User $user, AcademyPromptPack $pack): bool
{
return $this->canAccessContent($user, (string) $pack->access_level);
}
public function canAccessChallenge(?User $user, AcademyChallenge $challenge): bool
{
return $this->canAccessContent($user, (string) $challenge->access_level);
}
public function lessonPayload(AcademyLesson $lesson, ?User $viewer, bool $includeFull = false): array
{
$authorized = $this->canAccessLesson($viewer, $lesson);
$fullContent = (string) ($lesson->content ?? '');
return [
'id' => (int) $lesson->id,
'title' => (string) $lesson->title,
'slug' => (string) $lesson->slug,
'excerpt' => (string) ($lesson->excerpt ?? ''),
'content' => ($authorized && $includeFull) ? $fullContent : null,
'content_preview' => $authorized ? null : $this->previewText($fullContent, 360),
'difficulty' => (string) $lesson->difficulty,
'access_level' => (string) $lesson->access_level,
'lesson_type' => (string) $lesson->lesson_type,
'cover_image' => $lesson->cover_image,
'cover_image_url' => $this->resolveLessonCoverImageUrl((string) ($lesson->cover_image ?? '')),
'video_url' => $authorized ? $lesson->video_url : null,
'reading_minutes' => (int) $lesson->reading_minutes,
'featured' => (bool) $lesson->featured,
'active' => (bool) $lesson->active,
'published_at' => $lesson->published_at?->toISOString(),
'category' => $lesson->category ? [
'id' => (int) $lesson->category->id,
'name' => (string) $lesson->category->name,
'slug' => (string) $lesson->category->slug,
] : null,
'blocks' => ($authorized && $includeFull)
? $lesson->activeBlocks->map(fn (AcademyLessonBlock $block): ?array => $this->lessonBlockPayload($block))->filter()->values()->all()
: [],
'locked' => ! $authorized,
'can_access' => $authorized,
];
}
public function promptPayload(AcademyPromptTemplate $prompt, ?User $viewer, bool $includeFull = false): array
{
$authorized = $this->canAccessPrompt($viewer, $prompt);
return [
'id' => (int) $prompt->id,
'title' => (string) $prompt->title,
'slug' => (string) $prompt->slug,
'excerpt' => (string) ($prompt->excerpt ?? ''),
'prompt' => ($authorized && $includeFull) ? (string) $prompt->prompt : null,
'negative_prompt' => ($authorized && $includeFull) ? (string) ($prompt->negative_prompt ?? '') : null,
'usage_notes' => ($authorized && $includeFull) ? (string) ($prompt->usage_notes ?? '') : null,
'workflow_notes' => ($authorized && $includeFull) ? (string) ($prompt->workflow_notes ?? '') : null,
'prompt_preview' => $authorized ? null : $this->previewText((string) $prompt->prompt, 220),
'difficulty' => (string) $prompt->difficulty,
'access_level' => (string) $prompt->access_level,
'aspect_ratio' => $prompt->aspect_ratio,
'tags' => array_values((array) ($prompt->tags ?? [])),
'tool_notes' => $authorized ? (array) ($prompt->tool_notes ?? []) : [],
'preview_image' => $this->resolvePreviewImageUrl((string) ($prompt->preview_image ?? '')),
'featured' => (bool) $prompt->featured,
'prompt_of_week' => (bool) $prompt->prompt_of_week,
'published_at' => $prompt->published_at?->toISOString(),
'category' => $prompt->category ? [
'id' => (int) $prompt->category->id,
'name' => (string) $prompt->category->name,
'slug' => (string) $prompt->category->slug,
] : null,
'locked' => ! $authorized,
'can_access' => $authorized,
];
}
public function packPayload(AcademyPromptPack $pack, ?User $viewer, bool $includePrompts = false): array
{
$authorized = $this->canAccessPack($viewer, $pack);
return [
'id' => (int) $pack->id,
'title' => (string) $pack->title,
'slug' => (string) $pack->slug,
'excerpt' => (string) ($pack->excerpt ?? ''),
'description' => (string) ($pack->description ?? ''),
'access_level' => (string) $pack->access_level,
'cover_image' => $pack->cover_image,
'tags' => array_values((array) ($pack->tags ?? [])),
'featured' => (bool) $pack->featured,
'published_at' => $pack->published_at?->toISOString(),
'locked' => ! $authorized,
'can_access' => $authorized,
'prompts' => $includePrompts
? $pack->prompts->map(fn (AcademyPromptTemplate $prompt): array => $this->promptPayload($prompt, $viewer, $authorized))->values()->all()
: [],
];
}
public function challengePayload(AcademyChallenge $challenge, ?User $viewer, bool $includeSubmissions = false): array
{
$authorized = $this->canAccessChallenge($viewer, $challenge);
return [
'id' => (int) $challenge->id,
'title' => (string) $challenge->title,
'slug' => (string) $challenge->slug,
'excerpt' => (string) ($challenge->excerpt ?? ''),
'description' => (string) ($challenge->description ?? ''),
'brief' => (string) ($challenge->brief ?? ''),
'rules' => (string) ($challenge->rules ?? ''),
'access_level' => (string) $challenge->access_level,
'status' => (string) $challenge->status,
'starts_at' => $challenge->starts_at?->toISOString(),
'ends_at' => $challenge->ends_at?->toISOString(),
'voting_starts_at' => $challenge->voting_starts_at?->toISOString(),
'voting_ends_at' => $challenge->voting_ends_at?->toISOString(),
'cover_image' => $challenge->cover_image,
'prize_text' => $challenge->prize_text,
'required_tags' => array_values((array) ($challenge->required_tags ?? [])),
'allowed_categories' => array_values((array) ($challenge->allowed_categories ?? [])),
'featured' => (bool) $challenge->featured,
'locked' => ! $authorized,
'can_access' => $authorized,
'submission_count' => $includeSubmissions ? (int) $challenge->submissions()->approved()->count() : null,
];
}
private function rankForUser(User $user): int
{
if (method_exists($user, 'hasAcademyProAccess') && $user->hasAcademyProAccess()) {
return $this->rankForLevel('pro');
}
if (method_exists($user, 'hasAcademyCreatorAccess') && $user->hasAcademyCreatorAccess()) {
return $this->rankForLevel('creator');
}
return $this->rankForLevel('free');
}
private function rankForLevel(string $accessLevel): int
{
return match (Str::lower(trim($accessLevel))) {
'admin' => 99,
'pro' => 30,
'creator' => 20,
default => 10,
};
}
private function previewText(string $value, int $limit): string
{
$plain = trim(strip_tags($value));
if ($plain === '') {
return '';
}
$length = mb_strlen($plain);
$previewLength = min($limit, max(12, (int) ceil($length * 0.55)));
if ($previewLength >= $length) {
$previewLength = max(1, $length - 1);
}
return rtrim(mb_substr($plain, 0, $previewLength)).'...';
}
private function resolvePreviewImageUrl(string $previewImage): ?string
{
$previewImage = trim($previewImage);
if ($previewImage === '') {
return null;
}
if (str_starts_with($previewImage, 'http://') || str_starts_with($previewImage, 'https://') || str_starts_with($previewImage, '/')) {
return $previewImage;
}
return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($previewImage);
}
private function resolveLessonCoverImageUrl(string $coverImage): ?string
{
$coverImage = trim($coverImage);
if ($coverImage === '') {
return null;
}
if (str_starts_with($coverImage, 'http://') || str_starts_with($coverImage, 'https://') || str_starts_with($coverImage, '/')) {
return $coverImage;
}
return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($coverImage);
}
private function resolveLessonMediaUrl(string $path): ?string
{
$path = trim($path);
if ($path === '') {
return null;
}
if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://') || str_starts_with($path, '/')) {
return $path;
}
return Storage::disk((string) config('uploads.object_storage.disk', 's3'))->url($path);
}
/**
* @return array<string, mixed>|null
*/
private function lessonBlockPayload(AcademyLessonBlock $block): ?array
{
if ($block->type !== 'ai_comparison') {
return null;
}
$payload = is_array($block->payload) ? $block->payload : [];
$criteria = collect($payload['criteria'] ?? [])
->map(static fn ($criterion): string => trim((string) $criterion))
->filter(static fn (string $criterion): bool => $criterion !== '')
->values()
->all();
$results = $block->activeComparisonResults
->map(fn (AcademyAiComparisonResult $result): array => [
'id' => (int) $result->id,
'provider' => (string) ($result->provider ?? ''),
'model_name' => (string) ($result->model_name ?? ''),
'image_path' => (string) $result->image_path,
'image_url' => $this->resolveLessonMediaUrl((string) $result->image_path),
'thumb_path' => (string) ($result->thumb_path ?? ''),
'thumb_url' => $this->resolveLessonMediaUrl((string) ($result->thumb_path ?? '')),
'settings' => (string) ($result->settings ?? ''),
'strengths' => (string) ($result->strengths ?? ''),
'weaknesses' => (string) ($result->weaknesses ?? ''),
'best_for' => (string) ($result->best_for ?? ''),
'score' => $result->score,
'sort_order' => (int) $result->sort_order,
'active' => (bool) $result->active,
])
->values()
->all();
$hasPromptData = filled($payload['prompt'] ?? null)
|| filled($payload['negative_prompt'] ?? null)
|| filled($payload['intro'] ?? null)
|| filled($payload['title'] ?? null)
|| filled($payload['aspect_ratio'] ?? null)
|| ! empty($criteria);
if (! $hasPromptData && $results === []) {
return null;
}
return [
'id' => (int) $block->id,
'type' => (string) $block->type,
'title' => (string) ($block->title ?? ($payload['title'] ?? '')),
'payload' => [
'title' => (string) ($payload['title'] ?? ''),
'intro' => (string) ($payload['intro'] ?? ''),
'prompt' => (string) ($payload['prompt'] ?? ''),
'negative_prompt' => (string) ($payload['negative_prompt'] ?? ''),
'aspect_ratio' => (string) ($payload['aspect_ratio'] ?? ''),
'criteria' => $criteria,
],
'sort_order' => (int) $block->sort_order,
'active' => (bool) $block->active,
'comparison_results' => $results,
];
}
}