@adonis-agora/authkit-server 0.82.3 → 0.84.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.
- package/build/host/views/login.edge +25 -5
- package/build/host/views/partials/styles.edge +1 -1
- package/build/src/define_config.d.ts +44 -4
- package/build/src/define_config.js +1 -0
- package/build/src/host/account_roles.d.ts +14 -3
- package/build/src/host/account_roles.js +15 -4
- package/build/src/host/admin_api/api_users_controller.d.ts +3 -0
- package/build/src/host/admin_api/api_users_controller.js +9 -5
- package/build/src/host/admin_api/dto.d.ts +24 -0
- package/build/src/host/admin_api/dto.js +14 -0
- package/build/src/host/admin_api/token_verify_service.js +7 -2
- package/build/src/host/admin_console/admin_shell_controller.js +3 -1
- package/build/src/host/admin_console/console_users_controller.d.ts +4 -0
- package/build/src/host/admin_console/console_users_controller.js +12 -6
- package/build/src/host/controllers/interaction_controller.d.ts +8 -0
- package/build/src/host/controllers/interaction_controller.js +59 -12
- package/build/src/host/controllers/pat_introspection_controller.d.ts +1 -1
- package/build/src/host/controllers/pat_introspection_controller.js +2 -1
- package/build/src/host/controllers/registration_controller.d.ts +10 -0
- package/build/src/host/controllers/registration_controller.js +97 -30
- package/build/src/host/controllers/social_controller.js +64 -1
- package/build/src/host/i18n.d.ts +2 -0
- package/build/src/host/i18n.js +3 -0
- package/build/src/host/last_login_method.d.ts +67 -0
- package/build/src/host/last_login_method.js +119 -0
- package/build/src/host/register_auth_host.js +4 -0
- package/build/src/host/signup_token.d.ts +9 -0
- package/build/src/host/signup_token.js +31 -0
- package/build/src/host/social_pending.d.ts +17 -0
- package/build/src/host/social_pending.js +11 -0
- package/build/src/host/ui-dist/assets/{index-C1eSfG8B.js → index-DhQzXw2i.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/provider/interaction_actions.js +7 -0
- package/package.json +2 -2
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
@end
|
|
20
20
|
|
|
21
21
|
<div class="mt-6">
|
|
22
|
-
|
|
22
|
+
{{-- Selo "último usado": este dispositivo entrou por e-mail (senha, link, código ou passkey). --}}
|
|
23
|
+
<div class="mb-1 flex items-center justify-between">
|
|
24
|
+
<label for="email" class="block text-sm font-medium text-gray-700">{{ t('login.email_label') }}</label>
|
|
25
|
+
@if(['password', 'magic_link', 'otp', 'passkey'].includes(lastUsedLoginMethod))
|
|
26
|
+
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">{{ t('login.last_used') }}</span>
|
|
27
|
+
@end
|
|
28
|
+
</div>
|
|
23
29
|
<input id="email" name="email" type="email" required autofocus
|
|
24
30
|
autocomplete="{{ authMethods && authMethods.passkeyAutofill ? 'username webauthn' : 'username' }}"
|
|
25
31
|
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-hidden transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
|
|
@@ -49,7 +55,7 @@
|
|
|
49
55
|
|
|
50
56
|
@each(provider in (authMethods && authMethods.social ? authMethods.social : []))
|
|
51
57
|
<a href="/auth/{{ provider }}/redirect/{{ uid }}"
|
|
52
|
-
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 mb-2">
|
|
58
|
+
class="relative 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 mb-2">
|
|
53
59
|
@if(provider === 'google')
|
|
54
60
|
<svg class="h-4 w-4" viewBox="0 0 24 24" aria-hidden="true">
|
|
55
61
|
<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"/>
|
|
@@ -61,6 +67,9 @@
|
|
|
61
67
|
@else
|
|
62
68
|
{{ t('login.social_provider', { provider: provider }) }}
|
|
63
69
|
@end
|
|
70
|
+
@if(lastUsedLoginMethod === 'social:' + provider)
|
|
71
|
+
<span class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">{{ t('login.last_used') }}</span>
|
|
72
|
+
@end
|
|
64
73
|
</a>
|
|
65
74
|
@end
|
|
66
75
|
@end
|
|
@@ -106,7 +115,12 @@
|
|
|
106
115
|
@end
|
|
107
116
|
|
|
108
117
|
<div class="mt-6">
|
|
109
|
-
<
|
|
118
|
+
<div class="mb-1 flex items-center justify-between">
|
|
119
|
+
<label for="password" class="block text-sm font-medium text-gray-700">{{ t('login.password_label') }}</label>
|
|
120
|
+
@if(lastUsedLoginMethod === 'password')
|
|
121
|
+
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">{{ t('login.last_used') }}</span>
|
|
122
|
+
@end
|
|
123
|
+
</div>
|
|
110
124
|
<input id="password" name="password" type="password" required autofocus
|
|
111
125
|
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-hidden transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
|
|
112
126
|
</div>
|
|
@@ -189,8 +203,11 @@
|
|
|
189
203
|
<form method="POST" action="/auth/interaction/{{ uid }}/magic" class="mt-4">
|
|
190
204
|
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
191
205
|
<button type="submit"
|
|
192
|
-
class="w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
|
|
206
|
+
class="relative w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
|
|
193
207
|
{{ t('login.magic_link_button') }}
|
|
208
|
+
@if(lastUsedLoginMethod === 'magic_link' || lastUsedLoginMethod === 'otp')
|
|
209
|
+
<span class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">{{ t('login.last_used') }}</span>
|
|
210
|
+
@end
|
|
194
211
|
</button>
|
|
195
212
|
</form>
|
|
196
213
|
@end
|
|
@@ -204,8 +221,11 @@
|
|
|
204
221
|
<input type="hidden" name="response" id="passkey-response">
|
|
205
222
|
</form>
|
|
206
223
|
<button type="button" id="passkey-button"
|
|
207
|
-
class="mt-4 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
|
|
224
|
+
class="relative mt-4 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
|
|
208
225
|
{{ t('login.passkey_button') }}
|
|
226
|
+
@if(lastUsedLoginMethod === 'passkey')
|
|
227
|
+
<span class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600">{{ t('login.last_used') }}</span>
|
|
228
|
+
@end
|
|
209
229
|
</button>
|
|
210
230
|
<p id="passkey-error" class="mt-3 hidden text-sm text-red-600">{{ t('mfa_challenge.passkey_error') }}</p>
|
|
211
231
|
{{-- M12: script inline sem nonce é bloqueado por CSP `script-src 'self'` — extraído
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
{{-- Gerado por scripts/build_host_css.mjs — não editar à mão. --}}
|
|
2
2
|
<style>/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
3
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-green-50:oklch(98.2% .018 155.826);--color-green-700:oklch(52.7% .154 150.069);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--tracking-widest:.1em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),[role=button]:not(:disabled){cursor:pointer}}@layer components;@layer utilities{.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-48{height:calc(var(--spacing) * 48)}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-48{width:calc(var(--spacing) * 48)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.flex-1{flex:1}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-300{border-color:var(--color-blue-300)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.4em\]{--tw-tracking:.4em;letter-spacing:.4em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-800{color:var(--color-amber-800)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-red-600:focus{border-color:var(--color-red-600)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}</style>
|
|
3
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-800:oklch(47.3% .137 46.201);--color-green-50:oklch(98.2% .018 155.826);--color-green-700:oklch(52.7% .154 150.069);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-blue-50:oklch(97% .014 254.604);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--tracking-widest:.1em;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),[role=button]:not(:disabled){cursor:pointer}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.right-2{right:calc(var(--spacing) * 2)}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-6{margin-block:calc(var(--spacing) * 6)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-48{height:calc(var(--spacing) * 48)}.h-px{height:1px}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-48{width:calc(var(--spacing) * 48)}.w-fit{width:fit-content}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.flex-1{flex:1}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-blue-300{border-color:var(--color-blue-300)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-900{border-color:var(--color-gray-900)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-600{background-color:var(--color-red-600)}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.4em\]{--tw-tracking:.4em;letter-spacing:.4em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.text-amber-600{color:var(--color-amber-600)}.text-amber-800{color:var(--color-amber-800)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-green-700{color:var(--color-green-700)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xs{--tw-shadow:0 1px 2px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-amber-200{--tw-ring-color:var(--color-amber-200)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-emerald-200{--tw-ring-color:var(--color-emerald-200)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-red-200{--tw-ring-color:var(--color-red-200)}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-none{-webkit-user-select:none;user-select:none}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media (hover:hover){.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-90:hover{opacity:.9}}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-red-600:focus{border-color:var(--color-red-600)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}</style>
|
|
@@ -23,8 +23,31 @@ import { type RedirectUriPolicy, type ResolvedRedirectUriPolicy, type ValidateRe
|
|
|
23
23
|
export type { AuthAccount };
|
|
24
24
|
export { adapters };
|
|
25
25
|
export type AuthHostRenderer = (ctx: HttpContext, view: string, props: Record<string, unknown>) => unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Quando o login social pede o 2º fator da conta:
|
|
28
|
+
* - `'enrolled'` (default): pede se a conta tem um fator cadastrado (TOTP,
|
|
29
|
+
* passkey ou método custom de `mfa`). Quem ativou o fator espera que ele
|
|
30
|
+
* valha em qualquer porta de entrada, inclusive "Continuar com Google".
|
|
31
|
+
* - `'required'`: exige sempre; conta sem fator vê a tela de "configure o MFA"
|
|
32
|
+
* (não há o que desafiar). Cuidado: tranca quem ainda não cadastrou fator.
|
|
33
|
+
* - `'never'`: confia só no provider (o comportamento antigo).
|
|
34
|
+
*/
|
|
35
|
+
export type SocialSecondFactorPolicy = 'enrolled' | 'required' | 'never';
|
|
26
36
|
export interface AuthSocialConfig {
|
|
27
37
|
providers: string[];
|
|
38
|
+
/**
|
|
39
|
+
* 2º fator no login social. Aceita uma política fixa ou uma função por conta
|
|
40
|
+
* (ex.: `'required'` só para papéis administrativos). Default: `'enrolled'`.
|
|
41
|
+
*
|
|
42
|
+
* `roles` vem da MESMA fonte do claim de papéis: `resolveTokenRoles` quando o
|
|
43
|
+
* host o configura (ex.: `@adonis-agora/authz`), senão `account.globalRoles`.
|
|
44
|
+
* Só é resolvido quando a política é uma função.
|
|
45
|
+
*/
|
|
46
|
+
secondFactor?: SocialSecondFactorPolicy | ((account: {
|
|
47
|
+
id: string;
|
|
48
|
+
email: string;
|
|
49
|
+
roles: string[];
|
|
50
|
+
}) => SocialSecondFactorPolicy | Promise<SocialSecondFactorPolicy>);
|
|
28
51
|
}
|
|
29
52
|
/**
|
|
30
53
|
* Hooks de e-mail plugáveis (best-effort). Quando ausentes, o host-kit cai no
|
|
@@ -62,6 +85,12 @@ export interface MailHooks {
|
|
|
62
85
|
* hosts existentes simplesmente ignoram este campo (back-compat).
|
|
63
86
|
*/
|
|
64
87
|
channel?: 'code' | 'link';
|
|
88
|
+
/**
|
|
89
|
+
* `'signup'`: link de confirmação do cadastro passwordless (a conta é criada
|
|
90
|
+
* ao abri-lo). Ausente = login por magic link. O host pode trocar o texto do
|
|
91
|
+
* e-mail ("confirme seu cadastro"); ignorar o campo continua funcionando.
|
|
92
|
+
*/
|
|
93
|
+
kind?: 'signup';
|
|
65
94
|
}) => Promise<void>;
|
|
66
95
|
/**
|
|
67
96
|
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Distinto de
|
|
@@ -488,10 +517,13 @@ export interface PasswordlessConfigInput {
|
|
|
488
517
|
passkeyFirst?: boolean;
|
|
489
518
|
/**
|
|
490
519
|
* Liga o cadastro público passwordless: o signup pede só e-mail + nome (sem
|
|
491
|
-
* senha)
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
520
|
+
* senha) e envia um link de confirmação (`onMagicLink` com `kind: 'signup'`).
|
|
521
|
+
* A conta só é criada quando o dono do e-mail abre o link: nasce com o e-mail
|
|
522
|
+
* verificado e uma senha aleatória inutilizável, e o login segue pelo magic
|
|
523
|
+
* link. E-mail que já tem conta recebe um magic link de login. Default: false.
|
|
524
|
+
* Exige que o accountStore implemente {@link MagicLinkCapability} e o serviço de
|
|
525
|
+
* encryption do app (APP_KEY), que assina o link; sem a capacidade o cadastro
|
|
526
|
+
* passwordless fica indisponível e o fluxo de senha segue valendo.
|
|
495
527
|
*/
|
|
496
528
|
signup?: boolean;
|
|
497
529
|
}
|
|
@@ -554,10 +586,18 @@ export interface LoginConfigInput {
|
|
|
554
586
|
* idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
|
|
555
587
|
*/
|
|
556
588
|
otp?: OtpLoginConfigInput;
|
|
589
|
+
/**
|
|
590
|
+
* Selo "último usado" na tela de login: o método com que este dispositivo
|
|
591
|
+
* entrou da última vez (senha, link, código, passkey, social) fica num cookie
|
|
592
|
+
* sem identidade e a tela o destaca. Por dispositivo, nunca por conta — não
|
|
593
|
+
* vaza como uma conta entra. Default: **ligado**. Ver `host/last_login_method.ts`.
|
|
594
|
+
*/
|
|
595
|
+
lastUsedMethod?: boolean;
|
|
557
596
|
}
|
|
558
597
|
export interface ResolvedLoginConfig {
|
|
559
598
|
requireVerifiedEmail: boolean;
|
|
560
599
|
otp: ResolvedOtpLoginConfig;
|
|
600
|
+
lastUsedMethod: boolean;
|
|
561
601
|
}
|
|
562
602
|
export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
|
|
563
603
|
/**
|
|
@@ -154,6 +154,7 @@ export function resolveLogin(input) {
|
|
|
154
154
|
return {
|
|
155
155
|
requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
|
|
156
156
|
otp: resolveOtpLoginConfig(input?.otp),
|
|
157
|
+
lastUsedMethod: input?.lastUsedMethod ?? true,
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
export function resolveRegistration(input) {
|
|
@@ -19,8 +19,19 @@ type RoleResolverConfig = {
|
|
|
19
19
|
* account's stored `globalRoles`. Falls back to `account.globalRoles ?? []` when no hook is set, so
|
|
20
20
|
* default behavior is unchanged.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
* empty context (
|
|
22
|
+
* Gates that are not tied to an OIDC client (console, maintenance mode, PAT introspection) call the
|
|
23
|
+
* hook with an empty context; callers that know the client (opaque access-token introspection) pass
|
|
24
|
+
* its `clientId`, matching what the token was minted with. `activeOrg` is always `null` here.
|
|
24
25
|
*/
|
|
25
|
-
export declare function resolveAccountRoles(cfg: RoleResolverConfig, account: AuthAccount
|
|
26
|
+
export declare function resolveAccountRoles(cfg: RoleResolverConfig, account: AuthAccount, context?: {
|
|
27
|
+
clientId?: string;
|
|
28
|
+
}): Promise<string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* true quando os papéis da conta vêm de fora do authkit: o host configurou
|
|
31
|
+
* `resolveTokenRoles` (ex.: `@adonis-agora/authz` ou uma tabela própria). Nesse
|
|
32
|
+
* caso `globalRoles` não decide nada, então o console e a Admin API mostram os
|
|
33
|
+
* papéis do hook e recusam editá-los — gravar na coluna diria "salvo" sem mudar
|
|
34
|
+
* acesso nenhum. Quem edita é o próprio app, na fonte dele.
|
|
35
|
+
*/
|
|
36
|
+
export declare function rolesManagedExternally(cfg: RoleResolverConfig): boolean;
|
|
26
37
|
export {};
|
|
@@ -7,12 +7,23 @@
|
|
|
7
7
|
* account's stored `globalRoles`. Falls back to `account.globalRoles ?? []` when no hook is set, so
|
|
8
8
|
* default behavior is unchanged.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* empty context (
|
|
10
|
+
* Gates that are not tied to an OIDC client (console, maintenance mode, PAT introspection) call the
|
|
11
|
+
* hook with an empty context; callers that know the client (opaque access-token introspection) pass
|
|
12
|
+
* its `clientId`, matching what the token was minted with. `activeOrg` is always `null` here.
|
|
12
13
|
*/
|
|
13
|
-
export async function resolveAccountRoles(cfg, account) {
|
|
14
|
+
export async function resolveAccountRoles(cfg, account, context = {}) {
|
|
14
15
|
if (cfg.resolveTokenRoles) {
|
|
15
|
-
return cfg.resolveTokenRoles(account, { clientId:
|
|
16
|
+
return cfg.resolveTokenRoles(account, { clientId: context.clientId, activeOrg: null });
|
|
16
17
|
}
|
|
17
18
|
return account.globalRoles ?? [];
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* true quando os papéis da conta vêm de fora do authkit: o host configurou
|
|
22
|
+
* `resolveTokenRoles` (ex.: `@adonis-agora/authz` ou uma tabela própria). Nesse
|
|
23
|
+
* caso `globalRoles` não decide nada, então o console e a Admin API mostram os
|
|
24
|
+
* papéis do hook e recusam editá-los — gravar na coluna diria "salvo" sem mudar
|
|
25
|
+
* acesso nenhum. Quem edita é o próprio app, na fonte dele.
|
|
26
|
+
*/
|
|
27
|
+
export function rolesManagedExternally(cfg) {
|
|
28
|
+
return typeof cfg.resolveTokenRoles === 'function';
|
|
29
|
+
}
|
|
@@ -23,6 +23,7 @@ export default class ApiUsersController {
|
|
|
23
23
|
avatarUrl: string | null;
|
|
24
24
|
globalRoles: string[];
|
|
25
25
|
disabled: boolean;
|
|
26
|
+
roles: string[];
|
|
26
27
|
}>;
|
|
27
28
|
/** POST /users — { email, name?, password? | invite?:true }. */
|
|
28
29
|
store(ctx: HttpContext): Promise<void | {
|
|
@@ -32,6 +33,7 @@ export default class ApiUsersController {
|
|
|
32
33
|
avatarUrl: string | null;
|
|
33
34
|
globalRoles: string[];
|
|
34
35
|
disabled: boolean;
|
|
36
|
+
roles: string[];
|
|
35
37
|
invited: boolean;
|
|
36
38
|
}>;
|
|
37
39
|
/** PATCH /users/:id — { globalRoles?, name?, avatarUrl? }. */
|
|
@@ -42,6 +44,7 @@ export default class ApiUsersController {
|
|
|
42
44
|
avatarUrl: string | null;
|
|
43
45
|
globalRoles: string[];
|
|
44
46
|
disabled: boolean;
|
|
47
|
+
roles: string[];
|
|
45
48
|
}>;
|
|
46
49
|
/** DELETE /users/:id — deleção completa (cascade) da conta. */
|
|
47
50
|
destroy(ctx: HttpContext): Promise<void | {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
|
|
3
|
+
import { rolesManagedExternally } from '../account_roles.js';
|
|
3
4
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
5
|
import { adminUserCreateValidator, adminUserUpdateValidator } from '../admin_validators.js';
|
|
5
6
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
6
7
|
import { enrichSessionsWithContext } from '../session_context.js';
|
|
7
8
|
import { AdminUsersService } from './admin_users_service.js';
|
|
8
|
-
import { apiError, grantDto, sessionDto,
|
|
9
|
+
import { apiError, grantDto, rolesManagedExternallyError, sessionDto, userDtoWithRoles, } from './dto.js';
|
|
9
10
|
/**
|
|
10
11
|
* Lê a config + monta o actor `admin-api` para auditoria. O `actorId` recebe o
|
|
11
12
|
* id NÃO-SENSÍVEL da API key que autenticou (anexado pelo `adminApiGuard` em
|
|
@@ -35,7 +36,7 @@ export default class ApiUsersController {
|
|
|
35
36
|
const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
|
|
36
37
|
const result = await cfg.accountStore.listAccounts({ search, page, size });
|
|
37
38
|
const users = new AdminUsersService(cfg);
|
|
38
|
-
const data = await Promise.all(result.data.map(async (u) =>
|
|
39
|
+
const data = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
|
|
39
40
|
return { meta: { page, size, total: result.total }, data };
|
|
40
41
|
}
|
|
41
42
|
/** GET /users/:id */
|
|
@@ -46,7 +47,7 @@ export default class ApiUsersController {
|
|
|
46
47
|
if (!account)
|
|
47
48
|
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
48
49
|
const disabled = await new AdminUsersService(cfg).isDisabled(id);
|
|
49
|
-
return
|
|
50
|
+
return userDtoWithRoles(cfg, account, disabled);
|
|
50
51
|
}
|
|
51
52
|
/** POST /users — { email, name?, password? | invite?:true }. */
|
|
52
53
|
async store(ctx) {
|
|
@@ -66,7 +67,7 @@ export default class ApiUsersController {
|
|
|
66
67
|
return ctx.response.conflict(apiError('email_taken', 'Já existe uma conta com este e-mail.'));
|
|
67
68
|
}
|
|
68
69
|
ctx.response.status(201);
|
|
69
|
-
return { ...
|
|
70
|
+
return { ...(await userDtoWithRoles(cfg, result.account)), invited: result.invited };
|
|
70
71
|
}
|
|
71
72
|
/** PATCH /users/:id — { globalRoles?, name?, avatarUrl? }. */
|
|
72
73
|
async update(ctx) {
|
|
@@ -77,6 +78,9 @@ export default class ApiUsersController {
|
|
|
77
78
|
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
78
79
|
const users = new AdminUsersService(cfg);
|
|
79
80
|
const { globalRoles: roles, name, avatarUrl, } = await ctx.request.validateUsing(adminUserUpdateValidator);
|
|
81
|
+
if (Array.isArray(roles) && rolesManagedExternally(cfg)) {
|
|
82
|
+
return ctx.response.conflict(rolesManagedExternallyError());
|
|
83
|
+
}
|
|
80
84
|
if (Array.isArray(roles)) {
|
|
81
85
|
const normalized = Array.from(new Set(roles.map((r) => r.trim()).filter(Boolean)));
|
|
82
86
|
// Proteções de escalonamento/lockout: último admin + auto-rebaixamento.
|
|
@@ -107,7 +111,7 @@ export default class ApiUsersController {
|
|
|
107
111
|
await users.updateProfile(id, patch);
|
|
108
112
|
}
|
|
109
113
|
const updated = await cfg.accountStore.findById(id);
|
|
110
|
-
return
|
|
114
|
+
return userDtoWithRoles(cfg, updated, await users.isDisabled(id));
|
|
111
115
|
}
|
|
112
116
|
/** DELETE /users/:id — deleção completa (cascade) da conta. */
|
|
113
117
|
async destroy(ctx) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuthAccount, OrgInvitation, OrgMember, OrgSummary } from '../../accounts/account_store.js';
|
|
2
2
|
import type { StoredAuditEvent } from '../../audit/audit_sink.js';
|
|
3
|
+
import { resolveAccountRoles } from '../account_roles.js';
|
|
3
4
|
import type { AdminClient, CreatedClient } from '../admin_clients_service.js';
|
|
4
5
|
import type { AdminGrant, AdminSession } from '../admin_sessions_service.js';
|
|
5
6
|
import type { OrgDetail } from './admin_orgs_service.js';
|
|
@@ -12,6 +13,21 @@ export declare function userDto(account: AuthAccount, disabled?: boolean): {
|
|
|
12
13
|
globalRoles: string[];
|
|
13
14
|
disabled: boolean;
|
|
14
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* {@link userDto} + `roles`: os papéis efetivos da conta, pela mesma fonte do
|
|
18
|
+
* claim (`resolveAccountRoles`). Sem `resolveTokenRoles` é igual a
|
|
19
|
+
* `globalRoles`; com ele, é o que o app diz (ex.: authz) e `globalRoles` segue
|
|
20
|
+
* sendo só a coluna guardada.
|
|
21
|
+
*/
|
|
22
|
+
export declare function userDtoWithRoles(cfg: Parameters<typeof resolveAccountRoles>[0], account: AuthAccount, disabled?: boolean): Promise<{
|
|
23
|
+
id: string;
|
|
24
|
+
email: string;
|
|
25
|
+
name: string | null;
|
|
26
|
+
avatarUrl: string | null;
|
|
27
|
+
globalRoles: string[];
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
roles: string[];
|
|
30
|
+
}>;
|
|
15
31
|
export declare function clientDto(client: AdminClient): {
|
|
16
32
|
clientId: string;
|
|
17
33
|
confidential: boolean;
|
|
@@ -129,3 +145,11 @@ export declare function settingDto(row: SettingRow): {
|
|
|
129
145
|
locked: boolean;
|
|
130
146
|
lockedBy?: "config" | undefined;
|
|
131
147
|
};
|
|
148
|
+
/** 409 de quem tenta editar papéis geridos fora do authkit (`resolveTokenRoles`). */
|
|
149
|
+
export declare function rolesManagedExternallyError(): {
|
|
150
|
+
error: {
|
|
151
|
+
code: string;
|
|
152
|
+
message: string;
|
|
153
|
+
details?: Record<string, unknown> | undefined;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAccountRoles } from '../account_roles.js';
|
|
1
2
|
/** Projeta uma conta para a forma JSON (camelCase) da Admin REST API. */
|
|
2
3
|
export function userDto(account, disabled = false) {
|
|
3
4
|
return {
|
|
@@ -9,6 +10,15 @@ export function userDto(account, disabled = false) {
|
|
|
9
10
|
disabled,
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* {@link userDto} + `roles`: os papéis efetivos da conta, pela mesma fonte do
|
|
15
|
+
* claim (`resolveAccountRoles`). Sem `resolveTokenRoles` é igual a
|
|
16
|
+
* `globalRoles`; com ele, é o que o app diz (ex.: authz) e `globalRoles` segue
|
|
17
|
+
* sendo só a coluna guardada.
|
|
18
|
+
*/
|
|
19
|
+
export async function userDtoWithRoles(cfg, account, disabled = false) {
|
|
20
|
+
return { ...userDto(account, disabled), roles: await resolveAccountRoles(cfg, account) };
|
|
21
|
+
}
|
|
12
22
|
export function clientDto(client) {
|
|
13
23
|
return {
|
|
14
24
|
clientId: client.clientId,
|
|
@@ -121,3 +131,7 @@ export function settingDto(row) {
|
|
|
121
131
|
...(locked ? { lockedBy: 'config' } : {}),
|
|
122
132
|
};
|
|
123
133
|
}
|
|
134
|
+
/** 409 de quem tenta editar papéis geridos fora do authkit (`resolveTokenRoles`). */
|
|
135
|
+
export function rolesManagedExternallyError() {
|
|
136
|
+
return apiError('roles_managed_externally', 'Os papéis desta instalação vêm do app (resolveTokenRoles) e só podem ser alterados lá.');
|
|
137
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveAccountRoles } from '../account_roles.js';
|
|
1
2
|
/**
|
|
2
3
|
* Introspecção genérica de token usada pela Admin REST API (`POST /tokens/verify`).
|
|
3
4
|
* Roteia por prefixo: tokens `pat_...` vão pelo {@link PatStore} (mesma rota do
|
|
@@ -36,7 +37,7 @@ export class TokenVerifyService {
|
|
|
36
37
|
sub: account.id,
|
|
37
38
|
email: account.email,
|
|
38
39
|
name: account.name ?? null,
|
|
39
|
-
roles:
|
|
40
|
+
roles: await resolveAccountRoles(this.cfg, account),
|
|
40
41
|
scopes: meta.scopes,
|
|
41
42
|
audience: meta.audience,
|
|
42
43
|
exp: meta.exp,
|
|
@@ -64,7 +65,11 @@ export class TokenVerifyService {
|
|
|
64
65
|
sub,
|
|
65
66
|
email: account?.email ?? null,
|
|
66
67
|
name: account?.name ?? null,
|
|
67
|
-
roles: account
|
|
68
|
+
roles: account
|
|
69
|
+
? await resolveAccountRoles(this.cfg, account, {
|
|
70
|
+
clientId: at.clientId ?? undefined,
|
|
71
|
+
})
|
|
72
|
+
: [],
|
|
68
73
|
scopes,
|
|
69
74
|
audience: at.aud ?? null,
|
|
70
75
|
clientId: at.clientId ?? null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { extname } from 'node:path';
|
|
3
3
|
import { accountPath } from '../account_paths.js';
|
|
4
|
-
import { resolveAccountRoles } from '../account_roles.js';
|
|
4
|
+
import { resolveAccountRoles, rolesManagedExternally } from '../account_roles.js';
|
|
5
5
|
import { getAdminPrefix } from '../admin_prefix.js';
|
|
6
6
|
// ─── Shell HTML cache ─────────────────────────────────────────────────────────
|
|
7
7
|
/**
|
|
@@ -141,6 +141,8 @@ export default class AdminShellController {
|
|
|
141
141
|
features: {
|
|
142
142
|
// A página de personal agents só aparece com `personalAgents` no config.
|
|
143
143
|
personalAgents: !!cfg.personalAgents,
|
|
144
|
+
// Papéis vindos de fora (resolveTokenRoles): a tela só mostra, não edita.
|
|
145
|
+
rolesManagedExternally: rolesManagedExternally(cfg),
|
|
144
146
|
},
|
|
145
147
|
};
|
|
146
148
|
const { html: rawHtml } = await loadShell();
|
|
@@ -35,6 +35,7 @@ export default class ConsoleUsersController {
|
|
|
35
35
|
avatarUrl: string | null;
|
|
36
36
|
globalRoles: string[];
|
|
37
37
|
disabled: boolean;
|
|
38
|
+
roles: string[];
|
|
38
39
|
sessionsSupported: boolean;
|
|
39
40
|
sessions: {
|
|
40
41
|
id: string;
|
|
@@ -70,6 +71,7 @@ export default class ConsoleUsersController {
|
|
|
70
71
|
avatarUrl: string | null;
|
|
71
72
|
globalRoles: string[];
|
|
72
73
|
disabled: boolean;
|
|
74
|
+
roles: string[];
|
|
73
75
|
invited: boolean;
|
|
74
76
|
}>;
|
|
75
77
|
/** PATCH {prefix}/api/users/:id/roles */
|
|
@@ -80,6 +82,7 @@ export default class ConsoleUsersController {
|
|
|
80
82
|
avatarUrl: string | null;
|
|
81
83
|
globalRoles: string[];
|
|
82
84
|
disabled: boolean;
|
|
85
|
+
roles: string[];
|
|
83
86
|
}>;
|
|
84
87
|
/** POST {prefix}/api/users/:id/disable */
|
|
85
88
|
disable(ctx: HttpContext): Promise<void | {}>;
|
|
@@ -91,6 +94,7 @@ export default class ConsoleUsersController {
|
|
|
91
94
|
avatarUrl: string | null;
|
|
92
95
|
globalRoles: string[];
|
|
93
96
|
disabled: boolean;
|
|
97
|
+
roles: string[];
|
|
94
98
|
}>;
|
|
95
99
|
/** POST {prefix}/api/users/:id/reset-password */
|
|
96
100
|
resetPassword(ctx: HttpContext): Promise<void | {}>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { supportsAccountDeletion, supportsAccountStatus } from '../../accounts/account_store.js';
|
|
3
3
|
import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
|
|
4
|
+
import { rolesManagedExternally } from '../account_roles.js';
|
|
4
5
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
5
6
|
import { AdminUsersService } from '../admin_api/admin_users_service.js';
|
|
6
|
-
import { apiError, grantDto, sessionDto,
|
|
7
|
+
import { apiError, grantDto, rolesManagedExternallyError, sessionDto, userDtoWithRoles, } from '../admin_api/dto.js';
|
|
7
8
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
8
9
|
import { adminUserCreateValidator, adminUserRolesValidator } from '../admin_validators.js';
|
|
9
10
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
@@ -61,7 +62,7 @@ export default class ConsoleUsersController {
|
|
|
61
62
|
size,
|
|
62
63
|
});
|
|
63
64
|
const users = new AdminUsersService(cfg);
|
|
64
|
-
const data = await Promise.all(result.data.map(async (u) =>
|
|
65
|
+
const data = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
|
|
65
66
|
return { meta: { page, size, total: result.total }, data };
|
|
66
67
|
}
|
|
67
68
|
/** GET {prefix}/api/users/:id */
|
|
@@ -92,7 +93,7 @@ export default class ConsoleUsersController {
|
|
|
92
93
|
// fail-safe
|
|
93
94
|
}
|
|
94
95
|
return {
|
|
95
|
-
...
|
|
96
|
+
...(await userDtoWithRoles(cfg, account, disabled)),
|
|
96
97
|
sessionsSupported: sessions.canList,
|
|
97
98
|
sessions: enriched.map(sessionDto),
|
|
98
99
|
grants: grants.map(grantDto),
|
|
@@ -122,7 +123,7 @@ export default class ConsoleUsersController {
|
|
|
122
123
|
return ctx.response.conflict(apiError('email_taken', 'Já existe uma conta com este e-mail.'));
|
|
123
124
|
}
|
|
124
125
|
ctx.response.status(201);
|
|
125
|
-
return { ...
|
|
126
|
+
return { ...(await userDtoWithRoles(cfg, result.account)), invited: result.invited };
|
|
126
127
|
}
|
|
127
128
|
/** PATCH {prefix}/api/users/:id/roles */
|
|
128
129
|
async updateRoles(ctx) {
|
|
@@ -132,6 +133,11 @@ export default class ConsoleUsersController {
|
|
|
132
133
|
const account = await cfg.accountStore.findById(id);
|
|
133
134
|
if (!account)
|
|
134
135
|
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
136
|
+
// Papéis de fora do authkit (resolveTokenRoles): a coluna não decide acesso,
|
|
137
|
+
// então gravar nela seria um "salvo" mentiroso. O app edita na fonte dele.
|
|
138
|
+
if (rolesManagedExternally(cfg)) {
|
|
139
|
+
return ctx.response.status(409).send(rolesManagedExternallyError());
|
|
140
|
+
}
|
|
135
141
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
136
142
|
const { roles: rolesInput } = await ctx.request.validateUsing(adminUserRolesValidator);
|
|
137
143
|
const roles = Array.from(new Set((rolesInput ?? []).map((r) => r.trim()).filter(Boolean)));
|
|
@@ -156,7 +162,7 @@ export default class ConsoleUsersController {
|
|
|
156
162
|
}
|
|
157
163
|
const updated = await cfg.accountStore.findById(id);
|
|
158
164
|
const disabled = await users.isDisabled(id);
|
|
159
|
-
return
|
|
165
|
+
return userDtoWithRoles(cfg, updated, disabled);
|
|
160
166
|
}
|
|
161
167
|
/** POST {prefix}/api/users/:id/disable */
|
|
162
168
|
async disable(ctx) {
|
|
@@ -193,7 +199,7 @@ export default class ConsoleUsersController {
|
|
|
193
199
|
}
|
|
194
200
|
const updated = await cfg.accountStore.findById(id);
|
|
195
201
|
const disabled = await users.isDisabled(id);
|
|
196
|
-
return
|
|
202
|
+
return userDtoWithRoles(cfg, updated, disabled);
|
|
197
203
|
}
|
|
198
204
|
/** POST {prefix}/api/users/:id/reset-password */
|
|
199
205
|
async resetPassword(ctx) {
|
|
@@ -136,6 +136,14 @@ export default class AuthInteractionController {
|
|
|
136
136
|
* Consome o magic link. Em sucesso finaliza o login (amr `['email']`). Token
|
|
137
137
|
* inválido/expirado volta ao início do login.
|
|
138
138
|
*/
|
|
139
|
+
/**
|
|
140
|
+
* GET /auth/interaction/:uid/social/continue — segunda metade do login social
|
|
141
|
+
* quando a conta deve o 2º fator (ver `AuthSocialController#callback`). Lê o
|
|
142
|
+
* login pendente da sessão UMA vez, confere que é desta interaction e passa
|
|
143
|
+
* pelo mesmo `secondFactorGate` dos outros fluxos, com `fed` como fator
|
|
144
|
+
* primário no `amr`.
|
|
145
|
+
*/
|
|
146
|
+
socialContinue(ctx: HttpContext): Promise<any>;
|
|
139
147
|
magicLinkConsume(ctx: HttpContext): Promise<any>;
|
|
140
148
|
/**
|
|
141
149
|
* POST /auth/interaction/:uid/otp-verify
|