index.twig 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. {% extends 'layout.twig' %}
  2. {% block title %}Tokens — IRDB{% endblock %}
  3. {% block content %}
  4. {% import 'partials/sort.twig' as sort %}
  5. <div class="mx-auto max-w-5xl">
  6. <div class="flex items-center justify-between">
  7. <h1 class="text-2xl font-semibold tracking-tight">API tokens</h1>
  8. <span class="text-sm text-slate-500 dark:text-slate-400">{{ list.total|default(0) }} total</span>
  9. </div>
  10. {% if can_write %}
  11. <section class="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"
  12. x-data="kindSwitcher" data-initial-kind="admin">
  13. <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">Issue token</h2>
  14. <form method="post" action="/app/tokens" class="mt-3 grid grid-cols-1 gap-3 md:grid-cols-3 text-sm">
  15. <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
  16. <div>
  17. <label for="t-kind" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Kind</label>
  18. <select id="t-kind" name="kind" x-model="kind"
  19. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 dark:border-slate-700 dark:bg-slate-950">
  20. <option value="admin">admin</option>
  21. <option value="reporter">reporter</option>
  22. <option value="consumer">consumer</option>
  23. </select>
  24. </div>
  25. <div x-show="isKind('admin')">
  26. <label for="t-role" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Role</label>
  27. <select id="t-role" name="role"
  28. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 dark:border-slate-700 dark:bg-slate-950">
  29. <option value="viewer">viewer</option>
  30. <option value="operator">operator</option>
  31. <option value="admin">admin</option>
  32. </select>
  33. </div>
  34. <div x-show="isKind('reporter')">
  35. <label for="t-rep" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Reporter</label>
  36. <select id="t-rep" name="reporter_id"
  37. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 dark:border-slate-700 dark:bg-slate-950">
  38. <option value="">— pick one —</option>
  39. {% for r in reporters %}
  40. <option value="{{ r.id }}">{{ r.name }}</option>
  41. {% endfor %}
  42. </select>
  43. </div>
  44. <div x-show="isKind('consumer')">
  45. <label for="t-con" class="block text-xs font-medium text-slate-600 dark:text-slate-400">Consumer</label>
  46. <select id="t-con" name="consumer_id"
  47. class="mt-1 w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 dark:border-slate-700 dark:bg-slate-950">
  48. <option value="">— pick one —</option>
  49. {% for c in consumers %}
  50. <option value="{{ c.id }}">{{ c.name }}</option>
  51. {% endfor %}
  52. </select>
  53. </div>
  54. <div class="md:col-span-3 flex justify-end">
  55. <button type="submit" class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-indigo-500">Issue</button>
  56. </div>
  57. </form>
  58. </section>
  59. {% endif %}
  60. <section class="mt-6 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
  61. <table class="w-full text-sm" data-sortable-table="tokens-index">
  62. <thead class="border-b border-slate-200 bg-slate-50 text-left text-xs uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400">
  63. <tr>
  64. {{ sort.th('Kind', 'kind') }}
  65. {{ sort.th('Prefix', 'prefix') }}
  66. {{ sort.th('Role / target', 'role_target') }}
  67. {{ sort.th('Issuer', 'issuer') }}
  68. {{ sort.th('Last used', 'last_used', 'date') }}
  69. {{ sort.th('Status', 'status') }}
  70. {% if can_write %}<th class="px-4 py-2 text-right font-medium">Actions</th>{% endif %}
  71. </tr>
  72. </thead>
  73. <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
  74. {% for t in list.data|default([]) %}
  75. {% set role_target_value = (t.kind == 'admin') ? ('role:' ~ (t.role|default(''))) : ((t.kind == 'reporter') ? ('reporter:' ~ t.reporter_id) : ((t.kind == 'consumer') ? ('consumer:' ~ t.consumer_id) : '')) %}
  76. <tr>
  77. <td class="px-4 py-2 font-mono text-xs uppercase" data-sort-value="{{ t.kind }}">{{ t.kind }}</td>
  78. <td class="px-4 py-2 font-mono" data-sort-value="{{ t.token_prefix }}">{{ t.token_prefix }}</td>
  79. <td class="px-4 py-2 text-slate-600 dark:text-slate-300" data-sort-value="{{ role_target_value }}">
  80. {%- if t.kind == 'admin' -%}role: <span class="font-mono">{{ t.role|default('—') }}</span>
  81. {%- elseif t.kind == 'reporter' -%}reporter #{{ t.reporter_id }}
  82. {%- elseif t.kind == 'consumer' -%}consumer #{{ t.consumer_id }}
  83. {%- else -%}—{%- endif -%}
  84. </td>
  85. <td class="px-4 py-2 text-slate-600 dark:text-slate-300" data-sort-value="{{ t.user_label|default('') }}">
  86. {%- if t.user_label -%}
  87. {{ t.user_label }}
  88. {%- elseif t.user_id -%}
  89. <span class="text-slate-400" title="Issuer was deleted">user #{{ t.user_id }}</span>
  90. {%- else -%}
  91. <span class="text-slate-400" title="Token is not bound to a user (legacy or console-issued)">—</span>
  92. {%- endif -%}
  93. </td>
  94. <td class="px-4 py-2 text-slate-500 dark:text-slate-400" data-sort-value="{{ t.last_used_at|default('') }}">{% if t.last_used_at %}<time class="irdb-dt" datetime="{{ t.last_used_at }}">{{ t.last_used_at }}</time>{% else %}never{% endif %}</td>
  95. <td class="px-4 py-2" data-sort-value="{{ t.revoked_at ? 'revoked' : 'active' }}">
  96. {% if t.revoked_at %}
  97. <span class="rounded bg-slate-100 px-1.5 py-0.5 text-xs uppercase text-slate-500 dark:bg-slate-800 dark:text-slate-400">revoked</span>
  98. {% else %}
  99. <span class="rounded bg-emerald-100 px-1.5 py-0.5 text-xs uppercase text-emerald-800 dark:bg-emerald-900 dark:text-emerald-100">active</span>
  100. {% endif %}
  101. </td>
  102. {% if can_write %}
  103. <td class="px-4 py-2 text-right">
  104. {% if not t.revoked_at %}
  105. {% include 'partials/confirm_form.twig' with {
  106. action: '/app/tokens/' ~ t.id ~ '/delete',
  107. label: 'Revoke',
  108. description: 'Revoke this token immediately. Clients using it will start getting 401.',
  109. } only %}
  110. {% else %}
  111. {% include 'partials/confirm_form.twig' with {
  112. action: '/app/tokens/' ~ t.id ~ '/purge',
  113. label: 'Remove',
  114. description: 'Permanently delete this revoked token row. The audit log entry referring to its prefix will remain.',
  115. } only %}
  116. {% endif %}
  117. </td>
  118. {% endif %}
  119. </tr>
  120. {% else %}
  121. <tr><td colspan="7" class="px-4 py-6 text-center text-slate-400">No tokens.</td></tr>
  122. {% endfor %}
  123. </tbody>
  124. </table>
  125. </section>
  126. </div>
  127. {% if just_created %}
  128. <div x-data="rawTokenCopy" x-show="open"
  129. class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/70 px-4">
  130. <div class="w-full max-w-lg rounded-2xl border border-amber-300 bg-white p-6 shadow-2xl dark:border-amber-700 dark:bg-slate-900">
  131. <h2 class="text-lg font-semibold text-amber-700 dark:text-amber-300">Copy this token now</h2>
  132. <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
  133. This is the only time you'll see the raw token. Refreshing this page or closing the dialog discards it permanently.
  134. If you lose it, revoke it and issue a new one.
  135. </p>
  136. <div class="mt-4">
  137. <label class="block text-xs font-medium text-slate-500 dark:text-slate-400">Kind: <span class="font-mono">{{ just_created.kind }}</span> · prefix: <span class="font-mono">{{ just_created.token_prefix }}</span></label>
  138. <div class="mt-1 flex items-center gap-2">
  139. <input id="raw-token" type="text" readonly value="{{ just_created.raw_token }}"
  140. class="w-full rounded-md border border-slate-300 bg-slate-50 px-3 py-2 font-mono text-xs dark:border-slate-700 dark:bg-slate-950">
  141. <button type="button"
  142. x-on:click="copy()"
  143. class="rounded-md border border-slate-300 px-3 py-2 text-xs hover:bg-slate-50 dark:border-slate-700 dark:hover:bg-slate-800">Copy</button>
  144. </div>
  145. </div>
  146. <div class="mt-6 flex justify-end">
  147. <button type="button" x-on:click="hide()"
  148. class="rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500">I have stored it safely</button>
  149. </div>
  150. </div>
  151. </div>
  152. {% endif %}
  153. {% endblock %}