Files
inter-hub/Web/View/ApiConsumers/Show.hs
Bernd Worsch f1978c3888 fix(WP-0014): pre-flight compilation fixes, Tailwind pipeline, and admin seed
A2 — Compilation fixes:
- Remove inline FK constraints from Schema.sql; IHP schema compiler cannot
  parse them. Add 1744329600-restore-fk-constraints.sql migration to restore
  referential integrity at the DB level.
- Rename `#label` → `#label_` throughout to avoid clash with Haskell built-in.
- Fix `hub.id == hid` UUID comparisons to use `toUUID hub.id`.
- Replace non-existent `setStatus`/`respondJson` calls with
  `renderJsonWithStatusCode` throughout Api controllers.
- Fix qualified package import for `cryptohash-sha256` in Auth.hs.
- Add `CanSelect (Text, Text)` instance in Helper.View.
- Refactor HSX inline lambdas to named helper functions in 100+ views
  (GHC cannot infer types for anonymous functions inside quasi-quoted HSX).
- Fix missing imports (IHP.QueryBuilder, IHP.Fetch, Web.Routes, Only, etc.)
  across helpers and controllers.
- Remove duplicate `diffUTCTime` definition in BottleneckDetector.
- Change `createEventForHub` return type from `IO ResponseReceived` to `IO ()`.
- Seed type-registry vocabulary via 1744502400-seed-type-registries.sql
  (moved from Schema.sql where IHP does not execute INSERT statements).

A3 — Tailwind build pipeline:
- Add `tailwindcss` to flake.nix native packages.
- Uncomment `tailwind.exec` process in devenv shell config.
- Add tailwind/tailwind.config.js (scans Web/View/**/*.hs).
- Add tailwind/app.css with @tailwind directives.

A4 — Admin user seed:
- Add 1744416000-seed-admin-user.sql: inserts admin@inter-hub.local
  with bcrypt-hashed password admin1234! (cost 10).
- Add .env.example documenting all required environment variables
  and default admin credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:55:12 +00:00

