@sentropic/auth-ui 0.2.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/LICENSE +21 -0
- package/README.md +163 -0
- package/dist/contracts.d.ts +7 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +7 -0
- package/dist/contracts.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +5 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +180 -0
- package/dist/labels.js.map +1 -0
- package/dist/transport-fetch.d.ts +25 -0
- package/dist/transport-fetch.d.ts.map +1 -0
- package/dist/transport-fetch.js +98 -0
- package/dist/transport-fetch.js.map +1 -0
- package/dist/transport-types.d.ts +77 -0
- package/dist/transport-types.d.ts.map +1 -0
- package/dist/transport-types.js +2 -0
- package/dist/transport-types.js.map +1 -0
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +29 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +137 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/webauthn.d.ts +26 -0
- package/dist/webauthn.d.ts.map +1 -0
- package/dist/webauthn.js +76 -0
- package/dist/webauthn.js.map +1 -0
- package/package.json +86 -0
- package/src/components/AuthDevicePair.svelte +173 -0
- package/src/components/AuthDevicePair.svelte.d.ts +17 -0
- package/src/components/AuthDevices.svelte +313 -0
- package/src/components/AuthDevices.svelte.d.ts +18 -0
- package/src/components/AuthLogin.svelte +222 -0
- package/src/components/AuthLogin.svelte.d.ts +18 -0
- package/src/components/AuthMagicLinkVerify.svelte +165 -0
- package/src/components/AuthMagicLinkVerify.svelte.d.ts +20 -0
- package/src/components/AuthRegister.svelte +394 -0
- package/src/components/AuthRegister.svelte.d.ts +25 -0
- package/src/contracts.ts +6 -0
- package/src/errors.ts +18 -0
- package/src/index.ts +2 -0
- package/src/labels.ts +186 -0
- package/src/transport-fetch.ts +170 -0
- package/src/transport-types.ts +105 -0
- package/src/transport.ts +33 -0
- package/src/types.ts +153 -0
- package/src/webauthn.ts +133 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
createDefaultAuthUiLabels,
|
|
5
|
+
type AuthUiError,
|
|
6
|
+
type AuthUiLabels,
|
|
7
|
+
type AuthUiTransport,
|
|
8
|
+
} from '../contracts.js';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
transport: AuthUiTransport;
|
|
12
|
+
labels?: Partial<AuthUiLabels>;
|
|
13
|
+
/** Optional source for an initial user code (e.g. from `?user_code=PAIR-XXXX`). */
|
|
14
|
+
userCodeSource?: () => string | null | undefined;
|
|
15
|
+
onPaired?: (deviceName?: string) => void | Promise<void>;
|
|
16
|
+
onError?: (error: AuthUiError) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { transport, labels, userCodeSource, onPaired, onError }: Props = $props();
|
|
20
|
+
|
|
21
|
+
const resolvedLabels = $derived(createDefaultAuthUiLabels(labels ?? {}));
|
|
22
|
+
|
|
23
|
+
let userCode = $state('');
|
|
24
|
+
let deviceName = $state('');
|
|
25
|
+
let submitting = $state(false);
|
|
26
|
+
let error = $state('');
|
|
27
|
+
let success = $state(false);
|
|
28
|
+
let pairedDeviceName = $state<string | undefined>(undefined);
|
|
29
|
+
|
|
30
|
+
onMount(() => {
|
|
31
|
+
const initial = userCodeSource?.();
|
|
32
|
+
if (initial) {
|
|
33
|
+
userCode = initial.toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
async function pair(event?: SubmitEvent): Promise<void> {
|
|
38
|
+
event?.preventDefault();
|
|
39
|
+
error = '';
|
|
40
|
+
const code = userCode.trim().toUpperCase();
|
|
41
|
+
if (!code) {
|
|
42
|
+
error = resolvedLabels.devicePairErrorCodeRequired;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
submitting = true;
|
|
47
|
+
const result = await transport.approveDevicePairing({
|
|
48
|
+
userCode: code,
|
|
49
|
+
deviceName: deviceName.trim() || undefined,
|
|
50
|
+
});
|
|
51
|
+
submitting = false;
|
|
52
|
+
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
const message = result.error.message || resolvedLabels.devicePairErrorGeneric;
|
|
55
|
+
const looksLikeNotFound = /not[\s-]?found|expired|invalid/i.test(message);
|
|
56
|
+
error = looksLikeNotFound ? resolvedLabels.devicePairErrorNotFound : message;
|
|
57
|
+
onError?.(result.error);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
success = true;
|
|
62
|
+
pairedDeviceName = result.value.deviceName;
|
|
63
|
+
await onPaired?.(pairedDeviceName);
|
|
64
|
+
}
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<div class="auth-ui-device-pair">
|
|
68
|
+
<header class="auth-ui-header">
|
|
69
|
+
<h1 class="auth-ui-title">{resolvedLabels.devicePairTitle}</h1>
|
|
70
|
+
<p class="auth-ui-subtitle">{resolvedLabels.devicePairSubtitle}</p>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
{#if error}
|
|
74
|
+
<div class="auth-ui-alert auth-ui-alert--error" role="alert">{error}</div>
|
|
75
|
+
{/if}
|
|
76
|
+
|
|
77
|
+
{#if success}
|
|
78
|
+
<div class="auth-ui-alert auth-ui-alert--success" role="status">
|
|
79
|
+
{resolvedLabels.devicePairSuccess}
|
|
80
|
+
</div>
|
|
81
|
+
<div class="auth-ui-actions">
|
|
82
|
+
<slot name="back-to-devices">
|
|
83
|
+
<span class="auth-ui-link">{resolvedLabels.devicePairBack}</span>
|
|
84
|
+
</slot>
|
|
85
|
+
</div>
|
|
86
|
+
{:else}
|
|
87
|
+
<form class="auth-ui-form" onsubmit={pair}>
|
|
88
|
+
<div class="auth-ui-field">
|
|
89
|
+
<label for="auth-ui-pair-code" class="auth-ui-label">
|
|
90
|
+
{resolvedLabels.devicePairCodeLabel}
|
|
91
|
+
</label>
|
|
92
|
+
<input
|
|
93
|
+
id="auth-ui-pair-code"
|
|
94
|
+
type="text"
|
|
95
|
+
bind:value={userCode}
|
|
96
|
+
oninput={() => (userCode = userCode.toUpperCase())}
|
|
97
|
+
placeholder={resolvedLabels.devicePairCodePlaceholder}
|
|
98
|
+
autocomplete="off"
|
|
99
|
+
class="auth-ui-input auth-ui-input--mono"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="auth-ui-field">
|
|
104
|
+
<label for="auth-ui-pair-device-name" class="auth-ui-label">
|
|
105
|
+
{resolvedLabels.devicePairDeviceNameLabel}
|
|
106
|
+
</label>
|
|
107
|
+
<input
|
|
108
|
+
id="auth-ui-pair-device-name"
|
|
109
|
+
type="text"
|
|
110
|
+
bind:value={deviceName}
|
|
111
|
+
placeholder={resolvedLabels.devicePairDeviceNamePlaceholder}
|
|
112
|
+
autocomplete="off"
|
|
113
|
+
class="auth-ui-input"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div class="auth-ui-form-actions">
|
|
118
|
+
<button
|
|
119
|
+
type="submit"
|
|
120
|
+
disabled={submitting}
|
|
121
|
+
class="auth-ui-button auth-ui-button--primary"
|
|
122
|
+
>
|
|
123
|
+
{submitting ? resolvedLabels.devicePairConfirming : resolvedLabels.devicePairConfirm}
|
|
124
|
+
</button>
|
|
125
|
+
<slot name="cancel">
|
|
126
|
+
<span class="auth-ui-link auth-ui-link--secondary">{resolvedLabels.devicePairBack}</span>
|
|
127
|
+
</slot>
|
|
128
|
+
</div>
|
|
129
|
+
</form>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<style>
|
|
134
|
+
.auth-ui-device-pair {
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 1.5rem;
|
|
138
|
+
max-width: 28rem;
|
|
139
|
+
margin: 0 auto;
|
|
140
|
+
padding: 2rem 1rem;
|
|
141
|
+
font-family: var(--auth-font-family, system-ui, -apple-system, sans-serif);
|
|
142
|
+
color: var(--auth-text, #111827);
|
|
143
|
+
}
|
|
144
|
+
.auth-ui-header { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
145
|
+
.auth-ui-title { margin: 0; font-size: 1.5rem; font-weight: 700; }
|
|
146
|
+
.auth-ui-subtitle { margin: 0; font-size: 0.875rem; color: var(--auth-muted, #6b7280); }
|
|
147
|
+
.auth-ui-alert {
|
|
148
|
+
padding: 0.75rem 1rem;
|
|
149
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
150
|
+
font-size: 0.875rem;
|
|
151
|
+
}
|
|
152
|
+
.auth-ui-alert--error { background: var(--auth-error-bg, #fef2f2); color: var(--auth-error-text, #991b1b); }
|
|
153
|
+
.auth-ui-alert--success { background: var(--auth-success-bg, #f0fdf4); color: var(--auth-success-text, #166534); }
|
|
154
|
+
.auth-ui-form { display: flex; flex-direction: column; gap: 1rem; padding: 1.5rem; background: var(--auth-surface, #ffffff); border-radius: var(--auth-radius-lg, 0.5rem); box-shadow: var(--auth-shadow, 0 1px 2px rgba(0,0,0,0.05)); }
|
|
155
|
+
.auth-ui-field { display: flex; flex-direction: column; gap: 0.375rem; }
|
|
156
|
+
.auth-ui-label { font-size: 0.875rem; font-weight: 500; color: var(--auth-text, #111827); }
|
|
157
|
+
.auth-ui-input {
|
|
158
|
+
padding: 0.5rem 0.75rem;
|
|
159
|
+
border: 1px solid var(--auth-border, #d1d5db);
|
|
160
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
161
|
+
font-size: 0.875rem;
|
|
162
|
+
}
|
|
163
|
+
.auth-ui-input:focus { outline: none; border-color: var(--auth-primary, #4f46e5); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); }
|
|
164
|
+
.auth-ui-input--mono { font-family: var(--auth-mono, ui-monospace, "SFMono-Regular", monospace); letter-spacing: 0.15em; text-transform: uppercase; }
|
|
165
|
+
.auth-ui-form-actions { display: flex; align-items: center; gap: 0.75rem; padding-top: 0.5rem; }
|
|
166
|
+
.auth-ui-button { padding: 0.5rem 1rem; border: none; border-radius: var(--auth-radius, 0.375rem); font-size: 0.875rem; font-weight: 500; cursor: pointer; }
|
|
167
|
+
.auth-ui-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
168
|
+
.auth-ui-button--primary { background: var(--auth-primary, #4f46e5); color: var(--auth-primary-text, #ffffff); }
|
|
169
|
+
.auth-ui-link { color: var(--auth-link, #4f46e5); font-size: 0.875rem; font-weight: 500; text-decoration: none; cursor: pointer; }
|
|
170
|
+
.auth-ui-link--secondary { color: var(--auth-link-secondary, #6b7280); }
|
|
171
|
+
.auth-ui-link:hover { text-decoration: underline; }
|
|
172
|
+
.auth-ui-actions { text-align: center; }
|
|
173
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type {
|
|
3
|
+
AuthUiError,
|
|
4
|
+
AuthUiLabels,
|
|
5
|
+
AuthUiTransport,
|
|
6
|
+
} from '../contracts.js';
|
|
7
|
+
|
|
8
|
+
export interface AuthDevicePairProps {
|
|
9
|
+
transport: AuthUiTransport;
|
|
10
|
+
labels?: Partial<AuthUiLabels>;
|
|
11
|
+
userCodeSource?: () => string | null | undefined;
|
|
12
|
+
onPaired?: (deviceName?: string) => void | Promise<void>;
|
|
13
|
+
onError?: (error: AuthUiError) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare const AuthDevicePair: Component<AuthDevicePairProps>;
|
|
17
|
+
export default AuthDevicePair;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
createDefaultAuthUiLabels,
|
|
5
|
+
type AuthUiCredential,
|
|
6
|
+
type AuthUiError,
|
|
7
|
+
type AuthUiLabels,
|
|
8
|
+
type AuthUiTransport,
|
|
9
|
+
} from '../contracts.js';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
transport: AuthUiTransport;
|
|
13
|
+
labels?: Partial<AuthUiLabels>;
|
|
14
|
+
/** Host-controlled date formatter; defaults to ISO date. */
|
|
15
|
+
formatDate?: (iso: string) => string;
|
|
16
|
+
/** Async confirmation hook so hosts can render a modal instead of `window.confirm`. */
|
|
17
|
+
confirmRevoke?: (message: string) => boolean | Promise<boolean>;
|
|
18
|
+
onUnauthorized?: () => void;
|
|
19
|
+
onError?: (error: AuthUiError) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
transport,
|
|
24
|
+
labels,
|
|
25
|
+
formatDate = defaultFormatDate,
|
|
26
|
+
confirmRevoke = defaultConfirm,
|
|
27
|
+
onUnauthorized,
|
|
28
|
+
onError,
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const resolvedLabels = $derived(createDefaultAuthUiLabels(labels ?? {}));
|
|
32
|
+
|
|
33
|
+
let credentials = $state<AuthUiCredential[]>([]);
|
|
34
|
+
let loading = $state(true);
|
|
35
|
+
let error = $state('');
|
|
36
|
+
let editingId = $state<string | null>(null);
|
|
37
|
+
let editingName = $state('');
|
|
38
|
+
|
|
39
|
+
function defaultFormatDate(iso: string): string {
|
|
40
|
+
return iso.slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function defaultConfirm(message: string): boolean {
|
|
44
|
+
if (typeof window === 'undefined') {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return window.confirm(message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatTemplate(template: string, values: Record<string, string>): string {
|
|
51
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMount(loadCredentials);
|
|
55
|
+
|
|
56
|
+
async function loadCredentials(): Promise<void> {
|
|
57
|
+
loading = true;
|
|
58
|
+
error = '';
|
|
59
|
+
const result = await transport.listCredentials();
|
|
60
|
+
loading = false;
|
|
61
|
+
if (!result.ok) {
|
|
62
|
+
handleError(result.error, resolvedLabels.devicesErrorLoad);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
credentials = result.value.credentials;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function startEdit(credential: AuthUiCredential): void {
|
|
69
|
+
editingId = credential.id;
|
|
70
|
+
editingName = credential.deviceName;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cancelEdit(): void {
|
|
74
|
+
editingId = null;
|
|
75
|
+
editingName = '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function saveDeviceName(credential: AuthUiCredential): Promise<void> {
|
|
79
|
+
const result = await transport.renameCredential({
|
|
80
|
+
credentialId: credential.id,
|
|
81
|
+
deviceName: editingName,
|
|
82
|
+
});
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
handleError(result.error, resolvedLabels.devicesErrorUpdate);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
editingId = null;
|
|
88
|
+
editingName = '';
|
|
89
|
+
await loadCredentials();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function revoke(credential: AuthUiCredential): Promise<void> {
|
|
93
|
+
const message = formatTemplate(resolvedLabels.devicesConfirmRevoke, {
|
|
94
|
+
deviceName: credential.deviceName,
|
|
95
|
+
});
|
|
96
|
+
const confirmed = await confirmRevoke(message);
|
|
97
|
+
if (!confirmed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const result = await transport.revokeCredential({ credentialId: credential.id });
|
|
101
|
+
if (!result.ok) {
|
|
102
|
+
handleError(result.error, resolvedLabels.devicesErrorRevoke);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
await loadCredentials();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleError(err: AuthUiError, fallback: string): void {
|
|
109
|
+
error = err.message || fallback;
|
|
110
|
+
if (err.code === 'transport_error' && err.message.toLowerCase().includes('unauth')) {
|
|
111
|
+
onUnauthorized?.();
|
|
112
|
+
}
|
|
113
|
+
onError?.(err);
|
|
114
|
+
}
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<div class="auth-ui-devices">
|
|
118
|
+
<header class="auth-ui-header">
|
|
119
|
+
<h1 class="auth-ui-title">{resolvedLabels.devicesTitle}</h1>
|
|
120
|
+
<p class="auth-ui-subtitle">{resolvedLabels.devicesSubtitle}</p>
|
|
121
|
+
</header>
|
|
122
|
+
|
|
123
|
+
{#if error}
|
|
124
|
+
<div class="auth-ui-alert auth-ui-alert--error" role="alert">{error}</div>
|
|
125
|
+
{/if}
|
|
126
|
+
|
|
127
|
+
<slot name="pair-cta">
|
|
128
|
+
<div class="auth-ui-cta">
|
|
129
|
+
<p class="auth-ui-cta__text">{resolvedLabels.devicePairSubtitle}</p>
|
|
130
|
+
<span class="auth-ui-button auth-ui-button--primary auth-ui-button--inline">
|
|
131
|
+
{resolvedLabels.devicePairTitle}
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
</slot>
|
|
135
|
+
|
|
136
|
+
{#if loading}
|
|
137
|
+
<div class="auth-ui-loading" role="status">
|
|
138
|
+
<div class="auth-ui-spinner" aria-hidden="true"></div>
|
|
139
|
+
<p class="auth-ui-loading__label">{resolvedLabels.loading}</p>
|
|
140
|
+
</div>
|
|
141
|
+
{:else if credentials.length === 0}
|
|
142
|
+
<div class="auth-ui-empty">
|
|
143
|
+
<p>{resolvedLabels.devicesEmpty}</p>
|
|
144
|
+
<slot name="register-device">
|
|
145
|
+
<span class="auth-ui-button auth-ui-button--primary auth-ui-button--inline">
|
|
146
|
+
{resolvedLabels.devicesRegister}
|
|
147
|
+
</span>
|
|
148
|
+
</slot>
|
|
149
|
+
</div>
|
|
150
|
+
{:else}
|
|
151
|
+
<ul class="auth-ui-list">
|
|
152
|
+
{#each credentials as credential (credential.id)}
|
|
153
|
+
<li class="auth-ui-list__item">
|
|
154
|
+
<div class="auth-ui-list__primary">
|
|
155
|
+
{#if editingId === credential.id}
|
|
156
|
+
<div class="auth-ui-edit">
|
|
157
|
+
<input
|
|
158
|
+
type="text"
|
|
159
|
+
class="auth-ui-input"
|
|
160
|
+
bind:value={editingName}
|
|
161
|
+
onkeydown={(e) => {
|
|
162
|
+
if (e.key === 'Enter') {
|
|
163
|
+
void saveDeviceName(credential);
|
|
164
|
+
} else if (e.key === 'Escape') {
|
|
165
|
+
cancelEdit();
|
|
166
|
+
}
|
|
167
|
+
}}
|
|
168
|
+
/>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="auth-ui-button auth-ui-button--primary auth-ui-button--small"
|
|
172
|
+
onclick={() => saveDeviceName(credential)}
|
|
173
|
+
>
|
|
174
|
+
{resolvedLabels.save}
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
class="auth-ui-button auth-ui-button--ghost auth-ui-button--small"
|
|
179
|
+
onclick={cancelEdit}
|
|
180
|
+
>
|
|
181
|
+
{resolvedLabels.cancel}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
{:else}
|
|
185
|
+
<h3 class="auth-ui-list__name">{credential.deviceName}</h3>
|
|
186
|
+
{/if}
|
|
187
|
+
<div class="auth-ui-list__meta">
|
|
188
|
+
<span>{formatTemplate(resolvedLabels.devicesAddedOn, { date: formatDate(credential.createdAt) })}</span>
|
|
189
|
+
{#if credential.lastUsedAt}
|
|
190
|
+
<span>{formatTemplate(resolvedLabels.devicesLastUsed, { date: formatDate(credential.lastUsedAt) })}</span>
|
|
191
|
+
{/if}
|
|
192
|
+
{#if credential.uv}
|
|
193
|
+
<span class="auth-ui-badge">{resolvedLabels.devicesUvEnabled}</span>
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
{#if editingId !== credential.id}
|
|
198
|
+
<div class="auth-ui-list__actions">
|
|
199
|
+
<button type="button" class="auth-ui-link" onclick={() => startEdit(credential)}>
|
|
200
|
+
{resolvedLabels.devicesRename}
|
|
201
|
+
</button>
|
|
202
|
+
<button type="button" class="auth-ui-link auth-ui-link--danger" onclick={() => revoke(credential)}>
|
|
203
|
+
{resolvedLabels.devicesRevoke}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
</li>
|
|
208
|
+
{/each}
|
|
209
|
+
</ul>
|
|
210
|
+
<div class="auth-ui-actions">
|
|
211
|
+
<slot name="add-device">
|
|
212
|
+
<span class="auth-ui-link">{resolvedLabels.devicesAddNew}</span>
|
|
213
|
+
</slot>
|
|
214
|
+
</div>
|
|
215
|
+
{/if}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<style>
|
|
219
|
+
.auth-ui-devices {
|
|
220
|
+
display: flex; flex-direction: column; gap: 1.5rem;
|
|
221
|
+
max-width: 48rem; margin: 0 auto; padding: 2rem 1rem;
|
|
222
|
+
font-family: var(--auth-font-family, system-ui, -apple-system, sans-serif);
|
|
223
|
+
color: var(--auth-text, #111827);
|
|
224
|
+
}
|
|
225
|
+
.auth-ui-header { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
226
|
+
.auth-ui-title { margin: 0; font-size: 1.75rem; font-weight: 700; }
|
|
227
|
+
.auth-ui-subtitle { margin: 0; font-size: 0.875rem; color: var(--auth-muted, #6b7280); }
|
|
228
|
+
.auth-ui-alert {
|
|
229
|
+
padding: 0.75rem 1rem;
|
|
230
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
231
|
+
font-size: 0.875rem;
|
|
232
|
+
}
|
|
233
|
+
.auth-ui-alert--error { background: var(--auth-error-bg, #fef2f2); color: var(--auth-error-text, #991b1b); }
|
|
234
|
+
.auth-ui-cta {
|
|
235
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
236
|
+
gap: 1rem; padding: 1rem;
|
|
237
|
+
background: var(--auth-info-bg, #eef2ff);
|
|
238
|
+
color: var(--auth-info-text, #312e81);
|
|
239
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
240
|
+
font-size: 0.875rem;
|
|
241
|
+
}
|
|
242
|
+
.auth-ui-cta__text { margin: 0; }
|
|
243
|
+
.auth-ui-loading { text-align: center; padding: 3rem 0; }
|
|
244
|
+
.auth-ui-spinner {
|
|
245
|
+
display: inline-block; width: 3rem; height: 3rem;
|
|
246
|
+
border: 2px solid transparent;
|
|
247
|
+
border-bottom-color: var(--auth-primary, #4f46e5);
|
|
248
|
+
border-radius: 50%; animation: auth-ui-spin 0.75s linear infinite;
|
|
249
|
+
}
|
|
250
|
+
.auth-ui-loading__label { margin-top: 1rem; font-size: 0.875rem; color: var(--auth-muted, #6b7280); }
|
|
251
|
+
@keyframes auth-ui-spin { to { transform: rotate(360deg); } }
|
|
252
|
+
.auth-ui-empty {
|
|
253
|
+
display: flex; flex-direction: column; align-items: center; gap: 1rem;
|
|
254
|
+
padding: 3rem 1rem;
|
|
255
|
+
background: var(--auth-surface, #ffffff);
|
|
256
|
+
border-radius: var(--auth-radius-lg, 0.5rem);
|
|
257
|
+
box-shadow: var(--auth-shadow, 0 1px 2px rgba(0,0,0,0.05));
|
|
258
|
+
color: var(--auth-muted, #6b7280);
|
|
259
|
+
}
|
|
260
|
+
.auth-ui-list {
|
|
261
|
+
margin: 0; padding: 0; list-style: none;
|
|
262
|
+
background: var(--auth-surface, #ffffff);
|
|
263
|
+
border-radius: var(--auth-radius-lg, 0.5rem);
|
|
264
|
+
box-shadow: var(--auth-shadow, 0 1px 2px rgba(0,0,0,0.05));
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
}
|
|
267
|
+
.auth-ui-list__item {
|
|
268
|
+
display: flex; align-items: flex-start; justify-content: space-between;
|
|
269
|
+
gap: 1rem; padding: 1.25rem 1.5rem;
|
|
270
|
+
border-bottom: 1px solid var(--auth-border, #e5e7eb);
|
|
271
|
+
}
|
|
272
|
+
.auth-ui-list__item:last-child { border-bottom: none; }
|
|
273
|
+
.auth-ui-list__primary { flex: 1; display: flex; flex-direction: column; gap: 0.375rem; }
|
|
274
|
+
.auth-ui-list__name { margin: 0; font-size: 1.05rem; font-weight: 600; }
|
|
275
|
+
.auth-ui-list__meta {
|
|
276
|
+
display: flex; flex-wrap: wrap; gap: 0.75rem;
|
|
277
|
+
font-size: 0.8rem; color: var(--auth-muted, #6b7280);
|
|
278
|
+
}
|
|
279
|
+
.auth-ui-list__actions { display: flex; align-items: center; gap: 0.5rem; }
|
|
280
|
+
.auth-ui-edit { display: flex; align-items: center; gap: 0.5rem; }
|
|
281
|
+
.auth-ui-input {
|
|
282
|
+
padding: 0.375rem 0.75rem;
|
|
283
|
+
border: 1px solid var(--auth-border, #d1d5db);
|
|
284
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
285
|
+
font-size: 0.875rem;
|
|
286
|
+
}
|
|
287
|
+
.auth-ui-input:focus { outline: none; border-color: var(--auth-primary, #4f46e5); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); }
|
|
288
|
+
.auth-ui-button {
|
|
289
|
+
padding: 0.5rem 1rem; border: none;
|
|
290
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
291
|
+
font-size: 0.875rem; font-weight: 500; cursor: pointer;
|
|
292
|
+
}
|
|
293
|
+
.auth-ui-button--small { padding: 0.375rem 0.75rem; }
|
|
294
|
+
.auth-ui-button--inline { display: inline-flex; }
|
|
295
|
+
.auth-ui-button--primary { background: var(--auth-primary, #4f46e5); color: var(--auth-primary-text, #ffffff); }
|
|
296
|
+
.auth-ui-button--ghost { background: var(--auth-ghost-bg, #e5e7eb); color: var(--auth-text, #111827); }
|
|
297
|
+
.auth-ui-badge {
|
|
298
|
+
display: inline-flex; align-items: center;
|
|
299
|
+
padding: 0.15rem 0.5rem;
|
|
300
|
+
background: var(--auth-success-bg, #d1fae5);
|
|
301
|
+
color: var(--auth-success-text, #065f46);
|
|
302
|
+
border-radius: var(--auth-radius, 0.375rem);
|
|
303
|
+
font-size: 0.7rem; font-weight: 500;
|
|
304
|
+
}
|
|
305
|
+
.auth-ui-link {
|
|
306
|
+
background: none; border: none; cursor: pointer;
|
|
307
|
+
color: var(--auth-link, #4f46e5);
|
|
308
|
+
font-size: 0.875rem; font-weight: 500;
|
|
309
|
+
}
|
|
310
|
+
.auth-ui-link--danger { color: var(--auth-danger, #dc2626); }
|
|
311
|
+
.auth-ui-link:hover { text-decoration: underline; }
|
|
312
|
+
.auth-ui-actions { text-align: center; }
|
|
313
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type {
|
|
3
|
+
AuthUiError,
|
|
4
|
+
AuthUiLabels,
|
|
5
|
+
AuthUiTransport,
|
|
6
|
+
} from '../contracts.js';
|
|
7
|
+
|
|
8
|
+
export interface AuthDevicesProps {
|
|
9
|
+
transport: AuthUiTransport;
|
|
10
|
+
labels?: Partial<AuthUiLabels>;
|
|
11
|
+
formatDate?: (iso: string) => string;
|
|
12
|
+
confirmRevoke?: (message: string) => boolean | Promise<boolean>;
|
|
13
|
+
onUnauthorized?: () => void;
|
|
14
|
+
onError?: (error: AuthUiError) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
declare const AuthDevices: Component<AuthDevicesProps>;
|
|
18
|
+
export default AuthDevices;
|