@dudousxd/adonis-authkit-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/build/assets/grafana/authkit-dashboard.json +118 -0
  4. package/build/commands/commands.json +30 -0
  5. package/build/commands/configure.d.ts +2 -0
  6. package/build/commands/configure.js +42 -0
  7. package/build/commands/eject.d.ts +11 -0
  8. package/build/commands/eject.js +96 -0
  9. package/build/commands/main.d.ts +12 -0
  10. package/build/commands/main.js +38 -0
  11. package/build/commands/ui_preset.d.ts +4 -0
  12. package/build/commands/ui_preset.js +32 -0
  13. package/build/database/migrations/make_authkit_oidc_table.d.ts +6 -0
  14. package/build/database/migrations/make_authkit_oidc_table.js +19 -0
  15. package/build/host/views/account/login.edge +29 -0
  16. package/build/host/views/account/mfa.edge +151 -0
  17. package/build/host/views/account/tokens.edge +70 -0
  18. package/build/host/views/admin/audit.edge +72 -0
  19. package/build/host/views/admin/clients.edge +51 -0
  20. package/build/host/views/admin/dashboard.edge +58 -0
  21. package/build/host/views/admin/users.edge +76 -0
  22. package/build/host/views/consent.edge +19 -0
  23. package/build/host/views/forgot.edge +30 -0
  24. package/build/host/views/login.edge +91 -0
  25. package/build/host/views/mfa-challenge.edge +88 -0
  26. package/build/host/views/reset.edge +29 -0
  27. package/build/host/views/signup.edge +44 -0
  28. package/build/host/views/verify-email.edge +16 -0
  29. package/build/index.d.ts +42 -0
  30. package/build/index.js +28 -0
  31. package/build/providers/authkit_server_provider.d.ts +19 -0
  32. package/build/providers/authkit_server_provider.js +81 -0
  33. package/build/src/accounts/account_store.d.ts +136 -0
  34. package/build/src/accounts/account_store.js +1 -0
  35. package/build/src/accounts/lucid_account_store.d.ts +75 -0
  36. package/build/src/accounts/lucid_account_store.js +396 -0
  37. package/build/src/adapters/adapter_contract.d.ts +18 -0
  38. package/build/src/adapters/adapter_contract.js +1 -0
  39. package/build/src/adapters/database_adapter.d.ts +15 -0
  40. package/build/src/adapters/database_adapter.js +63 -0
  41. package/build/src/adapters/factory.d.ts +30 -0
  42. package/build/src/adapters/factory.js +43 -0
  43. package/build/src/adapters/redis_adapter.d.ts +16 -0
  44. package/build/src/adapters/redis_adapter.js +95 -0
  45. package/build/src/audit/audit_sink.d.ts +54 -0
  46. package/build/src/audit/audit_sink.js +1 -0
  47. package/build/src/audit/lucid_audit_sink.d.ts +10 -0
  48. package/build/src/audit/lucid_audit_sink.js +60 -0
  49. package/build/src/controllers/oidc_callback_controller.d.ts +22 -0
  50. package/build/src/controllers/oidc_callback_controller.js +33 -0
  51. package/build/src/define_config.d.ts +261 -0
  52. package/build/src/define_config.js +115 -0
  53. package/build/src/host/account_lockout.d.ts +86 -0
  54. package/build/src/host/account_lockout.js +185 -0
  55. package/build/src/host/augmentations.d.ts +1 -0
  56. package/build/src/host/augmentations.js +1 -0
  57. package/build/src/host/branding.d.ts +17 -0
  58. package/build/src/host/branding.js +8 -0
  59. package/build/src/host/controllers/account_mfa_controller.d.ts +30 -0
  60. package/build/src/host/controllers/account_mfa_controller.js +157 -0
  61. package/build/src/host/controllers/account_session_controller.d.ts +7 -0
  62. package/build/src/host/controllers/account_session_controller.js +50 -0
  63. package/build/src/host/controllers/account_tokens_controller.d.ts +7 -0
  64. package/build/src/host/controllers/account_tokens_controller.js +55 -0
  65. package/build/src/host/controllers/admin/admin_audit_controller.d.ts +10 -0
  66. package/build/src/host/controllers/admin/admin_audit_controller.js +56 -0
  67. package/build/src/host/controllers/admin/admin_clients_controller.d.ts +10 -0
  68. package/build/src/host/controllers/admin/admin_clients_controller.js +24 -0
  69. package/build/src/host/controllers/admin/admin_dashboard_controller.d.ts +10 -0
  70. package/build/src/host/controllers/admin/admin_dashboard_controller.js +27 -0
  71. package/build/src/host/controllers/admin/admin_users_controller.d.ts +11 -0
  72. package/build/src/host/controllers/admin/admin_users_controller.js +53 -0
  73. package/build/src/host/controllers/interaction_controller.d.ts +44 -0
  74. package/build/src/host/controllers/interaction_controller.js +304 -0
  75. package/build/src/host/controllers/pat_introspection_controller.d.ts +22 -0
  76. package/build/src/host/controllers/pat_introspection_controller.js +46 -0
  77. package/build/src/host/controllers/registration_controller.d.ts +18 -0
  78. package/build/src/host/controllers/registration_controller.js +169 -0
  79. package/build/src/host/controllers/social_controller.d.ts +8 -0
  80. package/build/src/host/controllers/social_controller.js +82 -0
  81. package/build/src/host/default_mailer.d.ts +39 -0
  82. package/build/src/host/default_mailer.js +141 -0
  83. package/build/src/host/email_templates.d.ts +35 -0
  84. package/build/src/host/email_templates.js +66 -0
  85. package/build/src/host/i18n.d.ts +178 -0
  86. package/build/src/host/i18n.js +208 -0
  87. package/build/src/host/middleware/account_auth.d.ts +7 -0
  88. package/build/src/host/middleware/account_auth.js +11 -0
  89. package/build/src/host/rate_limit.d.ts +32 -0
  90. package/build/src/host/rate_limit.js +87 -0
  91. package/build/src/host/register_auth_host.d.ts +41 -0
  92. package/build/src/host/register_auth_host.js +133 -0
  93. package/build/src/host/renderers/edge_renderer.d.ts +3 -0
  94. package/build/src/host/renderers/edge_renderer.js +29 -0
  95. package/build/src/host/renderers/inertia_renderer.d.ts +5 -0
  96. package/build/src/host/renderers/inertia_renderer.js +26 -0
  97. package/build/src/host/validators.d.ts +39 -0
  98. package/build/src/host/validators.js +13 -0
  99. package/build/src/keys/jwks_manager.d.ts +6 -0
  100. package/build/src/keys/jwks_manager.js +11 -0
  101. package/build/src/mixins/with_audit_log.d.ts +19 -0
  102. package/build/src/mixins/with_audit_log.js +41 -0
  103. package/build/src/mixins/with_auth_user.d.ts +18 -0
  104. package/build/src/mixins/with_auth_user.js +39 -0
  105. package/build/src/mixins/with_credentials.d.ts +20 -0
  106. package/build/src/mixins/with_credentials.js +29 -0
  107. package/build/src/mixins/with_mfa.d.ts +31 -0
  108. package/build/src/mixins/with_mfa.js +39 -0
  109. package/build/src/mixins/with_personal_access_token.d.ts +19 -0
  110. package/build/src/mixins/with_personal_access_token.js +44 -0
  111. package/build/src/mixins/with_provider_identity.d.ts +20 -0
  112. package/build/src/mixins/with_provider_identity.js +32 -0
  113. package/build/src/mixins/with_webauthn_credential.d.ts +37 -0
  114. package/build/src/mixins/with_webauthn_credential.js +49 -0
  115. package/build/src/observability/metrics_controller.d.ts +5 -0
  116. package/build/src/observability/metrics_controller.js +24 -0
  117. package/build/src/observability/metrics_service.d.ts +2 -0
  118. package/build/src/observability/metrics_service.js +7 -0
  119. package/build/src/observability/otel_recorder.d.ts +10 -0
  120. package/build/src/observability/otel_recorder.js +59 -0
  121. package/build/src/observability/wire_provider_events.d.ts +12 -0
  122. package/build/src/observability/wire_provider_events.js +19 -0
  123. package/build/src/pat/lucid_pat_store.d.ts +6 -0
  124. package/build/src/pat/lucid_pat_store.js +62 -0
  125. package/build/src/pat/pat_store.d.ts +31 -0
  126. package/build/src/pat/pat_store.js +1 -0
  127. package/build/src/pat/pat_tokens.d.ts +4 -0
  128. package/build/src/pat/pat_tokens.js +9 -0
  129. package/build/src/provider/build_provider.d.ts +8 -0
  130. package/build/src/provider/build_provider.js +101 -0
  131. package/build/src/provider/interaction_actions.d.ts +21 -0
  132. package/build/src/provider/interaction_actions.js +32 -0
  133. package/build/src/provider/oidc_service.d.ts +17 -0
  134. package/build/src/provider/oidc_service.js +84 -0
  135. package/build/src/provider/token_exchange.d.ts +15 -0
  136. package/build/src/provider/token_exchange.js +72 -0
  137. package/build/src/register_routes.d.ts +16 -0
  138. package/build/src/register_routes.js +21 -0
  139. package/build/stubs/config/authkit.stub +29 -0
  140. package/build/stubs/main.d.ts +1 -0
  141. package/build/stubs/main.js +2 -0
  142. package/build/stubs/models/auth_user.stub +13 -0
  143. package/build/stubs/ui/edge/views/consent.edge +13 -0
  144. package/build/stubs/ui/edge/views/login.edge +19 -0
  145. package/build/stubs/ui/react/components/auth_shell.tsx +67 -0
  146. package/build/stubs/ui/react/pages/account/login.tsx +56 -0
  147. package/build/stubs/ui/react/pages/account/mfa.tsx +132 -0
  148. package/build/stubs/ui/react/pages/account/tokens.tsx +88 -0
  149. package/build/stubs/ui/react/pages/consent.tsx +39 -0
  150. package/build/stubs/ui/react/pages/forgot.tsx +44 -0
  151. package/build/stubs/ui/react/pages/login.tsx +171 -0
  152. package/build/stubs/ui/react/pages/mfa-challenge.tsx +72 -0
  153. package/build/stubs/ui/react/pages/reset.tsx +58 -0
  154. package/build/stubs/ui/react/pages/signup.tsx +78 -0
  155. package/build/stubs/ui/react/pages/verify-email.tsx +24 -0
  156. package/build/types.d.ts +7 -0
  157. package/build/types.js +1 -0
  158. package/package.json +108 -0
  159. package/stubs/config/authkit.stub +29 -0
  160. package/stubs/main.ts +2 -0
  161. package/stubs/models/auth_user.stub +13 -0
  162. package/stubs/ui/edge/views/consent.edge +13 -0
  163. package/stubs/ui/edge/views/login.edge +19 -0
  164. package/stubs/ui/react/components/auth_shell.tsx +67 -0
  165. package/stubs/ui/react/pages/account/login.tsx +56 -0
  166. package/stubs/ui/react/pages/account/mfa.tsx +132 -0
  167. package/stubs/ui/react/pages/account/tokens.tsx +88 -0
  168. package/stubs/ui/react/pages/consent.tsx +39 -0
  169. package/stubs/ui/react/pages/forgot.tsx +44 -0
  170. package/stubs/ui/react/pages/login.tsx +171 -0
  171. package/stubs/ui/react/pages/mfa-challenge.tsx +72 -0
  172. package/stubs/ui/react/pages/reset.tsx +58 -0
  173. package/stubs/ui/react/pages/signup.tsx +78 -0
  174. package/stubs/ui/react/pages/verify-email.tsx +24 -0