174 lines
8.0 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
module Web.View.ApiConsumers.Show where
import Web.Types
import Generated.Types
import IHP.Prelude
import IHP.ViewPrelude
import Web.Routes ()
data ShowView = ShowView
{ consumer :: !ApiConsumer
, apiKeys :: ![ApiKey]
, webhooks :: ![WebhookSubscription]
, mManifest :: !(Maybe HubCapabilityManifest)
}
instance View ShowView where
html ShowView { .. } = [hsx|
<div class="mb-6">
<div class="flex items-start justify-between">
<div>
<h1 class="text-2xl font-semibold">{consumer.name}</h1>
{maybeDescription}
</div>
<div class="flex gap-2">
<a href={EditApiConsumerAction consumer.id}
class="border text-sm px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
<a href={DeactivateApiConsumerAction consumer.id}
data-method="post" data-confirm="Deactivate this consumer?"
class="border border-red-200 text-red-600 text-sm px-3 py-1.5 rounded hover:bg-red-50">
Deactivate
</a>
</div>
</div>
</div>
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="bg-white border rounded p-4">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Status</div>
{renderConsumerStatusDetail consumer.isActive}
</div>
<div class="bg-white border rounded p-4">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Rate Limit</div>
<div class="text-sm font-medium">{show consumer.rateLimitPerMinute} req/min</div>
</div>
<div class="bg-white border rounded p-4">
<div class="text-xs text-gray-500 uppercase tracking-wide mb-1">Quota</div>
<div class="text-sm font-medium">{show consumer.quotaPerDay} req/day</div>
</div>
</div>
{manifestPanel}
<div class="mb-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold">API Keys</h2>
<a href={NewApiKeyAction consumer.id}
class="bg-indigo-600 text-white text-sm px-3 py-1.5 rounded hover:bg-indigo-700">
New Key
</a>
</div>
{if null apiKeys then noKeysMsg else keysTable}
</div>
<div>
<div class="flex items-center justify-between mb-3">
<h2 class="text-lg font-semibold">Webhook Subscriptions</h2>
<a href={NewWebhookSubscriptionAction consumer.id}
class="bg-indigo-600 text-white text-sm px-3 py-1.5 rounded hover:bg-indigo-700">
New Subscription
</a>
</div>
{if null webhooks then noWebhooksMsg else webhooksTable}
</div>
|]
where
maybeDescription = case consumer.description of
Just d -> [hsx|<p class="text-sm text-gray-500 mt-1">{d}</p>|]
Nothing -> mempty
manifestPanel = case mManifest of
Nothing -> mempty
Just m -> [hsx|
<div class="bg-indigo-50 border border-indigo-100 rounded p-4 mb-6">
<div class="text-xs text-indigo-500 uppercase tracking-wide mb-1">Hub Capability Manifest</div>
<div class="text-sm font-medium">{m.manifestVersion} <span class="text-indigo-600">{m.status}</span></div>
</div>
|]
keysTable = [hsx|
<div class="bg-white border rounded overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b"><tr>
<th class="text-left px-4 py-2 font-medium text-gray-600">Prefix</th>
<th class="text-left px-4 py-2 font-medium text-gray-600">Type</th>
<th class="text-left px-4 py-2 font-medium text-gray-600">Scopes</th>
<th class="text-left px-4 py-2 font-medium text-gray-600">Expires</th>
<th class="text-left px-4 py-2 font-medium text-gray-600">Status</th>
<th class="px-4 py-2"></th>
</tr></thead>
<tbody class="divide-y divide-gray-100">
{forEach apiKeys renderKey}
</tbody>
</table>
</div>
|]
renderKey k = [hsx|
<tr>
<td class="px-4 py-2 font-mono text-xs">{k.keyPrefix}...</td>
<td class="px-4 py-2 text-gray-500">{k.tokenType}</td>
<td class="px-4 py-2 text-gray-500">{if k.scopes == "" then "" else k.scopes}</td>
<td class="px-4 py-2 text-gray-500">{maybe "never" show k.expiresAt}</td>
<td class="px-4 py-2">
{renderKeyStatus (isJust k.revokedAt)}
</td>
<td class="px-4 py-2 text-right">
{renderRevokeLink k}
</td>
</tr>
|]
webhooksTable = [hsx|
<div class="bg-white border rounded overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b"><tr>
<th class="text-left px-4 py-2 font-medium text-gray-600">Event Type</th>
<th class="text-left px-4 py-2 font-medium text-gray-600">Target URL</th>
<th class="text-left px-4 py-2 font-medium text-gray-600">Status</th>
<th class="px-4 py-2"></th>
</tr></thead>
<tbody class="divide-y divide-gray-100">
{forEach webhooks renderWebhook}
</tbody>
</table>
</div>
|]
renderWebhook wh = [hsx|
<tr>
<td class="px-4 py-2 font-mono text-xs">{wh.eventType}</td>
<td class="px-4 py-2 text-gray-500 text-xs truncate max-w-xs">{wh.targetUrl}</td>
<td class="px-4 py-2">
{renderWebhookStatus wh.isActive}
</td>
<td class="px-4 py-2 text-right">
<a href={ToggleWebhookSubscriptionAction wh.id} data-method="post"
class="text-gray-400 hover:text-gray-700 text-xs mr-2">Toggle</a>
<a href={DeleteWebhookSubscriptionAction wh.id} data-method="delete"
data-confirm="Delete this subscription?"
class="text-red-400 hover:text-red-600 text-xs">Delete</a>
</td>
</tr>
|]
noKeysMsg :: Html
noKeysMsg = [hsx|<p class="text-sm text-gray-400">No keys yet.</p>|]
noWebhooksMsg :: Html
noWebhooksMsg = [hsx|<p class="text-sm text-gray-400">No webhooks yet.</p>|]
renderWebhookStatus :: Bool -> Html
renderWebhookStatus True = [hsx|<span class="text-green-600 text-xs">active</span>|]
renderWebhookStatus False = [hsx|<span class="text-gray-400 text-xs">paused</span>|]
renderConsumerStatusDetail :: Bool -> Html
renderConsumerStatusDetail True = [hsx|<span class="bg-green-100 text-green-700 text-sm font-medium px-2 py-0.5 rounded">active</span>|]
renderConsumerStatusDetail False = [hsx|<span class="bg-gray-100 text-gray-500 text-sm font-medium px-2 py-0.5 rounded">inactive</span>|]
renderKeyStatus :: Bool -> Html
renderKeyStatus True = [hsx|<span class="text-red-500 text-xs">revoked</span>|]
renderKeyStatus False = [hsx|<span class="text-green-600 text-xs">active</span>|]
renderRevokeLink :: ApiKey -> Html
renderRevokeLink k
| isNothing k.revokedAt = [hsx|<a href={RevokeApiKeyAction k.id} data-method="post"
data-confirm="Revoke this key? This cannot be undone."
class="text-red-500 hover:text-red-700 text-xs">Revoke</a>|]
| otherwise = mempty