Compare commits
2 Commits
88218c7798
...
9937da6a7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9937da6a7b | ||
|
|
c3ce549264 |
@@ -18,9 +18,10 @@ class AdminAnalyticsController extends Controller
|
||||
{
|
||||
$token = (string) config('dewemoji.admin.token', '');
|
||||
$provided = trim((string) $request->header('X-Admin-Token', ''));
|
||||
if ($token === '' || $provided === '' || !hash_equals($token, $provided)) {
|
||||
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class AdminPricingController extends Controller
|
||||
{
|
||||
$adminToken = (string) config('dewemoji.admin.token', '');
|
||||
$provided = trim((string) $request->header('X-Admin-Token', ''));
|
||||
if ($adminToken === '' || $provided === '' || !hash_equals($adminToken, $provided)) {
|
||||
if ($adminToken === '' || $provided === '' || ! hash_equals($adminToken, $provided)) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,17 +9,16 @@ use Illuminate\Http\Request;
|
||||
|
||||
class AdminSettingsController extends Controller
|
||||
{
|
||||
public function __construct(private readonly SettingsService $settings)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly SettingsService $settings) {}
|
||||
|
||||
private function authorizeAdmin(Request $request): ?JsonResponse
|
||||
{
|
||||
$token = (string) config('dewemoji.admin.token', '');
|
||||
$provided = trim((string) $request->header('X-Admin-Token', ''));
|
||||
if ($token === '' || $provided === '' || !hash_equals($token, $provided)) {
|
||||
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,7 +45,7 @@ class AdminSettingsController extends Controller
|
||||
}
|
||||
|
||||
$payload = $request->input('settings');
|
||||
if (!is_array($payload)) {
|
||||
if (! is_array($payload)) {
|
||||
return response()->json(['ok' => false, 'error' => 'invalid_payload'], 422);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,16 @@ class AdminSubscriptionController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
private function authorizeAdmin(Request $request): ?JsonResponse
|
||||
{
|
||||
$token = (string) config('dewemoji.admin.token', '');
|
||||
$provided = trim((string) $request->header('X-Admin-Token', ''));
|
||||
if ($token === '' || $provided === '' || !hash_equals($token, $provided)) {
|
||||
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class AdminSubscriptionController extends Controller
|
||||
}
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
@@ -99,16 +99,17 @@ class AdminSubscriptionController extends Controller
|
||||
|
||||
if ($id > 0) {
|
||||
$sub = Subscription::find($id);
|
||||
if (!$sub) {
|
||||
if (! $sub) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
$sub->update(['status' => 'revoked', 'expires_at' => $now]);
|
||||
$this->syncUserTier($sub->user_id);
|
||||
|
||||
return response()->json(['ok' => true, 'revoked' => true]);
|
||||
}
|
||||
|
||||
$user = $this->resolveUser($request);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
@@ -135,7 +136,7 @@ class AdminSubscriptionController extends Controller
|
||||
|
||||
private function parseDate(mixed $value): ?Carbon
|
||||
{
|
||||
if (!$value) {
|
||||
if (! $value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
@@ -159,7 +160,7 @@ class AdminSubscriptionController extends Controller
|
||||
'tier' => $active ? 'personal' : 'free',
|
||||
]);
|
||||
$this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free');
|
||||
if (!$active) {
|
||||
if (! $active) {
|
||||
UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ class AdminUserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
private function authorizeAdmin(Request $request): ?JsonResponse
|
||||
{
|
||||
$adminToken = (string) config('dewemoji.admin.token', '');
|
||||
$provided = trim((string) $request->header('X-Admin-Token', ''));
|
||||
if ($adminToken === '' || $provided === '' || !hash_equals($adminToken, $provided)) {
|
||||
if ($adminToken === '' || $provided === '' || ! hash_equals($adminToken, $provided)) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
@@ -71,7 +70,7 @@ class AdminUserController extends Controller
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = $query->first();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
@@ -100,9 +99,9 @@ class AdminUserController extends Controller
|
||||
]);
|
||||
|
||||
$query = User::query();
|
||||
if (!empty($data['email'])) {
|
||||
if (! empty($data['email'])) {
|
||||
$query->where('email', $data['email']);
|
||||
} elseif (!empty($data['user_id'])) {
|
||||
} elseif (! empty($data['user_id'])) {
|
||||
$query->where('id', $data['user_id']);
|
||||
} else {
|
||||
return response()->json(['ok' => false, 'error' => 'missing_target'], 400);
|
||||
@@ -110,7 +109,7 @@ class AdminUserController extends Controller
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = $query->first();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,16 @@ use Illuminate\Http\Request;
|
||||
|
||||
class AdminWebhookController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PaypalWebhookProcessor $processor)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly PaypalWebhookProcessor $processor) {}
|
||||
|
||||
private function authorizeAdmin(Request $request): ?JsonResponse
|
||||
{
|
||||
$token = (string) config('dewemoji.admin.token', '');
|
||||
$provided = trim((string) $request->header('X-Admin-Token', ''));
|
||||
if ($token === '' || $provided === '' || !hash_equals($token, $provided)) {
|
||||
if ($token === '' || $provided === '' || ! hash_equals($token, $provided)) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ class AdminWebhookController extends Controller
|
||||
}
|
||||
|
||||
$item = WebhookEvent::find($id);
|
||||
if (!$item) {
|
||||
if (! $item) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ class AdminWebhookController extends Controller
|
||||
}
|
||||
|
||||
$item = WebhookEvent::find($id);
|
||||
if (!$item) {
|
||||
if (! $item) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Auth\ApiKeyService;
|
||||
use App\Services\System\SettingsService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -13,6 +13,7 @@ use RuntimeException;
|
||||
class EmojiApiController extends Controller
|
||||
{
|
||||
private const TIER_FREE = 'free';
|
||||
|
||||
private const TIER_PRO = 'pro';
|
||||
|
||||
/** @var array<string,mixed>|null */
|
||||
@@ -20,8 +21,7 @@ class EmojiApiController extends Controller
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiKeyService $apiKeys
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/** @var array<string,string> */
|
||||
private const CATEGORY_MAP = [
|
||||
@@ -149,7 +149,7 @@ class EmojiApiController extends Controller
|
||||
}
|
||||
|
||||
$sourceItem = $itemsBySlug[$slug] ?? null;
|
||||
if (!$sourceItem) {
|
||||
if (! $sourceItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -213,12 +213,12 @@ class EmojiApiController extends Controller
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
$private = filter_var($request->query('private', false), FILTER_VALIDATE_BOOL);
|
||||
if (!$private) {
|
||||
if (! $private) {
|
||||
return $this->emojis($request);
|
||||
}
|
||||
|
||||
$user = $this->apiKeys->resolveUser($request) ?? $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
$q = trim((string) ($request->query('q', $request->query('query', ''))));
|
||||
@@ -413,6 +413,7 @@ class EmojiApiController extends Controller
|
||||
private function isNotModified(Request $request, string $etag): bool
|
||||
{
|
||||
$ifNoneMatch = trim((string) $request->header('If-None-Match', ''));
|
||||
|
||||
return $ifNoneMatch !== '' && $ifNoneMatch === $etag;
|
||||
}
|
||||
|
||||
@@ -431,7 +432,7 @@ class EmojiApiController extends Controller
|
||||
? $activePath
|
||||
: (string) config('dewemoji.data_path');
|
||||
|
||||
if (!is_file($path)) {
|
||||
if (! is_file($path)) {
|
||||
throw new RuntimeException('Emoji dataset file was not found at: '.$path);
|
||||
}
|
||||
|
||||
@@ -441,11 +442,12 @@ class EmojiApiController extends Controller
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
if (! is_array($decoded)) {
|
||||
throw new RuntimeException('Emoji dataset JSON is invalid.');
|
||||
}
|
||||
|
||||
self::$dataset = $decoded;
|
||||
|
||||
return self::$dataset;
|
||||
}
|
||||
|
||||
@@ -464,6 +466,7 @@ class EmojiApiController extends Controller
|
||||
$value = strtolower(trim($text));
|
||||
$value = str_replace('&', 'and', $value);
|
||||
$value = preg_replace('/[^a-z0-9]+/', '-', $value) ?? '';
|
||||
|
||||
return trim($value, '-');
|
||||
}
|
||||
|
||||
@@ -506,10 +509,11 @@ class EmojiApiController extends Controller
|
||||
if ($token === '') {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($haystack, $token)) {
|
||||
if (! str_contains($haystack, $token)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
@@ -583,7 +587,7 @@ class EmojiApiController extends Controller
|
||||
return $text;
|
||||
}
|
||||
|
||||
return rtrim(mb_substr($text, 0, $max - 1), " ,.;:-").'…';
|
||||
return rtrim(mb_substr($text, 0, $max - 1), ' ,.;:-').'…';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,7 @@ use Illuminate\Http\Request;
|
||||
|
||||
class ExtensionController extends Controller
|
||||
{
|
||||
public function __construct(private readonly ExtensionVerificationService $verifier)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly ExtensionVerificationService $verifier) {}
|
||||
|
||||
public function verify(Request $request): JsonResponse
|
||||
{
|
||||
|
||||
@@ -10,9 +10,7 @@ use Illuminate\Http\Request;
|
||||
|
||||
class PaypalWebhookController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PaypalWebhookProcessor $processor)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly PaypalWebhookProcessor $processor) {}
|
||||
|
||||
public function handle(Request $request): JsonResponse
|
||||
{
|
||||
@@ -37,7 +35,7 @@ class PaypalWebhookController extends Controller
|
||||
'error' => $signatureOk ? null : 'signature_unverified',
|
||||
]);
|
||||
|
||||
if (!$signatureOk) {
|
||||
if (! $signatureOk) {
|
||||
return response()->json(['ok' => true, 'signature' => 'unverified']);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class SystemController extends Controller
|
||||
|
||||
public function metricsLite(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->metricsEnabled()) {
|
||||
if (! $this->metricsEnabled()) {
|
||||
return $this->response($request, [
|
||||
'ok' => false,
|
||||
'error' => 'metrics_disabled',
|
||||
@@ -39,14 +39,14 @@ class SystemController extends Controller
|
||||
|
||||
public function metrics(Request $request): JsonResponse
|
||||
{
|
||||
if (!$this->metricsEnabled()) {
|
||||
if (! $this->metricsEnabled()) {
|
||||
return $this->response($request, [
|
||||
'ok' => false,
|
||||
'error' => 'metrics_disabled',
|
||||
], 404);
|
||||
}
|
||||
|
||||
if (!$this->canAccessMetrics($request)) {
|
||||
if (! $this->canAccessMetrics($request)) {
|
||||
return $this->response($request, [
|
||||
'ok' => false,
|
||||
'error' => 'forbidden',
|
||||
@@ -83,6 +83,7 @@ class SystemController extends Controller
|
||||
$key = 'dw_metrics_ping';
|
||||
Cache::put($key, 'ok', 60);
|
||||
$val = Cache::get($key);
|
||||
|
||||
return $val === 'ok' ? 'ok' : 'degraded';
|
||||
} catch (\Throwable) {
|
||||
return 'down';
|
||||
|
||||
@@ -15,8 +15,7 @@ class UserController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ApiKeyService $keys
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
@@ -57,7 +56,7 @@ class UserController extends Controller
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
if (!$user || !Hash::check($data['password'], $user->password)) {
|
||||
if (! $user || ! Hash::check($data['password'], $user->password)) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'error' => 'invalid_credentials',
|
||||
@@ -96,7 +95,7 @@ class UserController extends Controller
|
||||
public function listApiKeys(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->keys->resolveUser($request);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@ class UserController extends Controller
|
||||
public function createApiKey(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->keys->resolveUser($request);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
if ((string) $user->tier !== 'personal') {
|
||||
@@ -149,7 +148,7 @@ class UserController extends Controller
|
||||
public function revokeApiKey(Request $request, string $key): JsonResponse
|
||||
{
|
||||
$user = $this->keys->resolveUser($request);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['ok' => false, 'error' => 'unauthorized'], 401);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,12 @@ class UserKeywordController extends Controller
|
||||
public function __construct(
|
||||
private readonly ApiKeyService $keys,
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
private function ensureUser(Request $request): ?array
|
||||
{
|
||||
$user = $this->keys->resolveUser($request);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return ['error' => 'unauthorized', 'status' => 401];
|
||||
}
|
||||
|
||||
@@ -30,7 +29,7 @@ class UserKeywordController extends Controller
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -45,7 +44,7 @@ class UserKeywordController extends Controller
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ class UserKeywordController extends Controller
|
||||
$targetActive = $existing ? (bool) $existing->is_active : true;
|
||||
$limit = $this->keywordLimitFor($user);
|
||||
if ($limit !== null) {
|
||||
if (!$existing && $targetActive) {
|
||||
if (! $existing && $targetActive) {
|
||||
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
|
||||
if ($activeCount >= $limit) {
|
||||
return response()->json(['ok' => false, 'error' => 'free_active_limit_reached', 'limit' => $limit], 403);
|
||||
@@ -93,7 +92,7 @@ class UserKeywordController extends Controller
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -107,7 +106,7 @@ class UserKeywordController extends Controller
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@ class UserKeywordController extends Controller
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
if (!$item) {
|
||||
if (! $item) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
@@ -147,7 +146,7 @@ class UserKeywordController extends Controller
|
||||
public function export(Request $request): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -161,7 +160,7 @@ class UserKeywordController extends Controller
|
||||
public function import(Request $request): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -184,8 +183,9 @@ class UserKeywordController extends Controller
|
||||
->first();
|
||||
$targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL);
|
||||
|
||||
if (!$existing && $targetActive && $limit !== null && $activeCount >= $limit) {
|
||||
if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) {
|
||||
$skipped += 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ class UserKeywordController extends Controller
|
||||
public function toggleActive(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$check = $this->ensureUser($request);
|
||||
if (!isset($check['user'])) {
|
||||
if (! isset($check['user'])) {
|
||||
return response()->json(['ok' => false, 'error' => $check['error']], $check['status']);
|
||||
}
|
||||
|
||||
@@ -224,13 +224,13 @@ class UserKeywordController extends Controller
|
||||
|
||||
$user = $check['user'];
|
||||
$item = UserKeyword::where('user_id', $user->id)->where('id', $id)->first();
|
||||
if (!$item) {
|
||||
if (! $item) {
|
||||
return response()->json(['ok' => false, 'error' => 'not_found'], 404);
|
||||
}
|
||||
|
||||
if ((bool) $data['is_active'] && ($limit = $this->keywordLimitFor($user))) {
|
||||
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
|
||||
if (!$item->is_active && $activeCount >= $limit) {
|
||||
if (! $item->is_active && $activeCount >= $limit) {
|
||||
return response()->json(['ok' => false, 'error' => 'free_active_limit_reached', 'limit' => $limit], 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
@@ -12,7 +12,7 @@ class BillingPaymentController extends Controller
|
||||
public function resume(Request $request, Payment $payment): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user || (int) $payment->user_id !== (int) $user->id) {
|
||||
if (! $user || (int) $payment->user_id !== (int) $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class BillingPaymentController extends Controller
|
||||
}
|
||||
|
||||
$provider = strtolower((string) $payment->provider);
|
||||
|
||||
return match ($provider) {
|
||||
'paypal' => $this->resumePayPal($payment),
|
||||
'pakasir' => $this->resumePakasir($payment),
|
||||
@@ -34,7 +35,7 @@ class BillingPaymentController extends Controller
|
||||
$links = is_array($raw['links'] ?? null) ? $raw['links'] : [];
|
||||
$approveUrl = null;
|
||||
foreach ($links as $link) {
|
||||
if (!is_array($link)) {
|
||||
if (! is_array($link)) {
|
||||
continue;
|
||||
}
|
||||
if ((string) ($link['rel'] ?? '') === 'approve') {
|
||||
|
||||
@@ -22,8 +22,7 @@ class PakasirController extends Controller
|
||||
public function __construct(
|
||||
private readonly SubscriptionTransitionService $subscriptionTransition,
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function createTransaction(Request $request): JsonResponse
|
||||
{
|
||||
@@ -32,7 +31,7 @@ class PakasirController extends Controller
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'auth_required'], 401);
|
||||
}
|
||||
|
||||
@@ -51,7 +50,7 @@ class PakasirController extends Controller
|
||||
$project = (string) ($config['project'] ?? '');
|
||||
$timeout = (int) ($config['timeout'] ?? 10);
|
||||
|
||||
if (!$enabled || $apiBase === '' || $apiKey === '' || $project === '') {
|
||||
if (! $enabled || $apiBase === '' || $apiKey === '' || $project === '') {
|
||||
return response()->json(['error' => 'pakasir_not_configured'], 422);
|
||||
}
|
||||
|
||||
@@ -84,25 +83,27 @@ class PakasirController extends Controller
|
||||
$endpoint = $apiBase.'/api/transactioncreate/qris';
|
||||
// Pakasir expects form payloads; keep JSON as fallback for provider-side variations.
|
||||
$res = Http::asForm()->timeout($timeout)->post($endpoint, $payload);
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
$res = Http::timeout($timeout)->post($endpoint, $payload);
|
||||
}
|
||||
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
Log::warning('Pakasir create transaction failed', [
|
||||
'status' => $res->status(),
|
||||
'endpoint' => $endpoint,
|
||||
'body' => $res->body(),
|
||||
]);
|
||||
|
||||
return response()->json(['error' => 'pakasir_create_failed'], 502);
|
||||
}
|
||||
|
||||
$body = $res->json();
|
||||
if (!is_array($body)) {
|
||||
if (! is_array($body)) {
|
||||
Log::warning('Pakasir create transaction invalid response', [
|
||||
'endpoint' => $endpoint,
|
||||
'body' => $res->body(),
|
||||
]);
|
||||
|
||||
return response()->json(['error' => 'pakasir_invalid_response'], 502);
|
||||
}
|
||||
|
||||
@@ -112,6 +113,7 @@ class PakasirController extends Controller
|
||||
'endpoint' => $endpoint,
|
||||
'body' => $body,
|
||||
]);
|
||||
|
||||
return response()->json(['error' => 'pakasir_invalid_response'], 502);
|
||||
}
|
||||
|
||||
@@ -210,7 +212,7 @@ class PakasirController extends Controller
|
||||
->where('status', 'pending')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
if (!$pending || !$pending->created_at) {
|
||||
if (! $pending || ! $pending->created_at) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -232,7 +234,7 @@ class PakasirController extends Controller
|
||||
public function cancelPending(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'auth_required'], 401);
|
||||
}
|
||||
|
||||
@@ -247,7 +249,7 @@ class PakasirController extends Controller
|
||||
}
|
||||
|
||||
$order = $orderQuery->orderByDesc('id')->first();
|
||||
if (!$order) {
|
||||
if (! $order) {
|
||||
return response()->json(['ok' => true, 'canceled' => false, 'cancelled' => false]);
|
||||
}
|
||||
|
||||
@@ -264,7 +266,7 @@ class PakasirController extends Controller
|
||||
public function paymentStatus(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'auth_required'], 401);
|
||||
}
|
||||
|
||||
@@ -275,7 +277,7 @@ class PakasirController extends Controller
|
||||
}
|
||||
|
||||
$order = $query->orderByDesc('id')->first();
|
||||
if (!$order) {
|
||||
if (! $order) {
|
||||
return response()->json(['ok' => true, 'found' => false, 'status' => null, 'paid' => false]);
|
||||
}
|
||||
|
||||
@@ -319,11 +321,12 @@ class PakasirController extends Controller
|
||||
}
|
||||
|
||||
$status = strtolower((string) ($data['status'] ?? $payload['status'] ?? ''));
|
||||
if (!in_array($status, ['paid', 'success', 'settlement', 'completed'], true)) {
|
||||
if (! in_array($status, ['paid', 'success', 'settlement', 'completed'], true)) {
|
||||
Log::info('Pakasir webhook ignored: status not paid', [
|
||||
'status' => $status,
|
||||
'payload_status' => $payload['status'] ?? null,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,17 +341,19 @@ class PakasirController extends Controller
|
||||
));
|
||||
if ($orderId === '') {
|
||||
Log::warning('Pakasir webhook paid event missing order reference', ['payload' => $payload]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$order = Order::where('provider', 'pakasir')->where('provider_ref', $orderId)->first();
|
||||
if (!$order) {
|
||||
if (! $order) {
|
||||
if (preg_match('/^DW-(\d+)-/', $orderId, $matches) === 1) {
|
||||
$order = Order::where('provider', 'pakasir')->where('id', (int) $matches[1])->first();
|
||||
}
|
||||
}
|
||||
if (!$order) {
|
||||
if (! $order) {
|
||||
Log::warning('Pakasir webhook order not found', ['order_id' => $orderId, 'payload' => $payload]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,7 +361,7 @@ class PakasirController extends Controller
|
||||
Payment::where('order_id', $order->id)->update(['status' => 'paid']);
|
||||
|
||||
$user = User::find($order->user_id);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -400,7 +405,7 @@ class PakasirController extends Controller
|
||||
|
||||
$defaults = collect(config('dewemoji.pricing.defaults', []))->keyBy('code');
|
||||
$fallback = $defaults->get($planCode);
|
||||
if (!$fallback) {
|
||||
if (! $fallback) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PayPalController extends Controller
|
||||
@@ -25,8 +24,7 @@ class PayPalController extends Controller
|
||||
public function __construct(
|
||||
private readonly SubscriptionTransitionService $subscriptionTransition,
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function createSubscription(Request $request): RedirectResponse|JsonResponse
|
||||
{
|
||||
@@ -35,7 +33,7 @@ class PayPalController extends Controller
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'auth_required'], 401);
|
||||
}
|
||||
|
||||
@@ -48,7 +46,7 @@ class PayPalController extends Controller
|
||||
}
|
||||
|
||||
$mode = $this->resolvePaypalMode($this->billingMode());
|
||||
if (!$this->paypalConfigured($mode)) {
|
||||
if (! $this->paypalConfigured($mode)) {
|
||||
return response()->json(['error' => 'paypal_not_configured'], 422);
|
||||
}
|
||||
|
||||
@@ -57,12 +55,12 @@ class PayPalController extends Controller
|
||||
}
|
||||
|
||||
$planId = $this->resolvePlanId($data['plan_code'], $mode);
|
||||
if (!$planId) {
|
||||
if (! $planId) {
|
||||
return response()->json(['error' => 'paypal_plan_missing'], 422);
|
||||
}
|
||||
|
||||
$token = $this->getAccessToken($mode);
|
||||
if (!$token) {
|
||||
if (! $token) {
|
||||
return response()->json(['error' => 'paypal_auth_failed'], 502);
|
||||
}
|
||||
|
||||
@@ -94,8 +92,8 @@ class PayPalController extends Controller
|
||||
$subscriptionId = $body['id'] ?? null;
|
||||
$approveUrl = collect($body['links'] ?? [])->firstWhere('rel', 'approve')['href'] ?? null;
|
||||
|
||||
if (!$subscriptionId || !$approveUrl) {
|
||||
if (!$res->ok()) {
|
||||
if (! $subscriptionId || ! $approveUrl) {
|
||||
if (! $res->ok()) {
|
||||
Log::warning('PayPal create subscription failed', [
|
||||
'status' => $res->status(),
|
||||
'body' => $res->body(),
|
||||
@@ -106,6 +104,7 @@ class PayPalController extends Controller
|
||||
'body' => $res->body(),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['error' => 'paypal_invalid_response'], 502);
|
||||
}
|
||||
|
||||
@@ -156,13 +155,14 @@ class PayPalController extends Controller
|
||||
$token = trim((string) $request->query('token', ''));
|
||||
if ($token !== '') {
|
||||
$captured = $this->captureLifetimeOrder($token, $request->user()?->id);
|
||||
if (!$captured) {
|
||||
if (! $captured) {
|
||||
return redirect()->route('dashboard.billing', ['status' => 'error']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$status = (string) $request->query('status', 'success');
|
||||
|
||||
return redirect()->route('dashboard.billing', ['status' => $status]);
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ class PayPalController extends Controller
|
||||
|
||||
if ($webhookId) {
|
||||
$verified = $this->verifySignature($mode, $webhookId, $payload, $request);
|
||||
if (!$verified) {
|
||||
if (! $verified) {
|
||||
return response()->json(['error' => 'invalid_signature'], 401);
|
||||
}
|
||||
}
|
||||
@@ -223,14 +223,14 @@ class PayPalController extends Controller
|
||||
'provider' => 'paypal',
|
||||
'provider_ref' => $subscriptionId,
|
||||
]);
|
||||
if (!$sub->user_id) {
|
||||
if (! $sub->user_id) {
|
||||
$order = Order::where('provider', 'paypal')->where('provider_ref', $subscriptionId)->first();
|
||||
if ($order) {
|
||||
$sub->user_id = $order->user_id;
|
||||
$resolvedPlan = (string) ($order->plan_code ?: $resolvedPlan);
|
||||
}
|
||||
}
|
||||
if (!empty($sub->plan)) {
|
||||
if (! empty($sub->plan)) {
|
||||
$resolvedPlan = (string) $sub->plan;
|
||||
}
|
||||
$sub->plan = $resolvedPlan;
|
||||
@@ -267,6 +267,7 @@ class PayPalController extends Controller
|
||||
$sub->canceled_at = now();
|
||||
$sub->save();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -311,7 +312,7 @@ class PayPalController extends Controller
|
||||
->where('status', 'pending')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
if (!$pending || !$pending->created_at) {
|
||||
if (! $pending || ! $pending->created_at) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -333,20 +334,21 @@ class PayPalController extends Controller
|
||||
private function resolvePlanAmountUsd(string $planCode): int
|
||||
{
|
||||
$plan = PricingPlan::where('code', $planCode)->first();
|
||||
if (!$plan) {
|
||||
if (! $plan) {
|
||||
return 0;
|
||||
}
|
||||
$rate = (int) config('dewemoji.pricing.usd_rate', 15000);
|
||||
if ($rate <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) round($plan->amount / $rate);
|
||||
}
|
||||
|
||||
private function resolvePlanAmountUsdValue(string $planCode): string
|
||||
{
|
||||
$plan = PricingPlan::where('code', $planCode)->first();
|
||||
if (!$plan) {
|
||||
if (! $plan) {
|
||||
return '0.00';
|
||||
}
|
||||
$rate = (int) config('dewemoji.pricing.usd_rate', 15000);
|
||||
@@ -363,7 +365,7 @@ class PayPalController extends Controller
|
||||
$clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret");
|
||||
$apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base");
|
||||
|
||||
if (!$clientId || !$clientSecret || !$apiBase) {
|
||||
if (! $clientId || ! $clientSecret || ! $apiBase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -374,7 +376,7 @@ class PayPalController extends Controller
|
||||
'grant_type' => 'client_credentials',
|
||||
]);
|
||||
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -394,6 +396,7 @@ class PayPalController extends Controller
|
||||
private function billingMode(): string
|
||||
{
|
||||
$settings = app(SettingsService::class);
|
||||
|
||||
return (string) ($settings->get('billing_mode', config('dewemoji.billing.mode', 'sandbox')) ?: 'sandbox');
|
||||
}
|
||||
|
||||
@@ -414,7 +417,7 @@ class PayPalController extends Controller
|
||||
private function verifySignature(string $mode, string $webhookId, array $payload, Request $request): bool
|
||||
{
|
||||
$token = $this->getAccessToken($mode);
|
||||
if (!$token) {
|
||||
if (! $token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -443,12 +446,12 @@ class PayPalController extends Controller
|
||||
private function createLifetimeOrder(Request $request, string $mode): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return response()->json(['error' => 'auth_required'], 401);
|
||||
}
|
||||
|
||||
$token = $this->getAccessToken($mode);
|
||||
if (!$token) {
|
||||
if (! $token) {
|
||||
return response()->json(['error' => 'paypal_auth_failed'], 502);
|
||||
}
|
||||
|
||||
@@ -487,7 +490,7 @@ class PayPalController extends Controller
|
||||
$body = $res->json();
|
||||
$orderId = (string) ($body['id'] ?? '');
|
||||
$approveUrl = collect($body['links'] ?? [])->firstWhere('rel', 'approve')['href'] ?? null;
|
||||
if ($orderId === '' || !$approveUrl) {
|
||||
if ($orderId === '' || ! $approveUrl) {
|
||||
Log::warning('PayPal create lifetime order failed', [
|
||||
'status' => $res->status(),
|
||||
'body' => $res->body(),
|
||||
@@ -529,7 +532,7 @@ class PayPalController extends Controller
|
||||
{
|
||||
$mode = $this->resolvePaypalMode($this->billingMode());
|
||||
$token = $this->getAccessToken($mode);
|
||||
if (!$token) {
|
||||
if (! $token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -537,7 +540,7 @@ class PayPalController extends Controller
|
||||
->where('provider_ref', $orderId)
|
||||
->where('type', 'one_time')
|
||||
->first();
|
||||
if (!$order) {
|
||||
if (! $order) {
|
||||
return false;
|
||||
}
|
||||
if ($userId !== null && (int) $order->user_id !== $userId) {
|
||||
@@ -581,7 +584,7 @@ class PayPalController extends Controller
|
||||
->where('provider_ref', $orderId)
|
||||
->where('type', 'one_time')
|
||||
->first();
|
||||
if (!$order) {
|
||||
if (! $order) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -594,7 +597,7 @@ class PayPalController extends Controller
|
||||
->first();
|
||||
if ($payment) {
|
||||
$payment->status = 'paid';
|
||||
if (!empty($rawPayload)) {
|
||||
if (! empty($rawPayload)) {
|
||||
$payment->raw_payload = $rawPayload;
|
||||
}
|
||||
$payment->save();
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
namespace App\Http\Controllers\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PricingChange;
|
||||
use App\Models\PricingPlan;
|
||||
use App\Models\AdminAuditLog;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use App\Models\PricingChange;
|
||||
use App\Models\PricingPlan;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\User;
|
||||
use App\Models\UserApiKey;
|
||||
use App\Models\UserKeyword;
|
||||
use App\Models\WebhookEvent;
|
||||
use App\Models\AdminAuditLog;
|
||||
use App\Services\Billing\PayPalPlanSyncService;
|
||||
use App\Services\Keywords\KeywordQuotaService;
|
||||
use App\Services\System\SettingsService;
|
||||
@@ -30,9 +30,7 @@ class AdminDashboardController extends Controller
|
||||
public function __construct(
|
||||
private readonly SettingsService $settings,
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
)
|
||||
{
|
||||
}
|
||||
) {}
|
||||
|
||||
public function users(Request $request): View
|
||||
{
|
||||
@@ -206,7 +204,7 @@ class AdminDashboardController extends Controller
|
||||
]);
|
||||
|
||||
$user = $this->resolveUser($data['user_id'] ?? null, $data['email'] ?? null);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return back()->withErrors(['user' => 'User not found.']);
|
||||
}
|
||||
|
||||
@@ -246,9 +244,9 @@ class AdminDashboardController extends Controller
|
||||
'email' => 'nullable|email|max:255',
|
||||
]);
|
||||
|
||||
if (!empty($data['subscription_id'])) {
|
||||
if (! empty($data['subscription_id'])) {
|
||||
$sub = Subscription::find($data['subscription_id']);
|
||||
if (!$sub) {
|
||||
if (! $sub) {
|
||||
return back()->withErrors(['subscription' => 'Subscription not found.']);
|
||||
}
|
||||
$sub->update(['status' => 'revoked', 'expires_at' => now()]);
|
||||
@@ -257,11 +255,12 @@ class AdminDashboardController extends Controller
|
||||
'subscription_id' => $sub->id,
|
||||
'user_id' => $sub->user_id,
|
||||
]);
|
||||
|
||||
return back()->with('status', 'Subscription revoked.');
|
||||
}
|
||||
|
||||
$user = $this->resolveUser($data['user_id'] ?? null, $data['email'] ?? null);
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return back()->withErrors(['user' => 'User not found.']);
|
||||
}
|
||||
|
||||
@@ -447,7 +446,7 @@ class AdminDashboardController extends Controller
|
||||
public function replayWebhook(int $id): RedirectResponse
|
||||
{
|
||||
$event = WebhookEvent::find($id);
|
||||
if (!$event) {
|
||||
if (! $event) {
|
||||
return back()->withErrors(['webhook' => 'Webhook not found.']);
|
||||
}
|
||||
|
||||
@@ -557,7 +556,7 @@ class AdminDashboardController extends Controller
|
||||
'tier' => $active ? 'personal' : 'free',
|
||||
]);
|
||||
$this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free');
|
||||
if (!$active) {
|
||||
if (! $active) {
|
||||
UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]);
|
||||
}
|
||||
}
|
||||
@@ -565,7 +564,7 @@ class AdminDashboardController extends Controller
|
||||
public function exportCsv(Request $request, string $type): StreamedResponse
|
||||
{
|
||||
$type = strtolower($type);
|
||||
$filename = "dewemoji-{$type}-export-".now()->format('Ymd_His').".csv";
|
||||
$filename = "dewemoji-{$type}-export-".now()->format('Ymd_His').'.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($type, $request): void {
|
||||
$out = fopen('php://output', 'w');
|
||||
@@ -676,7 +675,7 @@ class AdminDashboardController extends Controller
|
||||
|
||||
private function logAdminAction(string $action, array $payload = []): void
|
||||
{
|
||||
if (!Schema::hasTable('admin_audit_logs')) {
|
||||
if (! Schema::hasTable('admin_audit_logs')) {
|
||||
return;
|
||||
}
|
||||
$user = auth()->user();
|
||||
@@ -692,6 +691,7 @@ class AdminDashboardController extends Controller
|
||||
private function sanitizeSort(mixed $value, array $allowed, string $fallback): string
|
||||
{
|
||||
$sort = is_string($value) ? $value : '';
|
||||
|
||||
return in_array($sort, $allowed, true) ? $sort : $fallback;
|
||||
}
|
||||
|
||||
@@ -706,6 +706,7 @@ class AdminDashboardController extends Controller
|
||||
private function splitCsv(string $value): array
|
||||
{
|
||||
$items = array_filter(array_map('trim', explode(',', $value)));
|
||||
|
||||
return array_values($items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,16 +10,15 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Throwable;
|
||||
use Illuminate\View\View;
|
||||
use Throwable;
|
||||
|
||||
class AdminEmojiCatalogController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EmojiCatalogService $catalog,
|
||||
private readonly SettingsService $settings
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
{
|
||||
@@ -64,6 +63,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
$emojiId = $this->catalog->saveItem($validated);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return back()->withInput()->with('error', $e->getMessage() ?: 'Failed to create catalog item.');
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
$savedId = $this->catalog->saveItem($validated);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return back()
|
||||
->withInput()
|
||||
->with('error', $e->getMessage() ?: 'Failed to save catalog item.');
|
||||
@@ -105,6 +106,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
$this->catalog->deleteItem($emojiId);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return back()->with('error', $e->getMessage() ?: 'Failed to delete catalog item.');
|
||||
}
|
||||
|
||||
@@ -123,6 +125,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
$result = $this->catalog->importFromDataFile($path);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return back()->with('error', $e->getMessage() ?: 'Failed to import dataset.');
|
||||
}
|
||||
|
||||
@@ -145,6 +148,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
$result = $this->catalog->publishSnapshot($request->user()?->email);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return back()->with('error', $e->getMessage() ?: 'Failed to publish snapshot.');
|
||||
}
|
||||
|
||||
@@ -163,6 +167,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
$result = $this->catalog->activateSnapshot($validated['snapshot'], $request->user()?->email);
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return back()->with('error', $e->getMessage() ?: 'Failed to activate snapshot.');
|
||||
}
|
||||
|
||||
@@ -177,7 +182,7 @@ class AdminEmojiCatalogController extends Controller
|
||||
private function logAdminAction(Request $request, string $action, array $payload): void
|
||||
{
|
||||
$admin = $request->user();
|
||||
if (!$admin) {
|
||||
if (! $admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Dashboard;
|
||||
|
||||
use App\Http\Controllers\Api\V1\EmojiApiController;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
@@ -10,7 +11,6 @@ use App\Models\User;
|
||||
use App\Models\UserApiKey;
|
||||
use App\Models\UserKeyword;
|
||||
use App\Models\WebhookEvent;
|
||||
use App\Http\Controllers\Api\V1\EmojiApiController;
|
||||
use App\Services\Auth\ApiKeyService;
|
||||
use App\Services\Keywords\KeywordQuotaService;
|
||||
use Carbon\Carbon;
|
||||
@@ -27,8 +27,7 @@ class UserDashboardController extends Controller
|
||||
public function __construct(
|
||||
private readonly ApiKeyService $keys,
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function overview(Request $request): View
|
||||
{
|
||||
@@ -171,7 +170,7 @@ class UserDashboardController extends Controller
|
||||
public function storeKeyword(Request $request): RedirectResponse|JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -188,7 +187,7 @@ class UserDashboardController extends Controller
|
||||
$targetActive = $existing ? (bool) $existing->is_active : true;
|
||||
|
||||
if ($limit = $this->keywordLimitFor($user)) {
|
||||
if (!$existing && $targetActive) {
|
||||
if (! $existing && $targetActive) {
|
||||
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
|
||||
if ($activeCount >= $limit) {
|
||||
return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached');
|
||||
@@ -221,7 +220,7 @@ class UserDashboardController extends Controller
|
||||
public function updateKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -261,7 +260,7 @@ class UserDashboardController extends Controller
|
||||
public function deleteKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -281,7 +280,7 @@ class UserDashboardController extends Controller
|
||||
public function importKeywords(Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -291,7 +290,7 @@ class UserDashboardController extends Controller
|
||||
}
|
||||
|
||||
$items = json_decode((string) $payload, true);
|
||||
if (!is_array($items)) {
|
||||
if (! is_array($items)) {
|
||||
return back()->withErrors(['payload' => 'Invalid JSON payload.']);
|
||||
}
|
||||
|
||||
@@ -300,7 +299,7 @@ class UserDashboardController extends Controller
|
||||
$limit = $this->keywordLimitFor($user);
|
||||
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
|
||||
foreach ($items as $row) {
|
||||
if (!is_array($row)) {
|
||||
if (! is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$emojiSlug = trim((string) ($row['emoji_slug'] ?? ''));
|
||||
@@ -315,8 +314,9 @@ class UserDashboardController extends Controller
|
||||
->first();
|
||||
$targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL);
|
||||
|
||||
if (!$existing && $targetActive && $limit !== null && $activeCount >= $limit) {
|
||||
if (! $existing && $targetActive && $limit !== null && $activeCount >= $limit) {
|
||||
$skipped += 1;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ class UserDashboardController extends Controller
|
||||
public function exportKeywords(Request $request): BinaryFileResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ class UserDashboardController extends Controller
|
||||
public function createApiKey(Request $request): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
abort(403);
|
||||
}
|
||||
if ((string) $user->tier !== 'personal') {
|
||||
@@ -405,7 +405,7 @@ class UserDashboardController extends Controller
|
||||
public function revokeApiKey(Request $request, UserApiKey $key): RedirectResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user || $key->user_id !== $user->id) {
|
||||
if (! $user || $key->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -447,7 +447,7 @@ class UserDashboardController extends Controller
|
||||
public function toggleKeywordActive(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user || $keyword->user_id !== $user->id) {
|
||||
if (! $user || $keyword->user_id !== $user->id) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
@@ -458,7 +458,7 @@ class UserDashboardController extends Controller
|
||||
$target = (bool) $data['is_active'];
|
||||
if ($target && ($limit = $this->keywordLimitFor($user))) {
|
||||
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
|
||||
if (!$keyword->is_active && $activeCount >= $limit) {
|
||||
if (! $keyword->is_active && $activeCount >= $limit) {
|
||||
return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached');
|
||||
}
|
||||
}
|
||||
@@ -483,7 +483,7 @@ class UserDashboardController extends Controller
|
||||
|
||||
private function keywordLimitFor(?User $user): ?int
|
||||
{
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
return null;
|
||||
}
|
||||
if ((string) $user->tier === 'personal') {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Redirect;
|
||||
|
||||
@@ -32,7 +32,12 @@ class SiteController extends Controller
|
||||
private function billingMode(): string
|
||||
{
|
||||
$settings = app(SettingsService::class);
|
||||
$preferred = (string) ($settings->get('billing_mode', config('dewemoji.billing.mode', 'sandbox')) ?: 'sandbox');
|
||||
$preferred =
|
||||
(string) ($settings->get(
|
||||
'billing_mode',
|
||||
config('dewemoji.billing.mode', 'sandbox'),
|
||||
) ?:
|
||||
'sandbox');
|
||||
if ($this->paypalConfiguredMode($preferred)) {
|
||||
return $preferred;
|
||||
}
|
||||
@@ -50,7 +55,9 @@ class SiteController extends Controller
|
||||
return view('site.home', [
|
||||
'initialQuery' => trim((string) $request->query('q', '')),
|
||||
'initialCategory' => trim((string) $request->query('category', '')),
|
||||
'initialSubcategory' => trim((string) $request->query('subcategory', '')),
|
||||
'initialSubcategory' => trim(
|
||||
(string) $request->query('subcategory', ''),
|
||||
),
|
||||
'canonicalPath' => '/',
|
||||
'userTier' => $request->user()?->tier,
|
||||
]);
|
||||
@@ -59,14 +66,20 @@ class SiteController extends Controller
|
||||
public function browse(Request $request): RedirectResponse|View
|
||||
{
|
||||
$cat = strtolower(trim((string) $request->query('cat', 'all')));
|
||||
if ($cat !== '' && $cat !== 'all' && array_key_exists($cat, $this->categorySlugMap())) {
|
||||
if (
|
||||
$cat !== '' &&
|
||||
$cat !== 'all' &&
|
||||
array_key_exists($cat, $this->categorySlugMap())
|
||||
) {
|
||||
return redirect('/'.$cat, 301);
|
||||
}
|
||||
|
||||
return view('site.home', [
|
||||
'initialQuery' => trim((string) $request->query('q', '')),
|
||||
'initialCategory' => trim((string) $request->query('category', '')),
|
||||
'initialSubcategory' => trim((string) $request->query('subcategory', '')),
|
||||
'initialSubcategory' => trim(
|
||||
(string) $request->query('subcategory', ''),
|
||||
),
|
||||
'canonicalPath' => '/browse',
|
||||
'userTier' => $request->user()?->tier,
|
||||
]);
|
||||
@@ -96,8 +109,10 @@ class SiteController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function categorySubcategory(string $categorySlug, string $subcategorySlug): View
|
||||
{
|
||||
public function categorySubcategory(
|
||||
string $categorySlug,
|
||||
string $subcategorySlug,
|
||||
): View {
|
||||
if ($categorySlug === 'all') {
|
||||
abort(404);
|
||||
}
|
||||
@@ -123,7 +138,7 @@ class SiteController extends Controller
|
||||
{
|
||||
$user = request()->user();
|
||||
$currencyPref = strtoupper((string) session('pricing_currency', ''));
|
||||
if (!in_array($currencyPref, ['IDR', 'USD'], true)) {
|
||||
if (! in_array($currencyPref, ['IDR', 'USD'], true)) {
|
||||
$currencyPref = $this->detectPricingCurrency(request());
|
||||
session(['pricing_currency' => $currencyPref]);
|
||||
}
|
||||
@@ -135,7 +150,8 @@ class SiteController extends Controller
|
||||
|
||||
$getPlanAmount = function (string $code) use ($plans, $fallback): int {
|
||||
$plan = $plans->get($code) ?? $fallback->get($code);
|
||||
return (int) ($plan['amount'] ?? $plan->amount ?? 0);
|
||||
|
||||
return (int) ($plan['amount'] ?? ($plan->amount ?? 0));
|
||||
};
|
||||
|
||||
$pricing = [
|
||||
@@ -151,7 +167,8 @@ class SiteController extends Controller
|
||||
];
|
||||
|
||||
foreach ($pricing as $key => $row) {
|
||||
$pricing[$key]['usd'] = $rate > 0 ? round($row['idr'] / $rate, 2) : 0;
|
||||
$pricing[$key]['usd'] =
|
||||
$rate > 0 ? round($row['idr'] / $rate, 2) : 0;
|
||||
}
|
||||
|
||||
$hasActiveLifetime = false;
|
||||
@@ -163,7 +180,8 @@ class SiteController extends Controller
|
||||
->where('plan', 'personal_lifetime')
|
||||
->where('status', 'active')
|
||||
->where(function ($query) {
|
||||
$query->whereNull('expires_at')
|
||||
$query
|
||||
->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
})
|
||||
->exists();
|
||||
@@ -171,7 +189,10 @@ class SiteController extends Controller
|
||||
->where('user_id', $user->id)
|
||||
->where('status', 'pending')
|
||||
->exists();
|
||||
$cooldown = (int) config('dewemoji.billing.pending_cooldown_seconds', 120);
|
||||
$cooldown = (int) config(
|
||||
'dewemoji.billing.pending_cooldown_seconds',
|
||||
120,
|
||||
);
|
||||
if ($cooldown > 0) {
|
||||
$latestPending = Payment::query()
|
||||
->where('user_id', $user->id)
|
||||
@@ -179,7 +200,11 @@ class SiteController extends Controller
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
if ($latestPending && $latestPending->created_at) {
|
||||
$age = max(0, now()->getTimestamp() - $latestPending->created_at->getTimestamp());
|
||||
$age = max(
|
||||
0,
|
||||
now()->getTimestamp() -
|
||||
$latestPending->created_at->getTimestamp(),
|
||||
);
|
||||
$pendingCooldownRemaining = max(0, $cooldown - $age);
|
||||
}
|
||||
}
|
||||
@@ -191,14 +216,31 @@ class SiteController extends Controller
|
||||
'pricing' => $pricing,
|
||||
'payments' => [
|
||||
'qris_url' => (string) config('dewemoji.payments.qris_url', ''),
|
||||
'paypal_url' => (string) config('dewemoji.payments.paypal_url', ''),
|
||||
'paypal_url' => (string) config(
|
||||
'dewemoji.payments.paypal_url',
|
||||
'',
|
||||
),
|
||||
],
|
||||
'pakasirEnabled' => (bool) config('dewemoji.billing.providers.pakasir.enabled', false)
|
||||
&& (string) config('dewemoji.billing.providers.pakasir.api_base', '') !== ''
|
||||
&& (string) config('dewemoji.billing.providers.pakasir.api_key', '') !== ''
|
||||
&& (string) config('dewemoji.billing.providers.pakasir.project', '') !== '',
|
||||
'pakasirEnabled' => (bool) config(
|
||||
'dewemoji.billing.providers.pakasir.enabled',
|
||||
false,
|
||||
) &&
|
||||
(string) config(
|
||||
'dewemoji.billing.providers.pakasir.api_base',
|
||||
'',
|
||||
) !== '' &&
|
||||
(string) config(
|
||||
'dewemoji.billing.providers.pakasir.api_key',
|
||||
'',
|
||||
) !== '' &&
|
||||
(string) config(
|
||||
'dewemoji.billing.providers.pakasir.project',
|
||||
'',
|
||||
) !== '',
|
||||
'paypalEnabled' => $this->paypalEnabled($this->billingMode()),
|
||||
'paypalPlans' => $this->paypalPlanAvailability($this->billingMode()),
|
||||
'paypalPlans' => $this->paypalPlanAvailability(
|
||||
$this->billingMode(),
|
||||
),
|
||||
'hasActiveLifetime' => $hasActiveLifetime,
|
||||
'hasPendingPayment' => $hasPendingPayment,
|
||||
'pendingCooldownRemaining' => $pendingCooldownRemaining,
|
||||
@@ -212,34 +254,59 @@ class SiteController extends Controller
|
||||
}
|
||||
|
||||
$fallback = $mode === 'live' ? 'sandbox' : 'live';
|
||||
|
||||
return $this->paypalConfiguredMode($fallback);
|
||||
}
|
||||
|
||||
private function paypalConfiguredMode(string $mode): bool
|
||||
{
|
||||
$enabled = (bool) config('dewemoji.billing.providers.paypal.enabled', false);
|
||||
$clientId = (string) config("dewemoji.billing.providers.paypal.{$mode}.client_id", '');
|
||||
$clientSecret = (string) config("dewemoji.billing.providers.paypal.{$mode}.client_secret", '');
|
||||
$apiBase = (string) config("dewemoji.billing.providers.paypal.{$mode}.api_base", '');
|
||||
$enabled = (bool) config(
|
||||
'dewemoji.billing.providers.paypal.enabled',
|
||||
false,
|
||||
);
|
||||
$clientId = (string) config(
|
||||
"dewemoji.billing.providers.paypal.{$mode}.client_id",
|
||||
'',
|
||||
);
|
||||
$clientSecret = (string) config(
|
||||
"dewemoji.billing.providers.paypal.{$mode}.client_secret",
|
||||
'',
|
||||
);
|
||||
$apiBase = (string) config(
|
||||
"dewemoji.billing.providers.paypal.{$mode}.api_base",
|
||||
'',
|
||||
);
|
||||
|
||||
return $enabled && $clientId !== '' && $clientSecret !== '' && $apiBase !== '';
|
||||
return $enabled &&
|
||||
$clientId !== '' &&
|
||||
$clientSecret !== '' &&
|
||||
$apiBase !== '';
|
||||
}
|
||||
|
||||
private function paypalPlanAvailability(string $mode): array
|
||||
{
|
||||
$plans = PricingPlan::whereIn('code', ['personal_monthly', 'personal_annual'])->get()->keyBy('code');
|
||||
$plans = PricingPlan::whereIn('code', [
|
||||
'personal_monthly',
|
||||
'personal_annual',
|
||||
])
|
||||
->get()
|
||||
->keyBy('code');
|
||||
|
||||
$fromDb = function (string $code) use ($plans, $mode): bool {
|
||||
$plan = $plans->get($code);
|
||||
if (!$plan) {
|
||||
if (! $plan) {
|
||||
return false;
|
||||
}
|
||||
$meta = $plan->meta ?? [];
|
||||
|
||||
return (string) ($meta['paypal'][$mode]['plan']['id'] ?? '') !== '';
|
||||
};
|
||||
|
||||
$fromEnv = function (string $code) use ($mode): bool {
|
||||
return (string) config("dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}", '') !== '';
|
||||
return (string) config(
|
||||
"dewemoji.billing.providers.paypal.plan_ids.{$mode}.{$code}",
|
||||
'',
|
||||
) !== '';
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -255,6 +322,7 @@ class SiteController extends Controller
|
||||
]);
|
||||
|
||||
session(['pricing_currency' => $data['currency']]);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
@@ -265,21 +333,32 @@ class SiteController extends Controller
|
||||
|
||||
public function download(): View
|
||||
{
|
||||
$downloadBaseUrl = rtrim((string) config('dewemoji.apk_release.public_base_url', ''), '/');
|
||||
$androidEnabled = (bool) config('dewemoji.apk_release.enabled', false) && $downloadBaseUrl !== '';
|
||||
$downloadBaseUrl = rtrim(
|
||||
(string) config('dewemoji.apk_release.public_base_url', ''),
|
||||
'/',
|
||||
);
|
||||
$androidEnabled = false;
|
||||
|
||||
return view('site.download', [
|
||||
'androidEnabled' => $androidEnabled,
|
||||
'androidVersionJsonUrl' => $androidEnabled ? $downloadBaseUrl.'/version.json' : '',
|
||||
'androidLatestApkUrl' => $androidEnabled ? $downloadBaseUrl.'/dewemoji-latest.apk' : '',
|
||||
'androidVersionJsonUrl' => $androidEnabled
|
||||
? $downloadBaseUrl.'/version.json'
|
||||
: '',
|
||||
'androidLatestApkUrl' => $androidEnabled
|
||||
? $downloadBaseUrl.'/dewemoji-latest.apk'
|
||||
: '',
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadVersionJson(Request $request): RedirectResponse|JsonResponse
|
||||
{
|
||||
public function downloadVersionJson(
|
||||
Request $request,
|
||||
): RedirectResponse|JsonResponse {
|
||||
$target = $this->apkReleaseTargetUrl('version_json');
|
||||
if ($target === '') {
|
||||
return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404);
|
||||
return response()->json(
|
||||
['ok' => false, 'error' => 'apk_release_not_configured'],
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
return redirect()->away($target, 302, [
|
||||
@@ -288,11 +367,15 @@ class SiteController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function downloadLatestApk(Request $request): RedirectResponse|JsonResponse
|
||||
{
|
||||
public function downloadLatestApk(
|
||||
Request $request,
|
||||
): RedirectResponse|JsonResponse {
|
||||
$target = $this->apkReleaseTargetUrl('latest_apk');
|
||||
if ($target === '') {
|
||||
return response()->json(['ok' => false, 'error' => 'apk_release_not_configured'], 404);
|
||||
return response()->json(
|
||||
['ok' => false, 'error' => 'apk_release_not_configured'],
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
return redirect()->away($target, 302, [
|
||||
@@ -304,10 +387,15 @@ class SiteController extends Controller
|
||||
public function assetLinks(): JsonResponse
|
||||
{
|
||||
$appId = trim((string) config('dewemoji.apk_release.app_id', ''));
|
||||
$rawFingerprints = (array) config('dewemoji.apk_release.assetlinks.fingerprints', []);
|
||||
$rawFingerprints = (array) config(
|
||||
'dewemoji.apk_release.assetlinks.fingerprints',
|
||||
[],
|
||||
);
|
||||
$fingerprints = [];
|
||||
foreach ($rawFingerprints as $fingerprint) {
|
||||
$normalized = $this->normalizeApkCertFingerprint((string) $fingerprint);
|
||||
$normalized = $this->normalizeApkCertFingerprint(
|
||||
(string) $fingerprint,
|
||||
);
|
||||
if ($normalized !== '') {
|
||||
$fingerprints[] = $normalized;
|
||||
}
|
||||
@@ -321,7 +409,8 @@ class SiteController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
return response()->json(
|
||||
[
|
||||
[
|
||||
'relation' => [
|
||||
'delegate_permission/common.handle_all_urls',
|
||||
@@ -332,10 +421,13 @@ class SiteController extends Controller
|
||||
'sha256_cert_fingerprints' => $fingerprints,
|
||||
],
|
||||
],
|
||||
], 200, [
|
||||
],
|
||||
200,
|
||||
[
|
||||
'Cache-Control' => 'no-store, no-cache, must-revalidate',
|
||||
'Pragma' => 'no-cache',
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public function privacy(): View
|
||||
@@ -350,12 +442,14 @@ class SiteController extends Controller
|
||||
|
||||
private function detectPricingCurrency(Request $request): string
|
||||
{
|
||||
$country = strtoupper((string) ($request->header('CF-IPCountry')
|
||||
?? $request->header('X-Country-Code')
|
||||
?? $request->header('X-Geo-Country')
|
||||
?? $request->header('X-Appengine-Country')
|
||||
?? $request->header('CloudFront-Viewer-Country')
|
||||
?? ''));
|
||||
$country = strtoupper(
|
||||
(string) ($request->header('CF-IPCountry') ??
|
||||
($request->header('X-Country-Code') ??
|
||||
($request->header('X-Geo-Country') ??
|
||||
($request->header('X-Appengine-Country') ??
|
||||
($request->header('CloudFront-Viewer-Country') ??
|
||||
''))))),
|
||||
);
|
||||
|
||||
return $country === 'ID' ? 'IDR' : 'USD';
|
||||
}
|
||||
@@ -363,7 +457,7 @@ class SiteController extends Controller
|
||||
public function emojiDetail(string $slug): View|Response
|
||||
{
|
||||
$dataPath = $this->datasetPath();
|
||||
if (!is_file($dataPath)) {
|
||||
if (! is_file($dataPath)) {
|
||||
abort(500, 'Emoji dataset file not found.');
|
||||
}
|
||||
|
||||
@@ -373,7 +467,7 @@ class SiteController extends Controller
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
if (! is_array($decoded)) {
|
||||
abort(500, 'Emoji dataset JSON is invalid.');
|
||||
}
|
||||
|
||||
@@ -382,7 +476,7 @@ class SiteController extends Controller
|
||||
$byEmoji = [];
|
||||
foreach ($items as $item) {
|
||||
$char = (string) ($item['emoji'] ?? '');
|
||||
if ($char !== '' && !isset($byEmoji[$char])) {
|
||||
if ($char !== '' && ! isset($byEmoji[$char])) {
|
||||
$byEmoji[$char] = $item;
|
||||
}
|
||||
if (($item['slug'] ?? '') === $slug) {
|
||||
@@ -390,7 +484,7 @@ class SiteController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if (!$match) {
|
||||
if (! $match) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
@@ -421,7 +515,8 @@ class SiteController extends Controller
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
}
|
||||
$limitReached = $keywordLimit !== null && $activeKeywordCount >= $keywordLimit;
|
||||
$limitReached =
|
||||
$keywordLimit !== null && $activeKeywordCount >= $keywordLimit;
|
||||
|
||||
return view('site.emoji-detail', [
|
||||
'emoji' => $match,
|
||||
@@ -438,28 +533,64 @@ class SiteController extends Controller
|
||||
|
||||
public function robotsTxt(): Response
|
||||
{
|
||||
$base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/');
|
||||
$body = "User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n";
|
||||
$base = rtrim(
|
||||
config('app.url', request()->getSchemeAndHttpHost()),
|
||||
'/',
|
||||
);
|
||||
$body =
|
||||
"User-agent: *\nAllow: /\n\nSitemap: ".$base."/sitemap.xml\n";
|
||||
|
||||
return response($body, 200)->header('Content-Type', 'text/plain; charset=UTF-8');
|
||||
return response($body, 200)->header(
|
||||
'Content-Type',
|
||||
'text/plain; charset=UTF-8',
|
||||
);
|
||||
}
|
||||
|
||||
public function sitemapXml(): Response
|
||||
{
|
||||
$data = $this->loadDataset();
|
||||
$items = is_array($data['emojis'] ?? null) ? $data['emojis'] : [];
|
||||
$base = rtrim(config('app.url', request()->getSchemeAndHttpHost()), '/');
|
||||
$base = rtrim(
|
||||
config('app.url', request()->getSchemeAndHttpHost()),
|
||||
'/',
|
||||
);
|
||||
|
||||
$lastUpdatedTs = isset($data['last_updated_ts']) ? (int) $data['last_updated_ts'] : time();
|
||||
$lastUpdated = gmdate('Y-m-d\TH:i:s\Z', $lastUpdatedTs);
|
||||
$lastUpdatedTs = isset($data['last_updated_ts'])
|
||||
? (int) $data['last_updated_ts']
|
||||
: time();
|
||||
$lastUpdated = gmdate("Y-m-d\TH:i:s\Z", $lastUpdatedTs);
|
||||
|
||||
$urls = [
|
||||
['loc' => $base.'/', 'priority' => '0.8', 'changefreq' => 'daily'],
|
||||
['loc' => $base.'/api-docs', 'priority' => '0.5', 'changefreq' => 'weekly'],
|
||||
['loc' => $base.'/pricing', 'priority' => '0.7', 'changefreq' => 'weekly'],
|
||||
['loc' => $base.'/privacy', 'priority' => '0.3', 'changefreq' => 'monthly'],
|
||||
['loc' => $base.'/terms', 'priority' => '0.3', 'changefreq' => 'monthly'],
|
||||
['loc' => $base.'/support', 'priority' => '0.4', 'changefreq' => 'weekly'],
|
||||
[
|
||||
'loc' => $base.'/',
|
||||
'priority' => '0.8',
|
||||
'changefreq' => 'daily',
|
||||
],
|
||||
[
|
||||
'loc' => $base.'/api-docs',
|
||||
'priority' => '0.5',
|
||||
'changefreq' => 'weekly',
|
||||
],
|
||||
[
|
||||
'loc' => $base.'/pricing',
|
||||
'priority' => '0.7',
|
||||
'changefreq' => 'weekly',
|
||||
],
|
||||
[
|
||||
'loc' => $base.'/privacy',
|
||||
'priority' => '0.3',
|
||||
'changefreq' => 'monthly',
|
||||
],
|
||||
[
|
||||
'loc' => $base.'/terms',
|
||||
'priority' => '0.3',
|
||||
'changefreq' => 'monthly',
|
||||
],
|
||||
[
|
||||
'loc' => $base.'/support',
|
||||
'priority' => '0.4',
|
||||
'changefreq' => 'weekly',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($items as $item) {
|
||||
@@ -475,10 +606,15 @@ class SiteController extends Controller
|
||||
}
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?>'."\n";
|
||||
$xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'."\n";
|
||||
$xml .=
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'.
|
||||
"\n";
|
||||
foreach ($urls as $url) {
|
||||
$xml .= " <url>\n";
|
||||
$xml .= ' <loc>'.htmlspecialchars((string) $url['loc'], ENT_XML1)."</loc>\n";
|
||||
$xml .=
|
||||
' <loc>'.
|
||||
htmlspecialchars((string) $url['loc'], ENT_XML1).
|
||||
"</loc>\n";
|
||||
$xml .= ' <lastmod>'.$lastUpdated."</lastmod>\n";
|
||||
$xml .= ' <changefreq>'.$url['changefreq']."</changefreq>\n";
|
||||
$xml .= ' <priority>'.$url['priority']."</priority>\n";
|
||||
@@ -486,7 +622,10 @@ class SiteController extends Controller
|
||||
}
|
||||
$xml .= '</urlset>'."\n";
|
||||
|
||||
return response($xml, 200)->header('Content-Type', 'application/xml; charset=UTF-8');
|
||||
return response($xml, 200)->header(
|
||||
'Content-Type',
|
||||
'application/xml; charset=UTF-8',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,7 +647,7 @@ class SiteController extends Controller
|
||||
private function loadDataset(): array
|
||||
{
|
||||
$dataPath = $this->datasetPath();
|
||||
if (!is_file($dataPath)) {
|
||||
if (! is_file($dataPath)) {
|
||||
return ['emojis' => []];
|
||||
}
|
||||
|
||||
@@ -518,7 +657,7 @@ class SiteController extends Controller
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
if (! is_array($decoded)) {
|
||||
return ['emojis' => []];
|
||||
}
|
||||
|
||||
@@ -538,12 +677,16 @@ class SiteController extends Controller
|
||||
|
||||
private function apkReleaseTargetUrl(string $key): string
|
||||
{
|
||||
if (!(bool) config('dewemoji.apk_release.enabled', false)) {
|
||||
if (! (bool) config('dewemoji.apk_release.enabled', false)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$base = trim((string) config('dewemoji.apk_release.r2_public_base_url', ''));
|
||||
$objectKey = trim((string) config("dewemoji.apk_release.r2_keys.{$key}", ''));
|
||||
$base = trim(
|
||||
(string) config('dewemoji.apk_release.r2_public_base_url', ''),
|
||||
);
|
||||
$objectKey = trim(
|
||||
(string) config("dewemoji.apk_release.r2_keys.{$key}", ''),
|
||||
);
|
||||
if ($base === '' || $objectKey === '') {
|
||||
return '';
|
||||
}
|
||||
@@ -581,32 +724,32 @@ class SiteController extends Controller
|
||||
if ($subcategory === 'family' || str_starts_with($name, 'family:')) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~\bwoman: beard\b~i', $name)) {
|
||||
if (preg_match("~\bwoman: beard\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~\bmen with bunny ears\b~i', $name)) {
|
||||
if (preg_match("~\bmen with bunny ears\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~\bpregnant man\b~i', $name)) {
|
||||
if (preg_match("~\bpregnant man\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if ($category === 'people & body') {
|
||||
if (preg_match('~\bmen holding hands\b~i', $name)) {
|
||||
if (preg_match("~\bmen holding hands\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~\bwomen holding hands\b~i', $name)) {
|
||||
if (preg_match("~\bwomen holding hands\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~kiss:.*\bman,\s*man\b~i', $name)) {
|
||||
if (preg_match("~kiss:.*\bman,\s*man\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~kiss:.*\bwoman,\s*woman\b~i', $name)) {
|
||||
if (preg_match("~kiss:.*\bwoman,\s*woman\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~couple.*\bman,\s*man\b~i', $name)) {
|
||||
if (preg_match("~couple.*\bman,\s*man\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
if (preg_match('~couple.*\bwoman,\s*woman\b~i', $name)) {
|
||||
if (preg_match("~couple.*\bwoman,\s*woman\b~i", $name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
|
||||
use Illuminate\Auth\MustVerifyEmail;
|
||||
use App\Notifications\ResetPasswordNotification;
|
||||
use App\Notifications\VerifyEmailNotification;
|
||||
use Illuminate\Auth\MustVerifyEmail;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
@@ -13,7 +13,7 @@ use Illuminate\Notifications\Notifiable;
|
||||
class User extends Authenticatable implements MustVerifyEmailContract
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable, MustVerifyEmail;
|
||||
use HasFactory, MustVerifyEmail, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
@@ -58,7 +58,7 @@ class User extends Authenticatable implements MustVerifyEmailContract
|
||||
|
||||
public function sendEmailVerificationNotification(): void
|
||||
{
|
||||
$this->notify(new VerifyEmailNotification());
|
||||
$this->notify(new VerifyEmailNotification);
|
||||
}
|
||||
|
||||
public function sendPasswordResetNotification($token): void
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use App\Mail\MailketingTransport;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\MailketingTransport;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Native\Desktop\Facades\Window;
|
||||
use Native\Desktop\Contracts\ProvidesPhpIni;
|
||||
use Native\Desktop\Facades\Window;
|
||||
|
||||
class NativeAppServiceProvider implements ProvidesPhpIni
|
||||
{
|
||||
|
||||
@@ -40,7 +40,7 @@ class ApiKeyService
|
||||
->whereNull('revoked_at')
|
||||
->first();
|
||||
|
||||
if (!$record) {
|
||||
if (! $record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class ApiKeyService
|
||||
$record->save();
|
||||
|
||||
$user = $record->user;
|
||||
if (!$user || (string) $user->tier !== 'personal') {
|
||||
if (! $user || (string) $user->tier !== 'personal') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,16 @@ class PayPalPlanSyncService
|
||||
];
|
||||
|
||||
$token = $this->getAccessToken($mode);
|
||||
if (!$token) {
|
||||
if (! $token) {
|
||||
Log::warning('PayPal plan sync aborted: missing access token', ['mode' => $mode]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$productId = $this->ensureProduct($mode, $token);
|
||||
if (!$productId) {
|
||||
if (! $productId) {
|
||||
Log::warning('PayPal plan sync aborted: missing product id', ['mode' => $mode]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -58,12 +60,14 @@ class PayPalPlanSyncService
|
||||
$keepIds[] = $currentPlanId;
|
||||
}
|
||||
$result['skipped']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$newPlanId = $this->createPlan($mode, $token, $productId, $plan->code, $plan->name, $amountUsd, $plan->period);
|
||||
if (!$newPlanId) {
|
||||
if (! $newPlanId) {
|
||||
$result['skipped']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -105,6 +109,7 @@ class PayPalPlanSyncService
|
||||
{
|
||||
$rate = (int) config('dewemoji.pricing.usd_rate', 15000);
|
||||
$usd = $rate > 0 ? $idrAmount / $rate : 0;
|
||||
|
||||
return number_format(max($usd, 1), 2, '.', '');
|
||||
}
|
||||
|
||||
@@ -125,7 +130,7 @@ class PayPalPlanSyncService
|
||||
$clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret");
|
||||
$apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base");
|
||||
|
||||
if (!$clientId || !$clientSecret || !$apiBase) {
|
||||
if (! $clientId || ! $clientSecret || ! $apiBase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -136,8 +141,9 @@ class PayPalPlanSyncService
|
||||
'grant_type' => 'client_credentials',
|
||||
]);
|
||||
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
Log::warning('PayPal auth failed', ['body' => $res->body()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -175,12 +181,13 @@ class PayPalPlanSyncService
|
||||
->post(rtrim($apiBase, '/').'/v1/catalogs/products', $payload);
|
||||
$createdId = $create->json('id');
|
||||
if ($createdId) {
|
||||
if (!$create->successful()) {
|
||||
if (! $create->successful()) {
|
||||
Log::warning('PayPal product create returned non-OK but provided id', [
|
||||
'status' => $create->status(),
|
||||
'body' => $create->body(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $createdId;
|
||||
}
|
||||
|
||||
@@ -188,6 +195,7 @@ class PayPalPlanSyncService
|
||||
'status' => $create->status(),
|
||||
'body' => $create->body(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -237,13 +245,14 @@ class PayPalPlanSyncService
|
||||
|
||||
$planId = $res->json('id');
|
||||
if ($planId) {
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
Log::warning('PayPal plan create returned non-OK but provided id', [
|
||||
'code' => $code,
|
||||
'status' => $res->status(),
|
||||
'body' => $res->body(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $planId;
|
||||
}
|
||||
|
||||
@@ -252,6 +261,7 @@ class PayPalPlanSyncService
|
||||
'status' => $res->status(),
|
||||
'body' => $res->body(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -262,7 +272,7 @@ class PayPalPlanSyncService
|
||||
->timeout((int) config('dewemoji.billing.providers.paypal.timeout', 10))
|
||||
->get(rtrim($apiBase, '/').'/v1/billing/plans/'.$planId);
|
||||
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -290,12 +300,12 @@ class PayPalPlanSyncService
|
||||
'page' => 1,
|
||||
]);
|
||||
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$items = $res->json('plans') ?? [];
|
||||
if (!is_array($items)) {
|
||||
if (! is_array($items)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -303,7 +313,7 @@ class PayPalPlanSyncService
|
||||
foreach ($items as $plan) {
|
||||
$id = $plan['id'] ?? null;
|
||||
$status = strtoupper((string) ($plan['status'] ?? ''));
|
||||
if (!$id || in_array($id, $keepIds, true)) {
|
||||
if (! $id || in_array($id, $keepIds, true)) {
|
||||
continue;
|
||||
}
|
||||
if ($status !== 'ACTIVE') {
|
||||
|
||||
@@ -14,8 +14,7 @@ class PaypalWebhookProcessor
|
||||
public function __construct(
|
||||
private readonly SubscriptionTransitionService $subscriptionTransition,
|
||||
private readonly KeywordQuotaService $keywordQuota
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
@@ -23,7 +22,7 @@ class PaypalWebhookProcessor
|
||||
public function process(string $eventType, array $payload): void
|
||||
{
|
||||
$resource = $payload['resource'] ?? [];
|
||||
if (!is_array($resource)) {
|
||||
if (! is_array($resource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +34,7 @@ class PaypalWebhookProcessor
|
||||
}
|
||||
|
||||
$user = User::where('email', $email)->first();
|
||||
if (!$user) {
|
||||
if (! $user) {
|
||||
throw new \RuntimeException('User not found for webhook email.');
|
||||
}
|
||||
|
||||
@@ -68,6 +67,7 @@ class PaypalWebhookProcessor
|
||||
$subscriptionId,
|
||||
$planCode
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class PaypalWebhookProcessor
|
||||
'tier' => $active ? 'personal' : 'free',
|
||||
]);
|
||||
$this->keywordQuota->enforceForUser($userId, $active ? 'personal' : 'free');
|
||||
if (!$active) {
|
||||
if (! $active) {
|
||||
UserApiKey::where('user_id', $userId)->update(['revoked_at' => now()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class SubscriptionTransitionService
|
||||
$sub->status = 'canceled';
|
||||
$sub->canceled_at = now();
|
||||
$sub->save();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -74,12 +75,13 @@ class SubscriptionTransitionService
|
||||
$isRecurring = in_array((string) $sub->plan, ['personal_monthly', 'personal_annual'], true);
|
||||
if ($sub->provider === 'paypal' && $isRecurring && $sub->provider_ref) {
|
||||
$cancelled = $this->cancelPaypalSubscription($mode, (string) $sub->provider_ref);
|
||||
if (!$cancelled) {
|
||||
if (! $cancelled) {
|
||||
Log::warning('Could not cancel previous PayPal subscription during plan transition', [
|
||||
'user_id' => $userId,
|
||||
'new_plan' => $newPlanCode,
|
||||
'old_subscription_id' => $sub->provider_ref,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +95,7 @@ class SubscriptionTransitionService
|
||||
private function cancelPaypalSubscription(string $mode, string $subscriptionId): bool
|
||||
{
|
||||
$token = $this->getAccessToken($mode);
|
||||
if (!$token) {
|
||||
if (! $token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -118,7 +120,7 @@ class SubscriptionTransitionService
|
||||
$clientSecret = config("dewemoji.billing.providers.paypal.{$mode}.client_secret");
|
||||
$apiBase = config("dewemoji.billing.providers.paypal.{$mode}.api_base");
|
||||
|
||||
if (!$clientId || !$clientSecret || !$apiBase) {
|
||||
if (! $clientId || ! $clientSecret || ! $apiBase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -129,7 +131,7 @@ class SubscriptionTransitionService
|
||||
'grant_type' => 'client_credentials',
|
||||
]);
|
||||
|
||||
if (!$res->successful()) {
|
||||
if (! $res->successful()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ class EmojiCatalogService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SettingsService $settings
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
@@ -20,7 +19,7 @@ class EmojiCatalogService
|
||||
public function findItem(int $emojiId): ?array
|
||||
{
|
||||
$base = DB::table('emojis')->where('emoji_id', $emojiId)->first();
|
||||
if (!$base) {
|
||||
if (! $base) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -151,7 +150,7 @@ class EmojiCatalogService
|
||||
public function deleteItem(int $emojiId): void
|
||||
{
|
||||
$emoji = DB::table('emojis')->where('emoji_id', $emojiId)->first();
|
||||
if (!$emoji) {
|
||||
if (! $emoji) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -176,7 +175,7 @@ class EmojiCatalogService
|
||||
*/
|
||||
public function importFromDataFile(string $path): array
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
if (! is_file($path)) {
|
||||
throw new RuntimeException('Dataset file not found: '.$path);
|
||||
}
|
||||
|
||||
@@ -186,7 +185,7 @@ class EmojiCatalogService
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded) || !is_array($decoded['emojis'] ?? null)) {
|
||||
if (! is_array($decoded) || ! is_array($decoded['emojis'] ?? null)) {
|
||||
throw new RuntimeException('Invalid emoji dataset format.');
|
||||
}
|
||||
|
||||
@@ -195,7 +194,7 @@ class EmojiCatalogService
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
if (! is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$total++;
|
||||
@@ -203,10 +202,12 @@ class EmojiCatalogService
|
||||
$emojiId = (int) ($row['emoji_id'] ?? 0);
|
||||
if ($slug !== '' && DB::table('emojis')->where('slug', $slug)->exists()) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($emojiId > 0 && DB::table('emojis')->where('emoji_id', $emojiId)->exists()) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ class EmojiCatalogService
|
||||
];
|
||||
|
||||
$snapshotDir = $this->snapshotDirectory();
|
||||
if (!is_dir($snapshotDir) && !mkdir($snapshotDir, 0775, true) && !is_dir($snapshotDir)) {
|
||||
if (! is_dir($snapshotDir) && ! mkdir($snapshotDir, 0775, true) && ! is_dir($snapshotDir)) {
|
||||
throw new RuntimeException('Could not create snapshot directory.');
|
||||
}
|
||||
|
||||
@@ -405,7 +406,7 @@ class EmojiCatalogService
|
||||
public function listSnapshots(): array
|
||||
{
|
||||
$dir = $this->snapshotDirectory();
|
||||
if (!is_dir($dir)) {
|
||||
if (! is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -413,11 +414,11 @@ class EmojiCatalogService
|
||||
$files = glob($dir.'/emojis-*.json') ?: [];
|
||||
$items = [];
|
||||
foreach ($files as $file) {
|
||||
if (!is_file($file)) {
|
||||
if (! is_file($file)) {
|
||||
continue;
|
||||
}
|
||||
$name = basename($file);
|
||||
if (!preg_match('/^emojis-(\d{14})\.json$/', $name, $m)) {
|
||||
if (! preg_match('/^emojis-(\d{14})\.json$/', $name, $m)) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
@@ -438,12 +439,12 @@ class EmojiCatalogService
|
||||
|
||||
public function activateSnapshot(string $filename, ?string $updatedBy = null): array
|
||||
{
|
||||
if (!preg_match('/^emojis-(\d{14})\.json$/', $filename, $m)) {
|
||||
if (! preg_match('/^emojis-(\d{14})\.json$/', $filename, $m)) {
|
||||
throw new RuntimeException('Invalid snapshot filename.');
|
||||
}
|
||||
|
||||
$fullPath = $this->snapshotDirectory().'/'.$filename;
|
||||
if (!is_file($fullPath)) {
|
||||
if (! is_file($fullPath)) {
|
||||
throw new RuntimeException('Snapshot file not found.');
|
||||
}
|
||||
|
||||
@@ -509,7 +510,6 @@ class EmojiCatalogService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $input
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private function normalizeArray(mixed $input): array
|
||||
@@ -517,7 +517,7 @@ class EmojiCatalogService
|
||||
if (is_string($input)) {
|
||||
$input = preg_split('/\r\n|\r|\n|,/', $input) ?: [];
|
||||
}
|
||||
if (!is_array($input)) {
|
||||
if (! is_array($input)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class ExtensionVerificationService
|
||||
}
|
||||
|
||||
$config = config('dewemoji.extension_verification', []);
|
||||
if (!(bool) ($config['enabled'] ?? true)) {
|
||||
if (! (bool) ($config['enabled'] ?? true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,12 +36,12 @@ class ExtensionVerificationService
|
||||
'Authorization' => 'key='.$serverKey,
|
||||
])->get($url);
|
||||
|
||||
if (!$response->ok()) {
|
||||
if (! $response->ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
if (!is_array($data)) {
|
||||
if (! is_array($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class KeywordQuotaService
|
||||
->where('user_id', $userId)
|
||||
->where('is_active', false)
|
||||
->update(['is_active' => true]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,4 +56,3 @@ class KeywordQuotaService
|
||||
->update(['is_active' => false]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class LiveSqlImportService
|
||||
|
||||
public function import(string $path, bool $truncate, int $batchSize, ?OutputInterface $output = null): void
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
if (! is_file($path)) {
|
||||
throw new \RuntimeException("SQL file not found: {$path}");
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@ class LiveSqlImportService
|
||||
$totalStatements = 0;
|
||||
$totalRows = 0;
|
||||
|
||||
while (!$file->eof()) {
|
||||
while (! $file->eof()) {
|
||||
$line = $file->fgets();
|
||||
if ($line === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($statement === '') {
|
||||
if (!Str::startsWith(ltrim($line), 'INSERT INTO')) {
|
||||
if (! Str::startsWith(ltrim($line), 'INSERT INTO')) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class LiveSqlImportService
|
||||
];
|
||||
|
||||
foreach ($targets as $table) {
|
||||
if (!Schema::hasTable($table)) {
|
||||
if (! Schema::hasTable($table)) {
|
||||
continue;
|
||||
}
|
||||
DB::table($table)->truncate();
|
||||
@@ -136,7 +136,7 @@ class LiveSqlImportService
|
||||
$statement = trim($statement);
|
||||
$pattern = '/^INSERT INTO `([^`]+)` \\(([^)]+)\\) VALUES\\s*(.+);$/s';
|
||||
|
||||
if (!preg_match($pattern, $statement, $matches)) {
|
||||
if (! preg_match($pattern, $statement, $matches)) {
|
||||
return [0, 'unknown'];
|
||||
}
|
||||
|
||||
@@ -145,13 +145,13 @@ class LiveSqlImportService
|
||||
$valuesRaw = $matches[3];
|
||||
|
||||
$columns = array_map(
|
||||
static fn (string $col): string => trim($col, " `"),
|
||||
static fn (string $col): string => trim($col, ' `'),
|
||||
explode(',', $columnsRaw)
|
||||
);
|
||||
|
||||
$targetTable = $this->tableRename[$table] ?? $table;
|
||||
|
||||
if (!Schema::hasTable($targetTable)) {
|
||||
if (! Schema::hasTable($targetTable)) {
|
||||
return [0, $targetTable];
|
||||
}
|
||||
|
||||
@@ -260,20 +260,24 @@ class LiveSqlImportService
|
||||
if ($escape) {
|
||||
$buffer .= $this->unescapeChar($ch);
|
||||
$escape = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === '\\\\') {
|
||||
$escape = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === '\'') {
|
||||
$inString = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$buffer .= $ch;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -282,12 +286,14 @@ class LiveSqlImportService
|
||||
$currentRow = [];
|
||||
$buffer = '';
|
||||
$valueIsString = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($ch === '\'') {
|
||||
$inString = true;
|
||||
$valueIsString = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -295,6 +301,7 @@ class LiveSqlImportService
|
||||
$currentRow[] = $this->convertValue($buffer, $valueIsString);
|
||||
$buffer = '';
|
||||
$valueIsString = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -306,6 +313,7 @@ class LiveSqlImportService
|
||||
$buffer = '';
|
||||
$valueIsString = false;
|
||||
$inRow = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -321,7 +329,7 @@ class LiveSqlImportService
|
||||
{
|
||||
$value = trim($buffer);
|
||||
|
||||
if (!$valueIsString) {
|
||||
if (! $valueIsString) {
|
||||
if ($value === '' || strtoupper($value) === 'NULL') {
|
||||
return null;
|
||||
}
|
||||
@@ -352,7 +360,7 @@ class LiveSqlImportService
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
if (! is_string($value)) {
|
||||
return json_encode($value);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Cache;
|
||||
class SettingsService
|
||||
{
|
||||
private const CACHE_KEY = 'dw_settings_all';
|
||||
|
||||
private const CACHE_TTL = 30;
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,7 @@ class SettingsService
|
||||
foreach (Setting::all(['key', 'value']) as $setting) {
|
||||
$out[$setting->key] = $setting->value;
|
||||
}
|
||||
|
||||
return $out;
|
||||
});
|
||||
}
|
||||
@@ -27,6 +29,7 @@ class SettingsService
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$all = $this->all();
|
||||
|
||||
return array_key_exists($key, $all) ? $all[$key] : $default;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,3 @@ return new class extends Migration
|
||||
Schema::dropIfExists('licenses');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,4 +28,3 @@ return new class extends Migration
|
||||
Schema::dropIfExists('license_activations');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,4 +26,3 @@ return new class extends Migration
|
||||
Schema::dropIfExists('usage_logs');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_audit_logs', function (Blueprint $table): void {
|
||||
|
||||
@@ -22,4 +22,3 @@ return new class extends Migration
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('emojis')) {
|
||||
if (! Schema::hasTable('emojis')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ return new class extends Migration
|
||||
->pluck('slug')
|
||||
->all();
|
||||
|
||||
if (!empty($duplicates)) {
|
||||
if (! empty($duplicates)) {
|
||||
throw new \RuntimeException(
|
||||
'Cannot add unique index on emojis.slug because duplicate slugs exist: '.implode(', ', $duplicates)
|
||||
);
|
||||
@@ -34,7 +34,7 @@ return new class extends Migration
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (!Schema::hasTable('emojis')) {
|
||||
if (! Schema::hasTable('emojis')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\V1\EmojiApiController;
|
||||
use App\Http\Controllers\Api\V1\SystemController;
|
||||
use App\Http\Controllers\Api\V1\AdminUserController;
|
||||
use App\Http\Controllers\Api\V1\AdminAnalyticsController;
|
||||
use App\Http\Controllers\Api\V1\AdminPricingController;
|
||||
use App\Http\Controllers\Api\V1\AdminSettingsController;
|
||||
use App\Http\Controllers\Api\V1\AdminSubscriptionController;
|
||||
use App\Http\Controllers\Api\V1\AdminAnalyticsController;
|
||||
use App\Http\Controllers\Api\V1\AdminUserController;
|
||||
use App\Http\Controllers\Api\V1\AdminWebhookController;
|
||||
use App\Http\Controllers\Api\V1\EmojiApiController;
|
||||
use App\Http\Controllers\Api\V1\ExtensionController;
|
||||
use App\Http\Controllers\Api\V1\PricingController;
|
||||
use App\Http\Controllers\Api\V1\SystemController;
|
||||
use App\Http\Controllers\Api\V1\UserController;
|
||||
use App\Http\Controllers\Api\V1\UserKeywordController;
|
||||
use App\Http\Controllers\Api\V1\PricingController;
|
||||
use App\Http\Controllers\Billing\PayPalController;
|
||||
use App\Http\Controllers\Billing\PakasirController;
|
||||
use App\Http\Controllers\Billing\PayPalController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::options('/v1/{any}', function () {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
use App\Services\LiveSqlImportService;
|
||||
use App\Services\Billing\PaypalWebhookProcessor;
|
||||
use App\Services\Billing\PayPalPlanSyncService;
|
||||
use App\Mail\TestMailketing;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Subscription;
|
||||
use App\Models\WebhookEvent;
|
||||
use App\Services\Billing\PayPalPlanSyncService;
|
||||
use App\Services\Billing\PaypalWebhookProcessor;
|
||||
use App\Services\LiveSqlImportService;
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Mail\TestMailketing;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
@@ -32,6 +32,7 @@ Artisan::command('dewemoji:webhooks:process {--limit=100 : Max events to process
|
||||
|
||||
if (empty($statuses)) {
|
||||
$this->error('No statuses provided.');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ Artisan::command('dewemoji:webhooks:process {--limit=100 : Max events to process
|
||||
}
|
||||
|
||||
$this->info("Processed {$processed} events, failed {$failed}.");
|
||||
|
||||
return 0;
|
||||
})->purpose('Process pending webhook events');
|
||||
|
||||
@@ -94,11 +96,13 @@ Artisan::command('mailketing:test {email : Recipient email address}', function (
|
||||
$email = (string) $this->argument('email');
|
||||
|
||||
try {
|
||||
Mail::to($email)->send(new TestMailketing());
|
||||
Mail::to($email)->send(new TestMailketing);
|
||||
$this->info("Mailketing test email sent to {$email}.");
|
||||
|
||||
return 0;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Mailketing test failed: '.$e->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
})->purpose('Send a Mailketing API test email');
|
||||
@@ -117,5 +121,6 @@ Artisan::command('dewemoji:normalize-statuses', function () {
|
||||
->update(['status' => 'canceled']);
|
||||
|
||||
$this->info("Normalized statuses: subscriptions={$subs}, orders={$orders}, payments={$payments}");
|
||||
|
||||
return 0;
|
||||
})->purpose('Normalize legacy cancelled status spelling to canceled');
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use App\Http\Controllers\Dashboard\AdminDashboardController;
|
||||
use App\Http\Controllers\Dashboard\AdminEmojiCatalogController;
|
||||
use App\Http\Controllers\Dashboard\UserDashboardController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Billing\BillingPaymentController;
|
||||
use App\Http\Controllers\Billing\PakasirController;
|
||||
use App\Http\Controllers\Billing\PayPalController;
|
||||
use App\Http\Controllers\ProfileController;
|
||||
use App\Http\Controllers\Web\SiteController;
|
||||
use App\Http\Controllers\Billing\PayPalController;
|
||||
use App\Http\Controllers\Billing\PakasirController;
|
||||
use App\Http\Controllers\Billing\BillingPaymentController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/', [SiteController::class, 'home'])->name('home');
|
||||
|
||||
Reference in New Issue
Block a user