@@ -0,0 +1,151 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('account.mfa.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-2xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('account.mfa.title') }}</h1>
10
+ </div>
11
+ <form method="POST" action="/account/logout">
12
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('account.mfa.logout') }}</button>
14
+ </form>
15
+ </div>
16
+
17
+ @if(error)
18
+ <p class="mb-4 text-sm text-red-600">{{ error }}</p>
19
+ @end
20
+
21
+ {{-- Recovery codes mostrados uma única vez após confirmar o enrollment --}}
22
+ @if(recoveryCodes && recoveryCodes.length > 0)
23
+ <div class="mb-6 rounded-lg border border-emerald-300 bg-emerald-50 p-4">
24
+ <p class="text-sm font-medium text-emerald-900">
25
+ {{ t('account.mfa.recovery_codes_notice') }}
26
+ </p>
27
+ <ul class="mt-3 grid grid-cols-2 gap-2">
28
+ @each(rc in recoveryCodes)
29
+ <li><code class="block rounded bg-white px-3 py-2 text-sm text-emerald-800 ring-1 ring-emerald-200">{{ rc }}</code></li>
30
+ @end
31
+ </ul>
32
+ </div>
33
+ @end
34
+
35
+ @if(enrolling)
36
+ <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
37
+ <p class="text-sm text-gray-600">
38
+ {{ t('account.mfa.enroll_intro') }}
39
+ </p>
40
+ @if(qrDataUrl)
41
+ <img src="{{ qrDataUrl }}" alt="{{ t('account.mfa.qr_alt') }}" class="mx-auto my-4 h-48 w-48" />
42
+ @end
43
+ @if(secret)
44
+ <p class="text-center text-xs text-gray-500">{{ t('account.mfa.manual_intro') }}</p>
45
+ <code class="mx-auto mt-1 block w-fit break-all rounded bg-gray-100 px-3 py-2 text-sm text-gray-800">{{ secret }}</code>
46
+ @end
47
+
48
+ <form method="POST" action="/account/mfa/confirm" class="mt-6">
49
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
50
+ <label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.mfa.confirm_code_label') }}</label>
51
+ <input id="code" name="code" inputmode="numeric" pattern="[0-9]*" maxlength="6" autofocus
52
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-center text-lg tracking-[0.4em] outline-none focus:border-gray-900" />
53
+ <button type="submit" class="mt-4 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white">
54
+ {{ t('account.mfa.activate') }}
55
+ </button>
56
+ </form>
57
+ </div>
58
+ @elseif(enabled)
59
+ <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
60
+ <p class="text-sm text-gray-700">
61
+ {{{ t('account.mfa.enabled_html') }}}
62
+ </p>
63
+ <form method="POST" action="/account/mfa/disable" class="mt-4">
64
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
65
+ <button type="submit" class="rounded-lg border border-red-300 px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50">
66
+ {{ t('account.mfa.disable') }}
67
+ </button>
68
+ </form>
69
+ </div>
70
+ @else
71
+ <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
72
+ <p class="text-sm text-gray-700">
73
+ {{ t('account.mfa.disabled_intro') }}
74
+ </p>
75
+ <form method="POST" action="/account/mfa/enroll" class="mt-4">
76
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
77
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 py-2 text-sm font-semibold text-white">
78
+ {{ t('account.mfa.enable') }}
79
+ </button>
80
+ </form>
81
+ </div>
82
+ @end
83
+
84
+ @if(passkeysSupported)
85
+ {{-- Passkeys / chaves de acesso (WebAuthn) — 2º fator alternativo ao TOTP. --}}
86
+ <div class="mt-6 rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
87
+ <h2 class="text-base font-semibold text-gray-900">{{ t('mfa.passkey.section_title') }}</h2>
88
+ <p class="mt-1 text-sm text-gray-600">{{ t('mfa.passkey.section_intro') }}</p>
89
+
90
+ @if(passkeys && passkeys.length > 0)
91
+ <ul class="mt-4 space-y-2">
92
+ @each(pk in passkeys)
93
+ <li class="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2">
94
+ <span class="text-sm text-gray-800">{{ pk.label ? pk.label : t('mfa.passkey.unnamed') }}</span>
95
+ <form method="POST" action="/account/mfa/passkeys/{{ pk.id }}/remove">
96
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
97
+ <button type="submit" class="text-sm text-red-600 hover:underline">{{ t('mfa.passkey.remove') }}</button>
98
+ </form>
99
+ </li>
100
+ @end
101
+ </ul>
102
+ @else
103
+ <p class="mt-4 text-sm text-gray-400">{{ t('mfa.passkey.empty') }}</p>
104
+ @end
105
+
106
+ <button type="button" id="passkey-add"
107
+ class="mt-4 rounded-lg border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 hover:bg-gray-50">
108
+ {{ t('mfa.passkey.add') }}
109
+ </button>
110
+ <p id="passkey-error" class="mt-3 hidden text-sm text-red-600">{{ t('mfa.passkey.register_error') }}</p>
111
+ </div>
112
+ @end
113
+ </div>
114
+
115
+ @if(passkeysSupported)
116
+ <script type="module">
117
+ import { startRegistration } from 'https://cdn.jsdelivr.net/npm/@simplewebauthn/browser@13/dist/bundle/index.js'
118
+ const btn = document.getElementById('passkey-add')
119
+ const errEl = document.getElementById('passkey-error')
120
+ const csrf = '{{ csrfToken }}'
121
+ btn?.addEventListener('click', async () => {
122
+ errEl.classList.add('hidden')
123
+ btn.disabled = true
124
+ try {
125
+ // 1) Opções de registro (challenge guardado na sessão server-side).
126
+ const optsRes = await fetch('/account/mfa/passkeys/options', {
127
+ method: 'POST',
128
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrf },
129
+ body: JSON.stringify({}),
130
+ })
131
+ if (!optsRes.ok) throw new Error('options')
132
+ const optionsJSON = await optsRes.json()
133
+ // 2) Cerimônia de registro no authenticator.
134
+ const attResp = await startRegistration({ optionsJSON })
135
+ // 3) Verifica/persiste no servidor.
136
+ const verifyRes = await fetch('/account/mfa/passkeys/verify', {
137
+ method: 'POST',
138
+ headers: { 'content-type': 'application/json', 'x-csrf-token': csrf },
139
+ body: JSON.stringify({ response: attResp }),
140
+ })
141
+ if (!verifyRes.ok) throw new Error('verify')
142
+ // 4) Recarrega para mostrar a nova passkey.
143
+ window.location.reload()
144
+ } catch (e) {
145
+ errEl.classList.remove('hidden')
146
+ btn.disabled = false
147
+ }
148
+ })
149
+ </script>
150
+ @end
151
+ </body></html>
@@ -0,0 +1,70 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('account.tokens.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-2xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('account.tokens.title') }}</h1>
10
+ </div>
11
+ <form method="POST" action="/account/logout">
12
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('account.tokens.logout') }}</button>
14
+ </form>
15
+ </div>
16
+
17
+ @if(createdToken)
18
+ <div class="mb-6 rounded-lg border border-emerald-300 bg-emerald-50 p-4">
19
+ <p class="text-sm font-medium text-emerald-900">
20
+ {{ t('account.tokens.created_notice') }}
21
+ </p>
22
+ <code class="mt-2 block break-all rounded bg-white px-3 py-2 text-sm text-emerald-800 ring-1 ring-emerald-200">
23
+ {{ createdToken }}
24
+ </code>
25
+ </div>
26
+ @end
27
+
28
+ <form method="POST" action="/account/tokens"
29
+ class="mb-6 flex gap-2 rounded-xl bg-white p-4 shadow-sm ring-1 ring-black/5">
30
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
31
+ <input name="name" placeholder="{{ t('account.tokens.name_placeholder') }}"
32
+ class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
33
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 text-sm font-semibold text-white">
34
+ {{ t('account.tokens.create') }}
35
+ </button>
36
+ </form>
37
+
38
+ <div class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
39
+ @if(tokens.length === 0)
40
+ <p class="p-6 text-sm text-gray-500">{{ t('account.tokens.empty') }}</p>
41
+ @else
42
+ @each(token in tokens)
43
+ <div class="flex items-center justify-between border-b border-gray-100 p-4 last:border-0">
44
+ <div>
45
+ <p class="text-sm font-medium text-gray-900">{{ token.name }}</p>
46
+ <p class="text-xs text-gray-500">
47
+ {{ t('account.tokens.created_at', { date: token.createdAt }) }}
48
+ @if(token.lastUsedAt)
49
+ {{ t('account.tokens.last_used', { date: token.lastUsedAt }) }}
50
+ @else
51
+ {{ t('account.tokens.never_used') }}
52
+ @end
53
+ </p>
54
+ @if(token.scopes && token.scopes.length > 0)
55
+ <p class="text-xs text-gray-400">{{ t('account.tokens.scopes', { scopes: token.scopes.join(', ') }) }}</p>
56
+ @end
57
+ @if(token.audience)
58
+ <p class="text-xs text-gray-400">{{ t('account.tokens.audience', { audience: token.audience }) }}</p>
59
+ @end
60
+ </div>
61
+ <form method="POST" action="/account/tokens/{{ token.id }}/revoke">
62
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
63
+ <button type="submit" class="text-sm text-red-600 hover:underline">{{ t('account.tokens.revoke') }}</button>
64
+ </form>
65
+ </div>
66
+ @end
67
+ @end
68
+ </div>
69
+ </div>
70
+ </body></html>
@@ -0,0 +1,72 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('admin.audit.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-4xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('admin.audit.title') }}</h1>
10
+ </div>
11
+ <form method="POST" action="/account/logout">
12
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('admin.nav.logout') }}</button>
14
+ </form>
15
+ </div>
16
+
17
+ <nav class="mb-6 flex gap-4 text-sm font-medium">
18
+ <a href="/admin" class="text-gray-500 hover:underline">{{ t('admin.nav.dashboard') }}</a>
19
+ <a href="/admin/users" class="text-gray-500 hover:underline">{{ t('admin.nav.users') }}</a>
20
+ <a href="/admin/clients" class="text-gray-500 hover:underline">{{ t('admin.nav.clients') }}</a>
21
+ <a href="/admin/audit" class="text-gray-900 underline">{{ t('admin.nav.audit') }}</a>
22
+ </nav>
23
+
24
+ @if(!supported)
25
+ <div class="rounded-xl bg-white p-6 text-sm text-gray-500 shadow-sm ring-1 ring-black/5">
26
+ {{ t('admin.audit.not_supported') }}
27
+ </div>
28
+ @else
29
+ <form method="GET" action="/admin/audit" class="mb-6 flex gap-2">
30
+ <input name="type" value="{{ type }}" placeholder="{{ t('admin.audit.type_placeholder') }}"
31
+ class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
32
+ <input name="subject" value="{{ subject }}" placeholder="{{ t('admin.audit.subject_placeholder') }}"
33
+ class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
34
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 text-sm font-semibold text-white">
35
+ {{ t('admin.audit.filter') }}
36
+ </button>
37
+ </form>
38
+
39
+ <div class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
40
+ @if(events.length === 0)
41
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.audit.empty') }}</p>
42
+ @else
43
+ @each(event in events)
44
+ <div class="border-b border-gray-100 p-4 last:border-0">
45
+ <div class="flex items-center justify-between">
46
+ <p class="text-sm font-medium text-gray-900">{{ event.type }}</p>
47
+ <p class="text-xs text-gray-400">{{ event.createdAt }}</p>
48
+ </div>
49
+ <p class="mt-1 text-xs text-gray-500">
50
+ {{ [event.email, event.accountId, event.clientId, event.ip].filter((v) => v).join(' · ') }}
51
+ </p>
52
+ </div>
53
+ @end
54
+ @end
55
+ </div>
56
+
57
+ @if(totalPages > 1)
58
+ <div class="mt-4 flex items-center justify-between text-sm text-gray-500">
59
+ <span>{{ t('admin.pagination.page', { page: page, total: totalPages }) }}</span>
60
+ <div class="flex gap-2">
61
+ @if(page > 1)
62
+ <a href="/admin/audit?type={{ type }}&subject={{ subject }}&page={{ page - 1 }}" class="rounded border border-gray-300 px-3 py-1 hover:bg-white">{{ t('admin.pagination.prev') }}</a>
63
+ @end
64
+ @if(page < totalPages)
65
+ <a href="/admin/audit?type={{ type }}&subject={{ subject }}&page={{ page + 1 }}" class="rounded border border-gray-300 px-3 py-1 hover:bg-white">{{ t('admin.pagination.next') }}</a>
66
+ @end
67
+ </div>
68
+ </div>
69
+ @end
70
+ @end
71
+ </div>
72
+ </body></html>
@@ -0,0 +1,51 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('admin.clients.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-4xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('admin.clients.title') }}</h1>
10
+ </div>
11
+ <form method="POST" action="/account/logout">
12
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('admin.nav.logout') }}</button>
14
+ </form>
15
+ </div>
16
+
17
+ <nav class="mb-6 flex gap-4 text-sm font-medium">
18
+ <a href="/admin" class="text-gray-500 hover:underline">{{ t('admin.nav.dashboard') }}</a>
19
+ <a href="/admin/users" class="text-gray-500 hover:underline">{{ t('admin.nav.users') }}</a>
20
+ <a href="/admin/clients" class="text-gray-900 underline">{{ t('admin.nav.clients') }}</a>
21
+ <a href="/admin/audit" class="text-gray-500 hover:underline">{{ t('admin.nav.audit') }}</a>
22
+ </nav>
23
+
24
+ @if(dynamicEnabled)
25
+ <div class="mb-6 rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900">
26
+ {{ t('admin.clients.dynamic_notice') }}
27
+ </div>
28
+ @end
29
+
30
+ <div class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
31
+ @if(clients.length === 0)
32
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.clients.empty') }}</p>
33
+ @else
34
+ @each(client in clients)
35
+ <div class="border-b border-gray-100 p-4 last:border-0">
36
+ <div class="flex items-center justify-between">
37
+ <p class="text-sm font-medium text-gray-900">{{ client.clientId }}</p>
38
+ <span class="rounded-full px-2 py-0.5 text-xs {{ client.confidential ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-600' }}">
39
+ {{ client.confidential ? t('admin.clients.confidential') : t('admin.clients.public') }}
40
+ </span>
41
+ </div>
42
+ <p class="mt-1 text-xs text-gray-500">{{ t('admin.clients.grants', { grants: client.grants.join(', ') }) }}</p>
43
+ @if(client.redirectUris.length > 0)
44
+ <p class="text-xs text-gray-400">{{ t('admin.clients.redirect_uris', { uris: client.redirectUris.join(', ') }) }}</p>
45
+ @end
46
+ </div>
47
+ @end
48
+ @end
49
+ </div>
50
+ </div>
51
+ </body></html>
@@ -0,0 +1,58 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('admin.dashboard.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-4xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('admin.dashboard.title') }}</h1>
10
+ </div>
11
+ <form method="POST" action="/account/logout">
12
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('admin.nav.logout') }}</button>
14
+ </form>
15
+ </div>
16
+
17
+ <nav class="mb-6 flex gap-4 text-sm font-medium">
18
+ <a href="/admin" class="text-gray-900 underline">{{ t('admin.nav.dashboard') }}</a>
19
+ <a href="/admin/users" class="text-gray-500 hover:underline">{{ t('admin.nav.users') }}</a>
20
+ <a href="/admin/clients" class="text-gray-500 hover:underline">{{ t('admin.nav.clients') }}</a>
21
+ <a href="/admin/audit" class="text-gray-500 hover:underline">{{ t('admin.nav.audit') }}</a>
22
+ </nav>
23
+
24
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
25
+ <a href="/admin/users" class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5 hover:ring-gray-900/20">
26
+ <p class="text-xs uppercase tracking-wide text-gray-400">{{ t('admin.dashboard.users_count') }}</p>
27
+ <p class="mt-1 text-2xl font-semibold text-gray-900">{{ usersTotal }}</p>
28
+ </a>
29
+ <a href="/admin/clients" class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5 hover:ring-gray-900/20">
30
+ <p class="text-xs uppercase tracking-wide text-gray-400">{{ t('admin.dashboard.clients_count') }}</p>
31
+ <p class="mt-1 text-2xl font-semibold text-gray-900">{{ clientsCount }}</p>
32
+ </a>
33
+ <a href="/admin/audit" class="rounded-xl bg-white p-5 shadow-sm ring-1 ring-black/5 hover:ring-gray-900/20">
34
+ <p class="text-xs uppercase tracking-wide text-gray-400">{{ t('admin.dashboard.audit_count') }}</p>
35
+ <p class="mt-1 text-2xl font-semibold text-gray-900">{{ auditSupported ? auditTotal : '—' }}</p>
36
+ </a>
37
+ </div>
38
+
39
+ <h2 class="mt-8 mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500">{{ t('admin.dashboard.recent_title') }}</h2>
40
+ <div class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
41
+ @if(!auditSupported)
42
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.audit.not_supported') }}</p>
43
+ @elseif(recentEvents.length === 0)
44
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.audit.empty') }}</p>
45
+ @else
46
+ @each(event in recentEvents)
47
+ <div class="flex items-center justify-between border-b border-gray-100 p-4 last:border-0">
48
+ <div>
49
+ <p class="text-sm font-medium text-gray-900">{{ event.type }}</p>
50
+ <p class="text-xs text-gray-500">{{ event.email ?? event.accountId ?? '—' }}</p>
51
+ </div>
52
+ <p class="text-xs text-gray-400">{{ event.createdAt }}</p>
53
+ </div>
54
+ @end
55
+ @end
56
+ </div>
57
+ </div>
58
+ </body></html>
@@ -0,0 +1,76 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('admin.users.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-4xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('admin.users.title') }}</h1>
10
+ </div>
11
+ <form method="POST" action="/account/logout">
12
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('admin.nav.logout') }}</button>
14
+ </form>
15
+ </div>
16
+
17
+ <nav class="mb-6 flex gap-4 text-sm font-medium">
18
+ <a href="/admin" class="text-gray-500 hover:underline">{{ t('admin.nav.dashboard') }}</a>
19
+ <a href="/admin/users" class="text-gray-900 underline">{{ t('admin.nav.users') }}</a>
20
+ <a href="/admin/clients" class="text-gray-500 hover:underline">{{ t('admin.nav.clients') }}</a>
21
+ <a href="/admin/audit" class="text-gray-500 hover:underline">{{ t('admin.nav.audit') }}</a>
22
+ </nav>
23
+
24
+ <form method="GET" action="/admin/users" class="mb-6 flex gap-2">
25
+ <input name="search" value="{{ search }}" placeholder="{{ t('admin.users.search_placeholder') }}"
26
+ class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
27
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 text-sm font-semibold text-white">
28
+ {{ t('admin.users.search') }}
29
+ </button>
30
+ </form>
31
+
32
+ <div class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
33
+ @if(users.length === 0)
34
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.users.empty') }}</p>
35
+ @else
36
+ @each(user in users)
37
+ <div class="border-b border-gray-100 p-4 last:border-0">
38
+ <div class="flex items-center justify-between">
39
+ <div>
40
+ <p class="text-sm font-medium text-gray-900">{{ user.email }}</p>
41
+ @if(user.name)
42
+ <p class="text-xs text-gray-500">{{ user.name }}</p>
43
+ @end
44
+ </div>
45
+ </div>
46
+ <form method="POST" action="/admin/users/{{ user.id }}/roles" class="mt-3 flex gap-2">
47
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
48
+ <input type="hidden" name="search" value="{{ search }}">
49
+ <input type="hidden" name="page" value="{{ page }}">
50
+ <input name="roles" value="{{ user.rolesText }}"
51
+ placeholder="{{ t('admin.users.roles_placeholder') }}"
52
+ class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
53
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 text-sm font-semibold text-white">
54
+ {{ t('admin.users.save_roles') }}
55
+ </button>
56
+ </form>
57
+ </div>
58
+ @end
59
+ @end
60
+ </div>
61
+
62
+ @if(totalPages > 1)
63
+ <div class="mt-4 flex items-center justify-between text-sm text-gray-500">
64
+ <span>{{ t('admin.pagination.page', { page: page, total: totalPages }) }}</span>
65
+ <div class="flex gap-2">
66
+ @if(page > 1)
67
+ <a href="/admin/users?search={{ search }}&page={{ page - 1 }}" class="rounded border border-gray-300 px-3 py-1 hover:bg-white">{{ t('admin.pagination.prev') }}</a>
68
+ @end
69
+ @if(page < totalPages)
70
+ <a href="/admin/users?search={{ search }}&page={{ page + 1 }}" class="rounded border border-gray-300 px-3 py-1 hover:bg-white">{{ t('admin.pagination.next') }}</a>
71
+ @end
72
+ </div>
73
+ </div>
74
+ @end
75
+ </div>
76
+ </body></html>
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('consent.page_title') }} — {{ brand && brand.appName ? brand.appName : t('common.app_fallback') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen flex items-center justify-center bg-gray-50">
5
+ <div class="w-full max-w-sm bg-white p-8 rounded-2xl shadow-xl ring-1 ring-black/5">
6
+ <form method="POST" action="/auth/interaction/{{ uid }}/consent">
7
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
8
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('consent.title') }}</h1>
9
+ <p class="mt-2 text-sm text-gray-600">
10
+ {{{ t('consent.body', { app: brand && brand.appName ? brand.appName : params.client_id }) }}}
11
+ </p>
12
+
13
+ <button type="submit"
14
+ class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
15
+ {{ t('consent.submit') }}
16
+ </button>
17
+ </form>
18
+ </div>
19
+ </body></html>
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('forgot.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen flex items-center justify-center bg-gray-50">
5
+ <div class="w-full max-w-sm bg-white p-8 rounded-2xl shadow-xl ring-1 ring-black/5">
6
+ @if(sent)
7
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('forgot.sent_title') }}</h1>
8
+ <p class="mt-2 text-sm text-gray-600">
9
+ {{ t('forgot.sent_body') }}
10
+ </p>
11
+ @else
12
+ <form method="POST" action="/auth/forgot-password">
13
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
14
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('forgot.title') }}</h1>
15
+ <p class="mt-1 text-sm text-gray-500">{{ t('forgot.intro') }}</p>
16
+
17
+ <div class="mt-6">
18
+ <label for="email" class="mb-1 block text-sm font-medium text-gray-700">{{ t('forgot.email_label') }}</label>
19
+ <input id="email" name="email" type="email" required autofocus
20
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
21
+ </div>
22
+
23
+ <button type="submit"
24
+ class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
25
+ {{ t('forgot.submit') }}
26
+ </button>
27
+ </form>
28
+ @end
29
+ </div>
30
+ </body></html>
@@ -0,0 +1,91 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('login.page_title') }} — {{ brand && brand.appName ? brand.appName : t('common.app_fallback') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen flex items-center justify-center bg-gray-50">
5
+ @if(step === 'identifier')
6
+ <div class="w-full max-w-sm bg-white p-8 rounded-2xl shadow-xl ring-1 ring-black/5">
7
+ <form method="POST" action="/auth/interaction/{{ uid }}/identifier">
8
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('login.title') }}</h1>
10
+ <p class="mt-1 text-sm text-gray-500">{{ t('login.identifier_intro') }}</p>
11
+
12
+ @if(error)
13
+ <p class="mt-4 text-sm text-red-600">{{ error }}</p>
14
+ @end
15
+
16
+ <div class="mt-6">
17
+ <label for="email" class="mb-1 block text-sm font-medium text-gray-700">{{ t('login.email_label') }}</label>
18
+ <input id="email" name="email" type="email" required autofocus
19
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
20
+ </div>
21
+
22
+ <button type="submit"
23
+ class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
24
+ {{ t('login.identifier_submit') }}
25
+ </button>
26
+
27
+ <div class="mt-4 flex justify-between text-sm text-gray-600">
28
+ <a href="/auth/interaction/{{ uid }}/signup" class="hover:underline">{{ t('login.create_account') }}</a>
29
+ <a href="/auth/forgot-password" class="hover:underline">{{ t('login.forgot_password') }}</a>
30
+ </div>
31
+ </form>
32
+
33
+ <div class="my-6 flex items-center gap-3 text-xs text-gray-400">
34
+ <span class="h-px flex-1 bg-gray-200"></span>
35
+ {{ t('login.divider_or') }}
36
+ <span class="h-px flex-1 bg-gray-200"></span>
37
+ </div>
38
+
39
+ <a href="/auth/google/redirect/{{ uid }}"
40
+ class="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white py-2.5 text-sm font-medium text-gray-700 transition hover:bg-gray-50">
41
+ <svg class="h-4 w-4" viewBox="0 0 24 24" aria-hidden="true">
42
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1Z"/>
43
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23Z"/>
44
+ <path fill="#FBBC05" d="M5.84 14.1a6.6 6.6 0 0 1 0-4.2V7.06H2.18a11 11 0 0 0 0 9.88l3.66-2.84Z"/>
45
+ <path fill="#EA4335" d="M12 4.75c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 1.46 14.97.5 12 .5A11 11 0 0 0 2.18 7.06l3.66 2.84C6.71 7.3 9.14 4.75 12 4.75Z"/>
46
+ </svg>
47
+ {{ t('login.google') }}
48
+ </a>
49
+ </div>
50
+ @else
51
+ <div class="w-full max-w-sm bg-white p-8 rounded-2xl shadow-xl ring-1 ring-black/5">
52
+ <form method="POST" action="/auth/interaction/{{ uid }}/login">
53
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
54
+
55
+ @if(account && account.fullName)
56
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('login.greeting', { name: account.fullName }) }}</h1>
57
+ @else
58
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('login.title') }}</h1>
59
+ @end
60
+
61
+ <div class="mt-1 flex items-center gap-2">
62
+ @if(email)
63
+ <span class="text-sm text-gray-500">{{ email }}</span>
64
+ @end
65
+ <a href="/auth/interaction/{{ uid }}/switch" class="text-xs font-medium text-gray-900 hover:underline">
66
+ {{ t('login.switch_account') }}
67
+ </a>
68
+ </div>
69
+
70
+ @if(error)
71
+ <p class="mt-4 text-sm text-red-600">{{ error }}</p>
72
+ @end
73
+
74
+ <div class="mt-6">
75
+ <label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('login.password_label') }}</label>
76
+ <input id="password" name="password" type="password" required autofocus
77
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
78
+ </div>
79
+
80
+ <button type="submit"
81
+ class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
82
+ {{ t('login.submit') }}
83
+ </button>
84
+
85
+ <div class="mt-4 flex justify-end text-sm text-gray-600">
86
+ <a href="/auth/forgot-password" class="hover:underline">{{ t('login.forgot_password') }}</a>
87
+ </div>
88
+ </form>
89
+ </div>
90
+ @end
91
+ </body></html>