160 lines
6.5 KiB
Vue
160 lines
6.5 KiB
Vue
<script setup lang="ts">
|
|
const emit = defineEmits(['confirm', 'cancel']);
|
|
|
|
const form = reactive({
|
|
name: '',
|
|
email: '',
|
|
});
|
|
|
|
const busy = ref(false);
|
|
const error = ref('');
|
|
|
|
// set once the account exists, the dialog then shows its setup link
|
|
const link = ref('');
|
|
const copied = ref(false);
|
|
|
|
const nameInputEl = ref<HTMLInputElement | null>(null);
|
|
|
|
onMounted(() => {
|
|
nameInputEl.value?.focus();
|
|
});
|
|
|
|
const ready = computed(() => form.name.trim() !== '' && form.email.trim() !== '');
|
|
|
|
const copyLink = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(link.value);
|
|
copied.value = true;
|
|
setTimeout(() => {
|
|
copied.value = false;
|
|
}, 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy setup link:', err);
|
|
}
|
|
};
|
|
|
|
const submit = async () => {
|
|
if (busy.value || !ready.value) return;
|
|
|
|
busy.value = true;
|
|
error.value = '';
|
|
|
|
try {
|
|
const { token } = await $fetch<{ token: string }>('/api/admin/users', {
|
|
method: 'POST',
|
|
body: {
|
|
name: form.name.trim(),
|
|
email: form.email.trim(),
|
|
},
|
|
});
|
|
|
|
link.value = `${window.location.origin}/auth/recover/${token}`;
|
|
} catch (err: any) {
|
|
busy.value = false;
|
|
|
|
switch (err?.data?.data?.code as string | undefined) {
|
|
case 'USER_ALREADY_EXISTS':
|
|
case 'USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL':
|
|
error.value = 'An account with this email already exists.';
|
|
break;
|
|
case 'INVALID_EMAIL':
|
|
error.value = 'That email address is not valid.';
|
|
break;
|
|
case 'INVALID_BODY':
|
|
error.value = 'Check the name and the email address.';
|
|
break;
|
|
default:
|
|
console.error('Failed to create user:', err);
|
|
error.value = 'The account could not be created. Try again.';
|
|
break;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
busy.value = false;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<form class="flex flex-col gap-4.5 p-6 w-screen md:w-130 max-w-full h-full md:h-auto overflow-y-auto"
|
|
aria-labelledby="create-user-title" @submit.prevent="submit">
|
|
<header class="flex flex-col gap-1">
|
|
<h2 id="create-user-title" class="text-lg font-semibold">
|
|
{{ link ? 'Account created' : 'Create user' }}
|
|
</h2>
|
|
<p class="text-sm text-[var(--text-secondary)]">
|
|
{{ link
|
|
? `Send ${form.name.trim()} this link. They open it, choose a password and are in.`
|
|
: 'Creates the account without a password. You get a link for them to choose their own, so you never know it.' }}
|
|
</p>
|
|
</header>
|
|
|
|
<template v-if="link">
|
|
<div class="flex flex-col gap-1.5">
|
|
<label for="create-user-link"
|
|
class="text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
|
Setup link
|
|
</label>
|
|
<div class="flex items-center rounded-md bg-[var(--bg-container)]">
|
|
<input id="create-user-link" type="text" readonly :value="link"
|
|
@focus="($event.target as HTMLInputElement).select()"
|
|
class="flex-1 min-w-0 h-9 px-2.5 text-[13px] font-mono bg-transparent outline-none" />
|
|
<button type="button" :aria-label="copied ? 'Copied' : 'Copy setup link'" @click="copyLink"
|
|
class="flex items-center justify-center w-9 h-9 flex-shrink-0 text-[var(--text-secondary)] @hover:text-[var(--text-primary)]">
|
|
<span class="text-4.5" :class="copied ? 'i-mynaui-check' : 'i-mynaui-copy'"></span>
|
|
</button>
|
|
</div>
|
|
<span class="text-xs leading-normal text-[var(--text-secondary)]">
|
|
Works once. Expires in 7 days. Shown only now; if it gets lost, make a new one from the
|
|
user's menu. Nobody can sign in to the account until it is used.
|
|
</span>
|
|
</div>
|
|
|
|
<footer class="flex justify-end mt-auto">
|
|
<button type="button"
|
|
class="flex items-center h-10 px-4 rounded-md font-semibold bg-[var(--color-accent)] text-[var(--color-accent-text)]"
|
|
@click="emit('confirm', true)">
|
|
Done
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="flex flex-col gap-1.5">
|
|
<label for="create-user-name"
|
|
class="text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
|
Name
|
|
</label>
|
|
<input id="create-user-name" ref="nameInputEl" v-model="form.name" type="text" required
|
|
maxlength="128" :disabled="busy" autocomplete="off"
|
|
class="w-full h-9 px-2.5 text-sm rounded-md bg-[var(--bg-container)]" />
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1.5">
|
|
<label for="create-user-email"
|
|
class="text-[13px] font-medium uppercase tracking-wider text-[var(--text-secondary)]">
|
|
Email
|
|
</label>
|
|
<input id="create-user-email" v-model="form.email" type="email" required :disabled="busy"
|
|
autocomplete="off" class="w-full h-9 px-2.5 text-sm rounded-md bg-[var(--bg-container)]" />
|
|
</div>
|
|
|
|
<p v-if="error" role="alert" class="text-sm text-red-400">{{ error }}</p>
|
|
|
|
<footer class="flex justify-between gap-2 mt-auto">
|
|
<button type="button" :disabled="busy"
|
|
class="flex items-center h-10 px-4 rounded-md border border-[var(--color-border)] text-[var(--text-secondary)] @hover:bg-[var(--color-hover)] disabled:opacity-50 disabled:cursor-not-allowed transition-[background-color] duration-200"
|
|
@click="emit('cancel')">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" :disabled="busy || !ready"
|
|
class="flex items-center gap-2 h-10 px-4 rounded-md font-semibold bg-[var(--color-accent)] text-[var(--color-accent-text)] disabled:opacity-40 disabled:cursor-not-allowed">
|
|
<span v-if="busy" class="i-svg-spinners-180-ring-with-bg text-4"></span>
|
|
Create user
|
|
</button>
|
|
</footer>
|
|
</template>
|
|
</form>
|
|
</template>
|