@adonis-agora/authkit-server 0.83.0 → 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 +8 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/controllers/interaction_controller.js +13 -0
- package/build/src/host/controllers/registration_controller.js +3 -0
- package/build/src/host/controllers/social_controller.js +2 -0
- 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/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>
|
|
@@ -586,10 +586,18 @@ export interface LoginConfigInput {
|
|
|
586
586
|
* idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
|
|
587
587
|
*/
|
|
588
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;
|
|
589
596
|
}
|
|
590
597
|
export interface ResolvedLoginConfig {
|
|
591
598
|
requireVerifiedEmail: boolean;
|
|
592
599
|
otp: ResolvedOtpLoginConfig;
|
|
600
|
+
lastUsedMethod: boolean;
|
|
593
601
|
}
|
|
594
602
|
export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
|
|
595
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) {
|
|
@@ -10,6 +10,7 @@ import { beginCustomMfa, clearMfaFlow, customMfaViewProps, getMfaFlow, initializ
|
|
|
10
10
|
import { sendMagicLinkEmail, sendOtpUnlockEmail } from '../default_mailer.js';
|
|
11
11
|
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
12
12
|
import { translate } from '../i18n.js';
|
|
13
|
+
import { readLastLoginMethod, rememberLastLoginMethod } from '../last_login_method.js';
|
|
13
14
|
import { assertLoginAllowed, attemptPasswordLogin, isEmailUnverifiedBlock, } from '../login_attempt.js';
|
|
14
15
|
import { magicChannelProp, normalizeLoginChannel } from '../login_channel.js';
|
|
15
16
|
import { notifyLoginSuccess } from '../login_notify.js';
|
|
@@ -125,6 +126,7 @@ export default class AuthInteractionController {
|
|
|
125
126
|
message: maintenance.message ?? translate(cfg.messages, 'maintenance.default_message'),
|
|
126
127
|
});
|
|
127
128
|
}
|
|
129
|
+
rememberLastLoginMethod(ctx, cfg, `custom:${input.method}`);
|
|
128
130
|
const gate = await this.secondFactorGate(ctx, cfg, account.id, details, input.method, input.factorId, input.remember, input.passwordless === true);
|
|
129
131
|
if (gate.kind === 'challenge') {
|
|
130
132
|
ctx.session.put(MFA_CUSTOM_PRIMARY_KEY, {
|
|
@@ -175,6 +177,8 @@ export default class AuthInteractionController {
|
|
|
175
177
|
authMethods,
|
|
176
178
|
magicLinkAvailable: authMethods.magicLink && magicLinkCapableConfig,
|
|
177
179
|
otpEnabled,
|
|
180
|
+
// Selo "último usado": método com que ESTE dispositivo entrou da última vez.
|
|
181
|
+
lastUsedLoginMethod: readLastLoginMethod(ctx, cfg),
|
|
178
182
|
};
|
|
179
183
|
}
|
|
180
184
|
/**
|
|
@@ -495,6 +499,7 @@ export default class AuthInteractionController {
|
|
|
495
499
|
// Gate do 2º fator — a MESMA regra dos três caminhos de login (ver
|
|
496
500
|
// `secondFactorGate`). Quando desafia, não finaliza a interaction: guarda o
|
|
497
501
|
// accountId pendente e devolve a tela.
|
|
502
|
+
rememberLastLoginMethod(ctx, cfg, 'password');
|
|
498
503
|
const gate = await this.secondFactorGate(ctx, cfg, acc.id, details, 'pwd');
|
|
499
504
|
if (gate.kind === 'challenge')
|
|
500
505
|
return gate.response;
|
|
@@ -1349,6 +1354,7 @@ export default class AuthInteractionController {
|
|
|
1349
1354
|
});
|
|
1350
1355
|
}
|
|
1351
1356
|
// Conta com segundo fator não termina o login só com o e-mail.
|
|
1357
|
+
rememberLastLoginMethod(ctx, cfg, 'magic_link');
|
|
1352
1358
|
const magicGate = await this.secondFactorGate(ctx, cfg, acc.id, await service.interactions.details(ctx), 'email');
|
|
1353
1359
|
if (magicGate.kind === 'challenge')
|
|
1354
1360
|
return magicGate.response;
|
|
@@ -1471,6 +1477,7 @@ export default class AuthInteractionController {
|
|
|
1471
1477
|
clientId,
|
|
1472
1478
|
});
|
|
1473
1479
|
// Conta com segundo fator não termina o login só com o código do e-mail.
|
|
1480
|
+
rememberLastLoginMethod(ctx, cfg, 'otp');
|
|
1474
1481
|
const otpGate = await this.secondFactorGate(ctx, cfg, result.account.id, await service.interactions.details(ctx), 'email');
|
|
1475
1482
|
if (otpGate.kind === 'challenge')
|
|
1476
1483
|
return otpGate.response;
|
|
@@ -1738,6 +1745,11 @@ export default class AuthInteractionController {
|
|
|
1738
1745
|
return ctx.response.redirect(`/auth/interaction/${ctx.request.param('uid')}`);
|
|
1739
1746
|
return this.finishVerifiedMfa(ctx, cfg, details, 'webauthn', 'webauthn');
|
|
1740
1747
|
}
|
|
1748
|
+
// Passkey como fator PRIMÁRIO (passkey-first/autofill). Como 2º fator ela não
|
|
1749
|
+
// é o método com que a pessoa se identificou — o selo fica com o primário.
|
|
1750
|
+
if (ctx.session.get(MFA_PENDING_KEY) !== accountId) {
|
|
1751
|
+
rememberLastLoginMethod(ctx, cfg, 'passkey');
|
|
1752
|
+
}
|
|
1741
1753
|
if (cfg.mfa) {
|
|
1742
1754
|
const gate = await this.secondFactorGate(ctx, cfg, accountId, details, 'webauthn');
|
|
1743
1755
|
if (gate.kind === 'challenge')
|
|
@@ -1845,6 +1857,7 @@ export default class AuthInteractionController {
|
|
|
1845
1857
|
// Senha trocada: limpa o step e finaliza o login.
|
|
1846
1858
|
ctx.session.forget(PASSWORD_EXPIRED_KEY);
|
|
1847
1859
|
// Verifica se precisa de MFA mesmo após a troca — mesma regra do resto.
|
|
1860
|
+
rememberLastLoginMethod(ctx, cfg, 'password');
|
|
1848
1861
|
const expiredGate = await this.secondFactorGate(ctx, cfg, accountId, details, 'pwd');
|
|
1849
1862
|
if (expiredGate.kind === 'challenge')
|
|
1850
1863
|
return expiredGate.response;
|
|
@@ -7,6 +7,7 @@ import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protec
|
|
|
7
7
|
import { brandFor } from '../branding.js';
|
|
8
8
|
import { sendEmailVerificationEmail, sendMagicLinkEmail, sendPasswordResetEmail, } from '../default_mailer.js';
|
|
9
9
|
import { translate } from '../i18n.js';
|
|
10
|
+
import { rememberLastLoginMethod } from '../last_login_method.js';
|
|
10
11
|
import { authkitOrigin } from '../origin.js';
|
|
11
12
|
import { RuntimeSettings, resolveRuntimeSettings } from '../runtime_settings.js';
|
|
12
13
|
import { resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, } from '../runtime_toggles.js';
|
|
@@ -165,6 +166,8 @@ export default class AuthRegistrationController {
|
|
|
165
166
|
ip: ctx.request.ip?.() ?? null,
|
|
166
167
|
clientId: details.params.client_id ?? null,
|
|
167
168
|
});
|
|
169
|
+
// Conta nova entra por senha: o selo "último usado" já nasce no método certo.
|
|
170
|
+
rememberLastLoginMethod(ctx, cfg, 'password');
|
|
168
171
|
// Finaliza a interaction como login (interactionFinished escreve o redirect 303 que
|
|
169
172
|
// retoma o authorize; o form nativo da tela segue esse redirect no sucesso).
|
|
170
173
|
const result = await service.interactions.login(ctx, {
|
|
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { supportsLoginMethodsPreference, supportsMagicLink, supportsPasskeys, supportsProviderIdentity, } from '../../accounts/account_store.js';
|
|
4
4
|
import { resolveAccountRoles } from '../account_roles.js';
|
|
5
5
|
import { normalizeEmailIdentifier } from '../email_identifier.js';
|
|
6
|
+
import { rememberLastLoginMethod } from '../last_login_method.js';
|
|
6
7
|
import { assertLoginAllowed } from '../login_attempt.js';
|
|
7
8
|
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
8
9
|
import { resolveEffectiveAuthMethods } from '../runtime_toggles.js';
|
|
@@ -196,6 +197,7 @@ export default class AuthSocialController {
|
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
ctx.session.forget(UID_SESSION_KEY);
|
|
200
|
+
rememberLastLoginMethod(ctx, cfg, `social:${provider}`);
|
|
199
201
|
// 2º fator: o provider provou a identidade, mas quem cadastrou TOTP/passkey
|
|
200
202
|
// aqui espera que o fator valha também neste caminho. O desafio mora no
|
|
201
203
|
// interaction controller (mesmo gate de senha/magic link/OTP), então este
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
50
50
|
'login.magic_link_sent': string;
|
|
51
51
|
'signup.magic_link_sent': string;
|
|
52
52
|
'login.passkey_button': string;
|
|
53
|
+
'login.last_used': string;
|
|
53
54
|
'login.otp_label': string;
|
|
54
55
|
'login.otp_placeholder': string;
|
|
55
56
|
'login.otp_submit': string;
|
|
@@ -809,6 +810,7 @@ export declare const PT_BR_MESSAGES: {
|
|
|
809
810
|
'login.magic_link_sent': string;
|
|
810
811
|
'signup.magic_link_sent': string;
|
|
811
812
|
'login.passkey_button': string;
|
|
813
|
+
'login.last_used': string;
|
|
812
814
|
'login.otp_label': string;
|
|
813
815
|
'login.otp_placeholder': string;
|
|
814
816
|
'login.otp_submit': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -41,6 +41,8 @@ export const DEFAULT_MESSAGES = {
|
|
|
41
41
|
'login.magic_link_sent': 'If the account exists, we sent you a login link.',
|
|
42
42
|
'signup.magic_link_sent': 'Check your email — we sent you a link to finish creating your account.',
|
|
43
43
|
'login.passkey_button': 'Sign in with a passkey',
|
|
44
|
+
// Selo do método com que este dispositivo entrou da última vez.
|
|
45
|
+
'login.last_used': 'Last used',
|
|
44
46
|
// Login por OTP (código digitável).
|
|
45
47
|
'login.otp_label': 'Enter the login code from the email',
|
|
46
48
|
'login.otp_placeholder': '000000',
|
|
@@ -891,6 +893,7 @@ export const PT_BR_MESSAGES = {
|
|
|
891
893
|
'login.magic_link_sent': 'Se a conta existir, enviamos um link de login.',
|
|
892
894
|
'signup.magic_link_sent': 'Enviamos um link para o seu e-mail. Abra-o para concluir o cadastro.',
|
|
893
895
|
'login.passkey_button': 'Entrar com passkey',
|
|
896
|
+
'login.last_used': 'Último usado',
|
|
894
897
|
// Login por OTP (código digitável).
|
|
895
898
|
'login.otp_label': 'Digite o código de login do e-mail',
|
|
896
899
|
'login.otp_placeholder': '000000',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* "Último método usado" no login (o selo "Last used" do Cloudflare, Vercel, Clerk…).
|
|
4
|
+
*
|
|
5
|
+
* O método fica num cookie DESTE DISPOSITIVO, não na conta. É de propósito:
|
|
6
|
+
* - o passo 1 (identifier) ainda não sabe quem é o usuário, e é exatamente ali
|
|
7
|
+
* que o selo mais ajuda ("da última vez você entrou com Google");
|
|
8
|
+
* - guardar por conta e mostrar no passo 2 diria a quem digita um e-mail qualquer
|
|
9
|
+
* como aquela conta entra — vazamento de enumeração. O cookie só conta ao
|
|
10
|
+
* próprio browser o que ele mesmo fez.
|
|
11
|
+
*
|
|
12
|
+
* O cookie não carrega identidade nenhuma (nem e-mail, nem accountId), só o método.
|
|
13
|
+
* É puramente de SUPERFÍCIE: a tela destaca um botão, mas nada no fluxo muda — os
|
|
14
|
+
* métodos oferecidos continuam vindo de `authMethods`, e um valor adulterado no
|
|
15
|
+
* cookie só destaca outro botão (ou nenhum, se não passar na validação).
|
|
16
|
+
*
|
|
17
|
+
* Gravado quando o fator PRIMÁRIO é aceito. Numa conta com 2º fator, isso acontece
|
|
18
|
+
* antes do desafio de MFA: o selo responde "como você se identificou", que é o
|
|
19
|
+
* botão que a pessoa procura na próxima vez.
|
|
20
|
+
*/
|
|
21
|
+
/** Nome do cookie do último método de login. */
|
|
22
|
+
export declare const LAST_LOGIN_METHOD_COOKIE = "authkit_last_login";
|
|
23
|
+
/**
|
|
24
|
+
* Método de login primário. `social:<provider>` e `custom:<id>` carregam o id do
|
|
25
|
+
* provedor/método para a tela destacar o botão certo entre vários.
|
|
26
|
+
*/
|
|
27
|
+
export type LastLoginMethod = 'password' | 'magic_link' | 'otp' | 'passkey' | `social:${string}` | `custom:${string}`;
|
|
28
|
+
/**
|
|
29
|
+
* Valida um valor lido do cookie. Só os métodos conhecidos passam; qualquer outra
|
|
30
|
+
* coisa (ausente, lixo, valor adulterado) vira `undefined` — sem selo.
|
|
31
|
+
*/
|
|
32
|
+
export declare function normalizeLastLoginMethod(raw: unknown): LastLoginMethod | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Lê o último método deste dispositivo para a tela de login. Fail-safe: feature
|
|
35
|
+
* desligada, ctx sem cookies ou qualquer erro → `undefined` (sem selo).
|
|
36
|
+
*/
|
|
37
|
+
export declare function readLastLoginMethod(ctx: HttpContext, cfg: {
|
|
38
|
+
login?: {
|
|
39
|
+
lastUsedMethod?: boolean;
|
|
40
|
+
};
|
|
41
|
+
}): LastLoginMethod | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Registra o método primário que acabou de ser aceito. Grava via resposta do
|
|
44
|
+
* Adonis; quando o login termina no `interactionFinished` (que encerra a resposta
|
|
45
|
+
* crua), o `completeLogin` repassa o cookie pendente — ver
|
|
46
|
+
* {@link relayPendingCookies}. Best-effort: nunca derruba o login.
|
|
47
|
+
*/
|
|
48
|
+
export declare function rememberLastLoginMethod(ctx: HttpContext, cfg: {
|
|
49
|
+
login?: {
|
|
50
|
+
lastUsedMethod?: boolean;
|
|
51
|
+
};
|
|
52
|
+
}, method: LastLoginMethod): void;
|
|
53
|
+
/**
|
|
54
|
+
* Repassa os `Set-Cookie` que o handler gravou na resposta do Adonis para a
|
|
55
|
+
* resposta crua do Node.
|
|
56
|
+
*
|
|
57
|
+
* O Adonis só escreve os headers no `finish()`, depois do handler. Mas o
|
|
58
|
+
* `interactionFinished` do oidc-provider escreve o 303 e dá `res.end()` direto na
|
|
59
|
+
* resposta crua — quando o Adonis chega no `finish()`, a resposta já saiu e todo
|
|
60
|
+
* cookie gravado via `ctx.response.cookie()` no mesmo request é descartado em
|
|
61
|
+
* silêncio. Chamado pelo `completeLogin` logo antes do `interactionFinished`.
|
|
62
|
+
*
|
|
63
|
+
* Os cookies saem do Adonis ao serem repassados, então não há como irem em dobro.
|
|
64
|
+
* O `Set-Cookie` que o próprio oidc-provider grava depois é ACRESCENTADO aos
|
|
65
|
+
* existentes (a lib `cookies` do Koa preserva o header), então nada se perde.
|
|
66
|
+
*/
|
|
67
|
+
export declare function relayPendingCookies(ctx: HttpContext): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Último método usado" no login (o selo "Last used" do Cloudflare, Vercel, Clerk…).
|
|
3
|
+
*
|
|
4
|
+
* O método fica num cookie DESTE DISPOSITIVO, não na conta. É de propósito:
|
|
5
|
+
* - o passo 1 (identifier) ainda não sabe quem é o usuário, e é exatamente ali
|
|
6
|
+
* que o selo mais ajuda ("da última vez você entrou com Google");
|
|
7
|
+
* - guardar por conta e mostrar no passo 2 diria a quem digita um e-mail qualquer
|
|
8
|
+
* como aquela conta entra — vazamento de enumeração. O cookie só conta ao
|
|
9
|
+
* próprio browser o que ele mesmo fez.
|
|
10
|
+
*
|
|
11
|
+
* O cookie não carrega identidade nenhuma (nem e-mail, nem accountId), só o método.
|
|
12
|
+
* É puramente de SUPERFÍCIE: a tela destaca um botão, mas nada no fluxo muda — os
|
|
13
|
+
* métodos oferecidos continuam vindo de `authMethods`, e um valor adulterado no
|
|
14
|
+
* cookie só destaca outro botão (ou nenhum, se não passar na validação).
|
|
15
|
+
*
|
|
16
|
+
* Gravado quando o fator PRIMÁRIO é aceito. Numa conta com 2º fator, isso acontece
|
|
17
|
+
* antes do desafio de MFA: o selo responde "como você se identificou", que é o
|
|
18
|
+
* botão que a pessoa procura na próxima vez.
|
|
19
|
+
*/
|
|
20
|
+
/** Nome do cookie do último método de login. */
|
|
21
|
+
export const LAST_LOGIN_METHOD_COOKIE = 'authkit_last_login';
|
|
22
|
+
/** Validade do cookie: 1 ano (renovado a cada login). */
|
|
23
|
+
const LAST_LOGIN_METHOD_MAX_AGE = 365 * 24 * 60 * 60;
|
|
24
|
+
const FIXED_METHODS = new Set(['password', 'magic_link', 'otp', 'passkey']);
|
|
25
|
+
/** Ids de provedor/método: mesmo formato curto que vai em rota/config. */
|
|
26
|
+
const QUALIFIED_METHOD = /^(social|custom):[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
27
|
+
/**
|
|
28
|
+
* Valida um valor lido do cookie. Só os métodos conhecidos passam; qualquer outra
|
|
29
|
+
* coisa (ausente, lixo, valor adulterado) vira `undefined` — sem selo.
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeLastLoginMethod(raw) {
|
|
32
|
+
if (typeof raw !== 'string')
|
|
33
|
+
return undefined;
|
|
34
|
+
if (FIXED_METHODS.has(raw) || QUALIFIED_METHOD.test(raw))
|
|
35
|
+
return raw;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/** `false` desliga a feature (`login.lastUsedMethod: false`); ausente = ligada. */
|
|
39
|
+
function isEnabled(cfg) {
|
|
40
|
+
return cfg.login?.lastUsedMethod !== false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Lê o último método deste dispositivo para a tela de login. Fail-safe: feature
|
|
44
|
+
* desligada, ctx sem cookies ou qualquer erro → `undefined` (sem selo).
|
|
45
|
+
*/
|
|
46
|
+
export function readLastLoginMethod(ctx, cfg) {
|
|
47
|
+
if (!isEnabled(cfg))
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
return normalizeLastLoginMethod(ctx.request.plainCookie?.(LAST_LOGIN_METHOD_COOKIE));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Registra o método primário que acabou de ser aceito. Grava via resposta do
|
|
58
|
+
* Adonis; quando o login termina no `interactionFinished` (que encerra a resposta
|
|
59
|
+
* crua), o `completeLogin` repassa o cookie pendente — ver
|
|
60
|
+
* {@link relayPendingCookies}. Best-effort: nunca derruba o login.
|
|
61
|
+
*/
|
|
62
|
+
export function rememberLastLoginMethod(ctx, cfg, method) {
|
|
63
|
+
if (!isEnabled(cfg))
|
|
64
|
+
return;
|
|
65
|
+
if (!normalizeLastLoginMethod(method))
|
|
66
|
+
return;
|
|
67
|
+
try {
|
|
68
|
+
ctx.response.plainCookie?.(LAST_LOGIN_METHOD_COOKIE, method, {
|
|
69
|
+
httpOnly: true,
|
|
70
|
+
sameSite: 'lax',
|
|
71
|
+
path: '/',
|
|
72
|
+
maxAge: LAST_LOGIN_METHOD_MAX_AGE,
|
|
73
|
+
secure: ctx.request.secure?.() ?? false,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Selo é cosmético: falha ao gravar não interfere no login.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Repassa os `Set-Cookie` que o handler gravou na resposta do Adonis para a
|
|
82
|
+
* resposta crua do Node.
|
|
83
|
+
*
|
|
84
|
+
* O Adonis só escreve os headers no `finish()`, depois do handler. Mas o
|
|
85
|
+
* `interactionFinished` do oidc-provider escreve o 303 e dá `res.end()` direto na
|
|
86
|
+
* resposta crua — quando o Adonis chega no `finish()`, a resposta já saiu e todo
|
|
87
|
+
* cookie gravado via `ctx.response.cookie()` no mesmo request é descartado em
|
|
88
|
+
* silêncio. Chamado pelo `completeLogin` logo antes do `interactionFinished`.
|
|
89
|
+
*
|
|
90
|
+
* Os cookies saem do Adonis ao serem repassados, então não há como irem em dobro.
|
|
91
|
+
* O `Set-Cookie` que o próprio oidc-provider grava depois é ACRESCENTADO aos
|
|
92
|
+
* existentes (a lib `cookies` do Koa preserva o header), então nada se perde.
|
|
93
|
+
*/
|
|
94
|
+
export function relayPendingCookies(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
const response = ctx.response;
|
|
97
|
+
const raw = response?.response;
|
|
98
|
+
if (!raw || typeof raw.setHeader !== 'function' || raw.headersSent)
|
|
99
|
+
return;
|
|
100
|
+
const pending = response.getHeader?.('set-cookie');
|
|
101
|
+
if (pending === undefined || pending === null)
|
|
102
|
+
return;
|
|
103
|
+
const toList = (value) => value === undefined || value === null
|
|
104
|
+
? []
|
|
105
|
+
: Array.isArray(value)
|
|
106
|
+
? value.map(String)
|
|
107
|
+
: [String(value)];
|
|
108
|
+
// O `getHeader` do Adonis também enxerga o que já está na resposta crua, então
|
|
109
|
+
// a lista pendente pode repetir o que lá está — dedup preservando a ordem.
|
|
110
|
+
const cookies = [...new Set([...toList(raw.getHeader?.('set-cookie')), ...toList(pending)])];
|
|
111
|
+
// O `removeHeader` do Adonis apaga também da resposta crua — por isso vem
|
|
112
|
+
// ANTES do `setHeader`, que regrava a lista completa.
|
|
113
|
+
response.removeHeader?.('set-cookie');
|
|
114
|
+
raw.setHeader('set-cookie', cookies);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Repasse é best-effort: nunca derruba o login.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readActiveOrgFromHostCtx } from '../host/active_org_cookie.js';
|
|
2
2
|
import { InteractionSessionLostException, isInteractionSessionLost, } from '../host/interaction_recovery.js';
|
|
3
|
+
import { relayPendingCookies } from '../host/last_login_method.js';
|
|
3
4
|
/**
|
|
4
5
|
* Choke point ÚNICO de `interactionDetails`: converte o `SessionNotFound` do
|
|
5
6
|
* `oidc-provider` (sessão de interaction perdida — caso NORMAL) na exceção
|
|
@@ -31,6 +32,7 @@ export function createInteractionActions(provider, deps) {
|
|
|
31
32
|
const account = await deps.verifyCredentials(email, password);
|
|
32
33
|
if (!account)
|
|
33
34
|
return { ok: false };
|
|
35
|
+
relayPendingCookies(ctx);
|
|
34
36
|
await provider.interactionFinished(ctx.request.request, ctx.response.response, { login: { accountId: account.id } }, { mergeWithLastSubmission: false });
|
|
35
37
|
return { ok: true };
|
|
36
38
|
},
|
|
@@ -47,6 +49,10 @@ export function createInteractionActions(provider, deps) {
|
|
|
47
49
|
// O campo `remember` é nativo do oidc-provider v9 — veja resume.js.
|
|
48
50
|
if (extra?.remember === false)
|
|
49
51
|
login.remember = false;
|
|
52
|
+
// O `interactionFinished` encerra a resposta crua: os cookies que o handler
|
|
53
|
+
// gravou no Adonis (último método de login, dispositivo confiável) só saem
|
|
54
|
+
// se forem repassados antes.
|
|
55
|
+
relayPendingCookies(ctx);
|
|
50
56
|
await provider.interactionFinished(ctx.request.request, ctx.response.response, { login }, { mergeWithLastSubmission: false });
|
|
51
57
|
return { ok: true };
|
|
52
58
|
},
|
|
@@ -75,6 +81,7 @@ export function createInteractionActions(provider, deps) {
|
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
const grantId = await grant.save();
|
|
84
|
+
relayPendingCookies(ctx);
|
|
78
85
|
return provider.interactionFinished(ctx.request.request, ctx.response.response, { consent: { grantId } }, { mergeWithLastSubmission: true });
|
|
79
86
|
},
|
|
80
87
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.84.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"qrcode": "1.5.4"
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
|
-
"@adonis-agora/authkit-react": "0.
|
|
119
|
+
"@adonis-agora/authkit-react": "0.26.0",
|
|
120
120
|
"@adonis-agora/durable": "0.53.0",
|
|
121
121
|
"@adonis-agora/telescope": "0.23.0",
|
|
122
122
|
"@adonisjs/ally": "6.3.2",
|