Files
dewemoji/app/app/Http/Controllers/Dashboard/UserDashboardController.php

496 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Subscription;
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;
use Illuminate\Contracts\View\View;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class UserDashboardController extends Controller
{
public function __construct(
private readonly ApiKeyService $keys,
private readonly KeywordQuotaService $keywordQuota
) {
}
public function overview(Request $request): View
{
$user = $request->user();
if (Gate::allows('admin')) {
$days = 7;
$start = now()->subDays($days - 1)->startOfDay();
$labels = [];
$values = [];
$subsValues = [];
$webhookValues = [];
$rawUsers = DB::table('users')
->selectRaw('DATE(created_at) as day, COUNT(*) as total')
->where('created_at', '>=', $start)
->groupBy('day')
->orderBy('day')
->get()
->keyBy('day');
$rawSubs = DB::table('subscriptions')
->selectRaw('DATE(created_at) as day, COUNT(*) as total')
->where('created_at', '>=', $start)
->groupBy('day')
->orderBy('day')
->get()
->keyBy('day');
$rawWebhooks = DB::table('webhook_events')
->selectRaw('DATE(COALESCE(received_at, created_at)) as day, COUNT(*) as total')
->where('created_at', '>=', $start)
->groupBy('day')
->orderBy('day')
->get()
->keyBy('day');
for ($i = $days - 1; $i >= 0; $i--) {
$date = Carbon::now()->subDays($i)->format('Y-m-d');
$labels[] = Carbon::parse($date)->format('M d');
$values[] = (int) ($rawUsers[$date]->total ?? 0);
$subsValues[] = (int) ($rawSubs[$date]->total ?? 0);
$webhookValues[] = (int) ($rawWebhooks[$date]->total ?? 0);
}
$usersTotal = User::count();
$usersPersonal = User::where('tier', 'personal')->count();
$subscriptionsActive = Subscription::where('status', 'active')->count();
$subscriptionsTotal = Subscription::count();
$webhookTotal = WebhookEvent::count();
$webhookErrors = WebhookEvent::where('status', 'error')->count();
return view('dashboard.index', [
'chartLabels' => $labels,
'chartValues' => $values,
'chartSubs' => $subsValues,
'chartWebhooks' => $webhookValues,
'overviewMetrics' => [
'users_total' => $usersTotal,
'users_personal' => $usersPersonal,
'subscriptions_active' => $subscriptionsActive,
'subscriptions_total' => $subscriptionsTotal,
'webhook_total' => $webhookTotal,
'webhook_errors' => $webhookErrors,
],
]);
}
$recentKeywords = UserKeyword::where('user_id', $user?->id)
->orderByDesc('id')
->limit(6)
->get();
$recentWeekCount = UserKeyword::where('user_id', $user?->id)
->where('created_at', '>=', now()->subDays(7))
->count();
$totalKeywords = UserKeyword::where('user_id', $user?->id)->count();
$apiKeyCount = UserApiKey::where('user_id', $user?->id)
->whereNull('revoked_at')
->count();
$activeSubscription = Subscription::where('user_id', $user?->id)
->orderByDesc('started_at')
->first();
return view('dashboard.user.overview', [
'totalKeywords' => $totalKeywords,
'recentKeywords' => $recentKeywords,
'recentWeekCount' => $recentWeekCount,
'apiKeyCount' => $apiKeyCount,
'subscription' => $activeSubscription,
]);
}
public function keywords(Request $request): View
{
$user = $request->user();
$items = UserKeyword::where('user_id', $user?->id)
->orderByDesc('id')
->get();
$activeCount = UserKeyword::where('user_id', $user?->id)
->where('is_active', true)
->count();
$emojiLookup = [];
$dataPath = (string) config('dewemoji.data_path');
if (is_file($dataPath)) {
$raw = file_get_contents($dataPath);
$decoded = json_decode((string) $raw, true);
if (is_array($decoded)) {
foreach ($decoded['emojis'] ?? [] as $row) {
$slug = (string) ($row['slug'] ?? '');
if ($slug !== '') {
$emojiLookup[$slug] = [
'emoji' => (string) ($row['emoji'] ?? ''),
'name' => (string) ($row['name'] ?? $slug),
];
}
}
}
}
return view('dashboard.user.keywords', [
'items' => $items,
'user' => $user,
'emojiLookup' => $emojiLookup,
'freeLimit' => $this->keywordLimitFor($user),
'activeCount' => $activeCount,
]);
}
public function keywordSearch(Request $request, EmojiApiController $emoji): JsonResponse
{
$request->query->set('private', 'true');
return $emoji->search($request);
}
public function storeKeyword(Request $request): RedirectResponse|JsonResponse
{
$user = $request->user();
if (!$user) {
abort(403);
}
$data = $request->validate([
'emoji_slug' => 'required|string|max:120',
'keyword' => 'required|string|max:200',
'lang' => 'nullable|string|max:10',
]);
$existing = UserKeyword::where('user_id', $user->id)
->where('emoji_slug', $data['emoji_slug'])
->where('keyword', $data['keyword'])
->first();
$targetActive = $existing ? (bool) $existing->is_active : true;
if ($limit = $this->keywordLimitFor($user)) {
if (!$existing && $targetActive) {
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
if ($activeCount >= $limit) {
return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached');
}
}
}
if ($existing) {
$existing->update([
'lang' => $data['lang'] ?? 'und',
]);
$item = $existing;
} else {
$item = UserKeyword::create([
'user_id' => $user->id,
'emoji_slug' => $data['emoji_slug'],
'keyword' => $data['keyword'],
'lang' => $data['lang'] ?? 'und',
'is_active' => true,
]);
}
if ($request->expectsJson()) {
return response()->json(['ok' => true, 'item' => $item]);
}
return back()->with('status', 'Keyword saved.');
}
public function updateKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
{
$user = $request->user();
if (!$user) {
abort(403);
}
if ($keyword->user_id !== $user->id) {
abort(403);
}
$data = $request->validate([
'emoji_slug' => 'required|string|max:120',
'keyword' => 'required|string|max:200',
'lang' => 'nullable|string|max:10',
]);
$duplicate = UserKeyword::where('user_id', $user->id)
->where('emoji_slug', $data['emoji_slug'])
->where('keyword', $data['keyword'])
->first();
if ($duplicate && $duplicate->id !== $keyword->id) {
$keyword->delete();
$keyword = $duplicate;
} else {
$keyword->update([
'emoji_slug' => $data['emoji_slug'],
'keyword' => $data['keyword'],
'lang' => $data['lang'] ?? 'und',
]);
}
if ($request->expectsJson()) {
return response()->json(['ok' => true, 'item' => $keyword]);
}
return back()->with('status', 'Keyword updated.');
}
public function deleteKeyword(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
{
$user = $request->user();
if (!$user) {
abort(403);
}
if ($keyword->user_id !== $user->id) {
abort(403);
}
$keyword->delete();
if ($request->expectsJson()) {
return response()->json(['ok' => true]);
}
return back()->with('status', 'Keyword removed.');
}
public function importKeywords(Request $request): RedirectResponse
{
$user = $request->user();
if (!$user) {
abort(403);
}
$payload = $request->input('payload');
if ($request->hasFile('file')) {
$payload = $request->file('file')->get();
}
$items = json_decode((string) $payload, true);
if (!is_array($items)) {
return back()->withErrors(['payload' => 'Invalid JSON payload.']);
}
$imported = 0;
$skipped = 0;
$limit = $this->keywordLimitFor($user);
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
foreach ($items as $row) {
if (!is_array($row)) {
continue;
}
$emojiSlug = trim((string) ($row['emoji_slug'] ?? ''));
$keyword = trim((string) ($row['keyword'] ?? ''));
$lang = trim((string) ($row['lang'] ?? 'und'));
if ($emojiSlug === '' || $keyword === '') {
continue;
}
$existing = UserKeyword::where('user_id', $user->id)
->where('emoji_slug', $emojiSlug)
->where('keyword', $keyword)
->first();
$targetActive = filter_var($row['is_active'] ?? true, FILTER_VALIDATE_BOOL);
if (!$existing && $targetActive && $limit !== null && $activeCount >= $limit) {
$skipped += 1;
continue;
}
if ($existing) {
$existing->update([
'lang' => $lang !== '' ? $lang : 'und',
]);
} else {
UserKeyword::create([
'user_id' => $user->id,
'emoji_slug' => $emojiSlug,
'keyword' => $keyword,
'lang' => $lang !== '' ? $lang : 'und',
'is_active' => $targetActive,
]);
if ($targetActive) {
$activeCount += 1;
}
}
$imported += 1;
}
$message = "Imported {$imported} keywords.";
if ($skipped > 0) {
$message .= " {$skipped} skipped (active free limit reached).";
}
return back()->with('status', $message);
}
public function exportKeywords(Request $request): BinaryFileResponse
{
$user = $request->user();
if (!$user) {
abort(403);
}
$items = UserKeyword::where('user_id', $user->id)
->orderByDesc('id')
->get(['emoji_slug', 'keyword', 'lang', 'is_active'])
->values()
->toJson(JSON_PRETTY_PRINT);
$filename = 'dewemoji-keywords-'.$user->id.'-'.now()->format('Ymd-His').'.json';
$path = storage_path('app/'.$filename);
file_put_contents($path, $items);
return response()->download($path, $filename)->deleteFileAfterSend(true);
}
public function apiKeys(Request $request): View
{
$user = $request->user();
$keys = UserApiKey::where('user_id', $user?->id)
->orderByDesc('id')
->get();
$canCreate = $user && (string) $user->tier === 'personal';
return view('dashboard.user.api-keys', [
'user' => $user,
'keys' => $keys,
'canCreate' => $canCreate,
'newKey' => session('new_api_key'),
]);
}
public function createApiKey(Request $request): RedirectResponse
{
$user = $request->user();
if (!$user) {
abort(403);
}
if ((string) $user->tier !== 'personal') {
return back()->withErrors(['api_key' => 'API keys are available on the Personal plan.']);
}
$data = $request->validate([
'name' => 'nullable|string|max:100',
]);
$issued = $this->keys->issueKey($user, $data['name'] ?? null);
return back()->with('new_api_key', $issued['plain']);
}
public function revokeApiKey(Request $request, UserApiKey $key): RedirectResponse
{
$user = $request->user();
if (!$user || $key->user_id !== $user->id) {
abort(403);
}
$key->update(['revoked_at' => Carbon::now()]);
return back()->with('status', 'API key revoked.');
}
public function billing(Request $request): View
{
$user = $request->user();
$subscription = Subscription::where('user_id', $user?->id)
->orderByDesc('started_at')
->first();
$orders = Order::where('user_id', $user?->id)
->orderByDesc('id')
->limit(5)
->get();
$payments = Payment::where('user_id', $user?->id)
->orderByDesc('id')
->limit(5)
->get();
return view('dashboard.user.billing', [
'subscription' => $subscription,
'user' => $user,
'orders' => $orders,
'payments' => $payments,
]);
}
public function preferences(Request $request): View
{
return view('dashboard.user.preferences');
}
public function toggleKeywordActive(Request $request, UserKeyword $keyword): RedirectResponse|JsonResponse
{
$user = $request->user();
if (!$user || $keyword->user_id !== $user->id) {
abort(403);
}
$data = $request->validate([
'is_active' => 'required|boolean',
]);
$target = (bool) $data['is_active'];
if ($target && ($limit = $this->keywordLimitFor($user))) {
$activeCount = $this->keywordQuota->activeCount((int) $user->id);
if (!$keyword->is_active && $activeCount >= $limit) {
return $this->rejectKeywordLimit($request, $limit, 'free_active_limit_reached');
}
}
$keyword->update(['is_active' => $target]);
if ($request->expectsJson()) {
return response()->json(['ok' => true, 'item' => $keyword]);
}
return back()->with('status', $target ? 'Keyword activated.' : 'Keyword deactivated.');
}
private function rejectKeywordLimit(Request $request, int $limit, string $error = 'free_limit_reached'): RedirectResponse|JsonResponse
{
if ($request->expectsJson()) {
return response()->json(['ok' => false, 'error' => $error, 'limit' => $limit], 403);
}
return back()->withErrors(['tier' => "Free plan active limit reached ({$limit} keywords)."]);
}
private function keywordLimitFor(?User $user): ?int
{
if (!$user) {
return null;
}
if ((string) $user->tier === 'personal') {
return null;
}
return (int) config('dewemoji.pagination.free_max_limit', 20);
}
}