diff --git a/app/.env.example b/app/.env.example index 40dafc6..e145657 100644 --- a/app/.env.example +++ b/app/.env.example @@ -75,6 +75,7 @@ DEWEMOJI_PRO_MAX_LIMIT=50 DEWEMOJI_BILLING_MODE=sandbox DEWEMOJI_CHECKOUT_PENDING_COOLDOWN=120 DEWEMOJI_ALLOWED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,https://dewemoji.com,https://www.dewemoji.com +DEWEMOJI_CORS_ALLOW_ALL_PUBLIC=true DEWEMOJI_FRONTEND_HEADER=web-v1 DEWEMOJI_METRICS_ENABLED=true DEWEMOJI_METRICS_TOKEN= diff --git a/app/app/Http/Controllers/Api/V1/EmojiApiController.php b/app/app/Http/Controllers/Api/V1/EmojiApiController.php index aaca255..1af23f2 100644 --- a/app/app/Http/Controllers/Api/V1/EmojiApiController.php +++ b/app/app/Http/Controllers/Api/V1/EmojiApiController.php @@ -606,6 +606,8 @@ class EmojiApiController extends Controller $allowedOrigins = config('dewemoji.cors.allowed_origins', []); if (is_array($allowedOrigins) && in_array($origin, $allowedOrigins, true)) { $headers['Access-Control-Allow-Origin'] = $origin; + } elseif ((bool) config('dewemoji.cors.allow_all_public', true)) { + $headers['Access-Control-Allow-Origin'] = '*'; } return response()->json($payload, $status, $headers + $extraHeaders); diff --git a/app/app/Http/Controllers/Api/V1/ExtensionController.php b/app/app/Http/Controllers/Api/V1/ExtensionController.php index ac9bb91..5e2acb7 100644 --- a/app/app/Http/Controllers/Api/V1/ExtensionController.php +++ b/app/app/Http/Controllers/Api/V1/ExtensionController.php @@ -6,13 +6,9 @@ use App\Http\Controllers\Controller; use App\Services\Extension\ExtensionVerificationService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use RuntimeException; class ExtensionController extends Controller { - /** @var array|null */ - private static ?array $dataset = null; - public function __construct(private readonly ExtensionVerificationService $verifier) { } @@ -28,180 +24,4 @@ class ExtensionController extends Controller 'verified' => $ok, ], $ok ? 200 : 403); } - - public function search(Request $request): JsonResponse - { - $token = trim((string) $request->header('X-Extension-Token', '')); - $expected = config('dewemoji.public_access.extension_ids', []); - $ok = $this->verifier->verifyToken($token, is_array($expected) ? $expected : []); - if (!$ok) { - return response()->json(['ok' => false, 'error' => 'extension_unverified'], 403); - } - - try { - $data = $this->loadData(); - } catch (RuntimeException $e) { - return response()->json([ - 'ok' => false, - 'error' => 'data_load_failed', - 'message' => $e->getMessage(), - ], 500); - } - - $items = $data['emojis'] ?? []; - $q = trim((string) ($request->query('q', $request->query('query', '')))); - $category = $this->normalizeCategoryFilter((string) $request->query('category', '')); - $subSlug = $this->slugify((string) $request->query('subcategory', '')); - $page = max((int) $request->query('page', 1), 1); - - $defaultLimit = max((int) config('dewemoji.pagination.default_limit', 20), 1); - $maxLimit = max((int) config('dewemoji.pagination.free_max_limit', 20), 1); - $limit = min(max((int) $request->query('limit', $defaultLimit), 1), $maxLimit); - - $filtered = $this->filterItems($items, $q, $category, $subSlug); - $total = count($filtered); - $offset = ($page - 1) * $limit; - $pageItems = array_slice($filtered, $offset, $limit); - - return response()->json([ - 'ok' => true, - 'items' => array_map(fn (array $item): array => $this->transformItem($item), $pageItems), - 'total' => $total, - 'page' => $page, - 'limit' => $limit, - 'plan' => 'free', - ]); - } - - /** - * @return array - */ - private function loadData(): array - { - if (self::$dataset !== null) { - return self::$dataset; - } - - $path = (string) config('dewemoji.data_path'); - if (!is_file($path)) { - throw new RuntimeException('Emoji dataset file was not found at: '.$path); - } - - $raw = file_get_contents($path); - if ($raw === false) { - throw new RuntimeException('Emoji dataset file could not be read.'); - } - - $decoded = json_decode($raw, true); - if (!is_array($decoded)) { - throw new RuntimeException('Emoji dataset JSON is invalid.'); - } - - self::$dataset = $decoded; - return self::$dataset; - } - - private function normalizeCategoryFilter(string $category): string - { - $value = strtolower(trim($category)); - if ($value === '' || $value === 'all') { - return ''; - } - - $map = [ - 'all' => 'all', - 'smileys' => 'Smileys & Emotion', - 'people' => 'People & Body', - 'animals' => 'Animals & Nature', - 'food' => 'Food & Drink', - 'travel' => 'Travel & Places', - 'activities' => 'Activities', - 'objects' => 'Objects', - 'symbols' => 'Symbols', - 'flags' => 'Flags', - ]; - return $map[$value] ?? $category; - } - - private function slugify(string $text): string - { - $value = strtolower(trim($text)); - $value = str_replace('&', 'and', $value); - $value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? ''; - return trim($value, '-'); - } - - /** - * @param array> $items - * @return array> - */ - private function filterItems(array $items, string $q, string $category, string $subSlug): array - { - return array_values(array_filter($items, function (array $item) use ($q, $category, $subSlug): bool { - $itemCategory = trim((string) ($item['category'] ?? '')); - $itemSubcategory = trim((string) ($item['subcategory'] ?? '')); - - if ($category !== '' && strcasecmp($itemCategory, $category) !== 0) { - return false; - } - if ($subSlug !== '' && $this->slugify($itemSubcategory) !== $subSlug) { - return false; - } - if ($q === '') { - return true; - } - - $haystack = strtolower(implode(' ', [ - (string) ($item['emoji'] ?? ''), - (string) ($item['name'] ?? ''), - (string) ($item['slug'] ?? ''), - $itemCategory, - $itemSubcategory, - implode(' ', $item['keywords_en'] ?? []), - implode(' ', $item['keywords_id'] ?? []), - implode(' ', $item['aliases'] ?? []), - implode(' ', $item['shortcodes'] ?? []), - implode(' ', $item['alt_shortcodes'] ?? []), - implode(' ', $item['intent_tags'] ?? []), - ])); - - $tokens = preg_split('/\s+/', strtolower($q)) ?: []; - foreach ($tokens as $token) { - if ($token === '') { - continue; - } - if (!str_contains($haystack, $token)) { - return false; - } - } - return true; - })); - } - - /** - * @param array $item - * @return array - */ - private function transformItem(array $item): array - { - return [ - 'emoji' => (string) ($item['emoji'] ?? ''), - 'name' => (string) ($item['name'] ?? ''), - 'slug' => (string) ($item['slug'] ?? ''), - 'category' => (string) ($item['category'] ?? ''), - 'subcategory' => (string) ($item['subcategory'] ?? ''), - 'supports_skin_tone' => (bool) ($item['supports_skin_tone'] ?? false), - 'summary' => $this->summary((string) ($item['description'] ?? ''), 150), - ]; - } - - private function summary(string $text, int $max): string - { - $text = trim(preg_replace('/\s+/', ' ', strip_tags($text)) ?? ''); - if (mb_strlen($text) <= $max) { - return $text; - } - - return rtrim(mb_substr($text, 0, $max - 1), " ,.;:-").'…'; - } } diff --git a/app/config/dewemoji.php b/app/config/dewemoji.php index 5b9c9ee..913e63f 100644 --- a/app/config/dewemoji.php +++ b/app/config/dewemoji.php @@ -56,6 +56,7 @@ return [ 'cors' => [ 'allowed_origins' => array_values(array_filter(array_map('trim', explode(',', (string) env('DEWEMOJI_ALLOWED_ORIGINS', 'http://127.0.0.1:8000,http://localhost:8000,https://dewemoji.com,https://www.dewemoji.com'))))), + 'allow_all_public' => filter_var(env('DEWEMOJI_CORS_ALLOW_ALL_PUBLIC', true), FILTER_VALIDATE_BOOL), 'allow_methods' => 'GET, POST, OPTIONS', 'allow_headers' => 'Content-Type, Authorization, X-Api-Key, X-Admin-Token, X-Account-Id, X-Dewemoji-Frontend, X-Extension-Token', ], diff --git a/app/routes/api.php b/app/routes/api.php index 0e08450..f2ba3fc 100644 --- a/app/routes/api.php +++ b/app/routes/api.php @@ -26,6 +26,8 @@ Route::options('/v1/{any}', function () { $allowedOrigins = config('dewemoji.cors.allowed_origins', []); if (is_array($allowedOrigins) && in_array($origin, $allowedOrigins, true)) { $headers['Access-Control-Allow-Origin'] = $origin; + } elseif ((bool) config('dewemoji.cors.allow_all_public', true)) { + $headers['Access-Control-Allow-Origin'] = '*'; } return response('', 204, $headers); @@ -42,7 +44,7 @@ Route::prefix('v1')->group(function () { Route::get('/emoji/{slug}', [EmojiApiController::class, 'emoji']); Route::get('/pricing', [PricingController::class, 'index']); Route::post('/extension/verify', [ExtensionController::class, 'verify']); - Route::get('/extension/search', [ExtensionController::class, 'search']); + Route::get('/extension/search', [EmojiApiController::class, 'emojis']); Route::post('/user/register', [UserController::class, 'register']); Route::post('/user/login', [UserController::class, 'login']);