feat: phase 2 api parity endpoints for extension
This commit is contained in:
@@ -13,6 +13,7 @@ This documentation now uses the corrected legacy/source folders:
|
|||||||
3. `legacy-credentials-and-config.md`
|
3. `legacy-credentials-and-config.md`
|
||||||
4. `rebuild-progress.md`
|
4. `rebuild-progress.md`
|
||||||
5. `phase-1-foundation.md`
|
5. `phase-1-foundation.md`
|
||||||
|
6. `phase-2-api.md`
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
|
|
||||||
|
|||||||
@@ -63,3 +63,9 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|
||||||
|
DEWEMOJI_DATA_PATH=
|
||||||
|
DEWEMOJI_DEFAULT_LIMIT=20
|
||||||
|
DEWEMOJI_MAX_LIMIT=50
|
||||||
|
DEWEMOJI_LICENSE_ACCEPT_ALL=true
|
||||||
|
DEWEMOJI_PRO_KEYS=
|
||||||
|
|||||||
226
app/app/Http/Controllers/Api/V1/EmojiApiController.php
Normal file
226
app/app/Http/Controllers/Api/V1/EmojiApiController.php
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class EmojiApiController extends Controller
|
||||||
|
{
|
||||||
|
private const TIER_FREE = 'free';
|
||||||
|
private const TIER_PRO = 'pro';
|
||||||
|
|
||||||
|
/** @var array<string,mixed>|null */
|
||||||
|
private static ?array $dataset = null;
|
||||||
|
|
||||||
|
public function categories(): JsonResponse
|
||||||
|
{
|
||||||
|
$tier = $this->detectTier(request());
|
||||||
|
try {
|
||||||
|
$data = $this->loadData();
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
return $this->jsonWithTier([
|
||||||
|
'error' => 'data_load_failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
], $tier, 500);
|
||||||
|
}
|
||||||
|
$items = $data['emojis'] ?? [];
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$category = (string) ($item['category'] ?? '');
|
||||||
|
$subcategory = (string) ($item['subcategory'] ?? '');
|
||||||
|
if ($category === '' || $subcategory === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$map[$category] ??= [];
|
||||||
|
$map[$category][$subcategory] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($map as $category => $subcategories) {
|
||||||
|
$out[$category] = array_keys($subcategories);
|
||||||
|
sort($out[$category], SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
}
|
||||||
|
ksort($out, SORT_NATURAL | SORT_FLAG_CASE);
|
||||||
|
|
||||||
|
return $this->jsonWithTier($out, $tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function emojis(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$tier = $this->detectTier($request);
|
||||||
|
try {
|
||||||
|
$data = $this->loadData();
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
return $this->jsonWithTier([
|
||||||
|
'error' => 'data_load_failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
], $tier, 500);
|
||||||
|
}
|
||||||
|
$items = $data['emojis'] ?? [];
|
||||||
|
|
||||||
|
$q = trim((string) ($request->query('q', $request->query('query', ''))));
|
||||||
|
$category = trim((string) $request->query('category', ''));
|
||||||
|
$subcategory = trim((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.max_limit', 50), 1);
|
||||||
|
$limit = min(max((int) $request->query('limit', $defaultLimit), 1), $maxLimit);
|
||||||
|
|
||||||
|
$filtered = array_values(array_filter($items, function (array $item) use ($q, $category, $subcategory): bool {
|
||||||
|
if ($category !== '' && strcasecmp((string) ($item['category'] ?? ''), $category) !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subcategory !== '' && strcasecmp((string) ($item['subcategory'] ?? ''), $subcategory) !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($q === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$haystack = strtolower(implode(' ', [
|
||||||
|
(string) ($item['emoji'] ?? ''),
|
||||||
|
(string) ($item['name'] ?? ''),
|
||||||
|
(string) ($item['slug'] ?? ''),
|
||||||
|
(string) ($item['category'] ?? ''),
|
||||||
|
(string) ($item['subcategory'] ?? ''),
|
||||||
|
implode(' ', $item['keywords_en'] ?? []),
|
||||||
|
implode(' ', $item['keywords_id'] ?? []),
|
||||||
|
implode(' ', $item['aliases'] ?? []),
|
||||||
|
implode(' ', $item['shortcodes'] ?? []),
|
||||||
|
implode(' ', $item['alt_shortcodes'] ?? []),
|
||||||
|
implode(' ', $item['intent_tags'] ?? []),
|
||||||
|
]));
|
||||||
|
|
||||||
|
return str_contains($haystack, strtolower($q));
|
||||||
|
}));
|
||||||
|
|
||||||
|
$total = count($filtered);
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
$pageItems = array_slice($filtered, $offset, $limit);
|
||||||
|
|
||||||
|
$outItems = array_map(fn (array $item): array => $this->transformItem($item, $tier), $pageItems);
|
||||||
|
|
||||||
|
return $this->jsonWithTier([
|
||||||
|
'items' => $outItems,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
], $tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectTier(Request $request): string
|
||||||
|
{
|
||||||
|
$key = trim((string) $request->bearerToken());
|
||||||
|
if ($key === '') {
|
||||||
|
$key = trim((string) $request->header('X-License-Key', ''));
|
||||||
|
}
|
||||||
|
if ($key === '') {
|
||||||
|
return self::TIER_FREE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) config('dewemoji.license.accept_all', false)) {
|
||||||
|
return self::TIER_PRO;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validKeys = config('dewemoji.license.pro_keys', []);
|
||||||
|
if (is_array($validKeys) && in_array($key, $validKeys, true)) {
|
||||||
|
return self::TIER_PRO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::TIER_FREE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $item
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
private function transformItem(array $item, string $tier): array
|
||||||
|
{
|
||||||
|
$out = [
|
||||||
|
'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),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($tier === self::TIER_PRO) {
|
||||||
|
$out += [
|
||||||
|
'unified' => (string) ($item['unified'] ?? ''),
|
||||||
|
'codepoints' => $item['codepoints'] ?? [],
|
||||||
|
'shortcodes' => $item['shortcodes'] ?? [],
|
||||||
|
'aliases' => $item['aliases'] ?? [],
|
||||||
|
'keywords_en' => $item['keywords_en'] ?? [],
|
||||||
|
'keywords_id' => $item['keywords_id'] ?? [],
|
||||||
|
'related' => $item['related'] ?? [],
|
||||||
|
'intent_tags' => $item['intent_tags'] ?? [],
|
||||||
|
'description' => (string) ($item['description'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
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), " ,.;:-").'…';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $payload
|
||||||
|
*/
|
||||||
|
private function jsonWithTier(array $payload, string $tier, int $status = 200): JsonResponse
|
||||||
|
{
|
||||||
|
return response()
|
||||||
|
->json($payload, $status, [
|
||||||
|
'X-Dewemoji-Tier' => $tier,
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Account-Id, X-Dewemoji-Frontend',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/app/Http/Controllers/Api/V1/LicenseController.php
Normal file
76
app/app/Http/Controllers/Api/V1/LicenseController.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class LicenseController extends Controller
|
||||||
|
{
|
||||||
|
public function verify(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$key = trim((string) $request->input('key', ''));
|
||||||
|
$accountId = trim((string) $request->input('account_id', ''));
|
||||||
|
$version = trim((string) $request->input('version', ''));
|
||||||
|
|
||||||
|
if ($key === '') {
|
||||||
|
return $this->response([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'missing_key',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($accountId === '') {
|
||||||
|
return $this->response([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'missing_account_id',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($version === '') {
|
||||||
|
return $this->response([
|
||||||
|
'ok' => false,
|
||||||
|
'error' => 'missing_version',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$valid = $this->isValidKey($key);
|
||||||
|
|
||||||
|
return $this->response([
|
||||||
|
'ok' => $valid,
|
||||||
|
'tier' => $valid ? 'pro' : 'free',
|
||||||
|
'error' => $valid ? null : 'invalid_license',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isValidKey(string $key): bool
|
||||||
|
{
|
||||||
|
if ((bool) config('dewemoji.license.accept_all', false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validKeys = config('dewemoji.license.pro_keys', []);
|
||||||
|
if (!is_array($validKeys)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($key, $validKeys, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $payload
|
||||||
|
*/
|
||||||
|
private function response(array $payload, int $status = 200): JsonResponse
|
||||||
|
{
|
||||||
|
$tier = ($payload['ok'] ?? false) ? 'pro' : 'free';
|
||||||
|
|
||||||
|
return response()->json($payload, $status, [
|
||||||
|
'X-Dewemoji-Tier' => $tier,
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Account-Id, X-Dewemoji-Frontend',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@ use Illuminate\Foundation\Configuration\Middleware;
|
|||||||
return Application::configure(basePath: dirname(__DIR__))
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
api: __DIR__.'/../routes/api.php',
|
||||||
|
apiPrefix: '',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
|
|||||||
16
app/config/dewemoji.php
Normal file
16
app/config/dewemoji.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'data_path' => env('DEWEMOJI_DATA_PATH', base_path('../../dewemoji-api/data/emojis.json')),
|
||||||
|
|
||||||
|
'pagination' => [
|
||||||
|
'default_limit' => (int) env('DEWEMOJI_DEFAULT_LIMIT', 20),
|
||||||
|
'max_limit' => (int) env('DEWEMOJI_MAX_LIMIT', 50),
|
||||||
|
],
|
||||||
|
|
||||||
|
'license' => [
|
||||||
|
'accept_all' => filter_var(env('DEWEMOJI_LICENSE_ACCEPT_ALL', false), FILTER_VALIDATE_BOOL),
|
||||||
|
'pro_keys' => array_values(array_filter(array_map('trim', explode(',', (string) env('DEWEMOJI_PRO_KEYS', ''))))),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
20
app/routes/api.php
Normal file
20
app/routes/api.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V1\EmojiApiController;
|
||||||
|
use App\Http\Controllers\Api\V1\LicenseController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::options('/v1/{any}', function () {
|
||||||
|
return response('', 204, [
|
||||||
|
'Access-Control-Allow-Origin' => '*',
|
||||||
|
'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-License-Key, X-Account-Id, X-Dewemoji-Frontend',
|
||||||
|
]);
|
||||||
|
})->where('any', '.*');
|
||||||
|
|
||||||
|
Route::prefix('v1')->group(function () {
|
||||||
|
Route::get('/categories', [EmojiApiController::class, 'categories']);
|
||||||
|
Route::get('/emojis', [EmojiApiController::class, 'emojis']);
|
||||||
|
Route::post('/license/verify', [LicenseController::class, 'verify']);
|
||||||
|
});
|
||||||
|
|
||||||
57
app/tests/Feature/ApiV1EndpointsTest.php
Normal file
57
app/tests/Feature/ApiV1EndpointsTest.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ApiV1EndpointsTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
config()->set('dewemoji.data_path', base_path('tests/Fixtures/emojis.fixture.json'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_categories_endpoint_returns_category_map(): void
|
||||||
|
{
|
||||||
|
$response = $this->getJson('/v1/categories');
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertHeader('X-Dewemoji-Tier', 'free')
|
||||||
|
->assertJsonPath('Smileys & Emotion.0', 'face-smiling')
|
||||||
|
->assertJsonPath('People & Body.0', 'hand-fingers-closed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_emojis_endpoint_supports_both_q_and_query_params(): void
|
||||||
|
{
|
||||||
|
$byQ = $this->getJson('/v1/emojis?q=grinning');
|
||||||
|
$byQuery = $this->getJson('/v1/emojis?query=grinning');
|
||||||
|
|
||||||
|
$byQ->assertOk()->assertJsonPath('total', 1);
|
||||||
|
$byQuery->assertOk()->assertJsonPath('total', 1);
|
||||||
|
$byQ->assertJsonPath('items.0.slug', 'grinning-face');
|
||||||
|
$byQuery->assertJsonPath('items.0.slug', 'grinning-face');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_license_verify_returns_ok_when_accept_all_is_enabled(): void
|
||||||
|
{
|
||||||
|
config()->set('dewemoji.license.accept_all', true);
|
||||||
|
|
||||||
|
$response = $this->postJson('/v1/license/verify', [
|
||||||
|
'key' => 'dummy-key',
|
||||||
|
'account_id' => 'acct_123',
|
||||||
|
'version' => '1.0.0',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertHeader('X-Dewemoji-Tier', 'pro')
|
||||||
|
->assertJson([
|
||||||
|
'ok' => true,
|
||||||
|
'tier' => 'pro',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
39
app/tests/Fixtures/emojis.fixture.json
Normal file
39
app/tests/Fixtures/emojis.fixture.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"emojis": [
|
||||||
|
{
|
||||||
|
"emoji": "😀",
|
||||||
|
"name": "grinning face",
|
||||||
|
"slug": "grinning-face",
|
||||||
|
"category": "Smileys & Emotion",
|
||||||
|
"subcategory": "face-smiling",
|
||||||
|
"supports_skin_tone": false,
|
||||||
|
"description": "A happy smiling face.",
|
||||||
|
"unified": "U+1F600",
|
||||||
|
"codepoints": ["1F600"],
|
||||||
|
"shortcodes": [":grinning_face:"],
|
||||||
|
"aliases": ["grin face"],
|
||||||
|
"keywords_en": ["happy", "smile"],
|
||||||
|
"keywords_id": ["senang", "senyum"],
|
||||||
|
"related": ["😃"],
|
||||||
|
"intent_tags": ["happiness"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"emoji": "👍",
|
||||||
|
"name": "thumbs up",
|
||||||
|
"slug": "thumbs-up",
|
||||||
|
"category": "People & Body",
|
||||||
|
"subcategory": "hand-fingers-closed",
|
||||||
|
"supports_skin_tone": true,
|
||||||
|
"description": "A thumbs up gesture.",
|
||||||
|
"unified": "U+1F44D",
|
||||||
|
"codepoints": ["1F44D"],
|
||||||
|
"shortcodes": [":thumbsup:"],
|
||||||
|
"aliases": ["like"],
|
||||||
|
"keywords_en": ["ok", "good"],
|
||||||
|
"keywords_id": ["bagus"],
|
||||||
|
"related": ["👎"],
|
||||||
|
"intent_tags": ["approval"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
43
phase-2-api.md
Normal file
43
phase-2-api.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Phase 2 API Delivery
|
||||||
|
|
||||||
|
## Implemented endpoints (Laravel app)
|
||||||
|
|
||||||
|
Base path: `app/` project
|
||||||
|
|
||||||
|
- `GET /v1/categories`
|
||||||
|
- `GET /v1/emojis`
|
||||||
|
- `POST /v1/license/verify`
|
||||||
|
- `OPTIONS /v1/{any}` (CORS preflight)
|
||||||
|
|
||||||
|
## Compatibility behavior
|
||||||
|
|
||||||
|
- `/v1/*` routes are exposed without `/api` prefix (extension-compatible).
|
||||||
|
- `GET /v1/emojis` accepts both `q` and `query`.
|
||||||
|
- Responses include `X-Dewemoji-Tier` header.
|
||||||
|
|
||||||
|
## Current license mode
|
||||||
|
|
||||||
|
Environment-driven stub:
|
||||||
|
|
||||||
|
- `DEWEMOJI_LICENSE_ACCEPT_ALL=true` => any key is accepted as Pro.
|
||||||
|
- `DEWEMOJI_PRO_KEYS=key1,key2` => explicit key whitelist mode.
|
||||||
|
|
||||||
|
## Data source
|
||||||
|
|
||||||
|
Configured via:
|
||||||
|
|
||||||
|
- `DEWEMOJI_DATA_PATH` (defaults to `../../dewemoji-api/data/emojis.json` from `app/`).
|
||||||
|
|
||||||
|
## Test coverage
|
||||||
|
|
||||||
|
Added feature tests:
|
||||||
|
|
||||||
|
- `app/tests/Feature/ApiV1EndpointsTest.php`
|
||||||
|
- fixture: `app/tests/Fixtures/emojis.fixture.json`
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
php artisan test --filter=ApiV1EndpointsTest
|
||||||
|
```
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
- [x] Decide canonical data source: start from `dewemoji-api/data/emojis.json`.
|
- [x] Decide canonical data source: start from `dewemoji-api/data/emojis.json`.
|
||||||
|
|
||||||
### Phase 2 - API parity (extension first)
|
### Phase 2 - API parity (extension first)
|
||||||
- [ ] Implement `GET /v1/emojis`.
|
- [x] Implement `GET /v1/emojis`.
|
||||||
- [ ] Implement `GET /v1/categories`.
|
- [x] Implement `GET /v1/categories`.
|
||||||
- [ ] Implement `POST /v1/license/verify`.
|
- [x] Implement `POST /v1/license/verify`.
|
||||||
- [ ] Preserve response/header compatibility (`X-Dewemoji-Tier`).
|
- [x] Preserve response/header compatibility (`X-Dewemoji-Tier`).
|
||||||
- [ ] Support both `q` and `query` inputs.
|
- [x] Support both `q` and `query` inputs.
|
||||||
|
|
||||||
### Phase 3 - Website rebuild
|
### Phase 3 - Website rebuild
|
||||||
- [ ] Build website pages in new app (index, emoji detail, api docs, legal pages).
|
- [ ] Build website pages in new app (index, emoji detail, api docs, legal pages).
|
||||||
@@ -51,3 +51,9 @@
|
|||||||
- Backend tier validation stubs in legacy PHP.
|
- Backend tier validation stubs in legacy PHP.
|
||||||
- Contract differences between current `/api/*` and extension `/v1/*`.
|
- Contract differences between current `/api/*` and extension `/v1/*`.
|
||||||
- Website source currently split (`dewemoji-api` active pages vs `dewemoji-site` empty scaffold).
|
- Website source currently split (`dewemoji-api` active pages vs `dewemoji-site` empty scaffold).
|
||||||
|
|
||||||
|
## Implementation notes (Phase 2)
|
||||||
|
|
||||||
|
- Routes are now available at `/v1/*` (no `/api` prefix) for extension compatibility.
|
||||||
|
- License verification is currently environment-driven (`DEWEMOJI_LICENSE_ACCEPT_ALL` / `DEWEMOJI_PRO_KEYS`) as a safe stub before real provider integration.
|
||||||
|
- Test coverage added for `v1` endpoints in `app/tests/Feature/ApiV1EndpointsTest.php`.
|
||||||
|
|||||||
Reference in New Issue
Block a user