141 lines
4.1 KiB
Vue
141 lines
4.1 KiB
Vue
<script setup lang="ts">
|
||
import { ArrowUpRight, LockKeyhole, Video, ShieldCheck } from "lucide-vue-next";
|
||
const { t } = useI18n();
|
||
useHead({ title: computed(() => `${t("login.title")} · Webka`) });
|
||
const session = useSessionStore();
|
||
const username = ref(""),
|
||
password = ref(""),
|
||
token = ref(""),
|
||
setup = ref(false),
|
||
loading = ref(true),
|
||
error = ref("");
|
||
const apiError = useApiError();
|
||
onMounted(async () => {
|
||
try {
|
||
if (await session.refresh()) {
|
||
await navigateTo(session.user?.role === "admin" ? "/host" : "/watch");
|
||
return;
|
||
}
|
||
setup.value = (
|
||
await $fetch<{ needs_setup: boolean }>("/api/bootstrap")
|
||
).needs_setup;
|
||
} catch (e) {
|
||
error.value = apiError(e);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
});
|
||
async function submit() {
|
||
error.value = "";
|
||
loading.value = true;
|
||
try {
|
||
if (setup.value)
|
||
await $fetch("/api/bootstrap", {
|
||
method: "POST",
|
||
body: {
|
||
username: username.value,
|
||
password: password.value,
|
||
setup_token: token.value,
|
||
},
|
||
});
|
||
await $fetch("/api/login", {
|
||
method: "POST",
|
||
body: { username: username.value, password: password.value },
|
||
});
|
||
await session.refresh();
|
||
await navigateTo(session.user?.role === "admin" ? "/host" : "/watch");
|
||
} catch (e) {
|
||
error.value = apiError(e);
|
||
try {
|
||
setup.value = (
|
||
await $fetch<{ needs_setup: boolean }>("/api/bootstrap")
|
||
).needs_setup;
|
||
} catch {}
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
</script>
|
||
<template>
|
||
<div class="login-layout">
|
||
<div class="login-intro">
|
||
<span class="eyebrow"
|
||
><span class="status-dot" />{{ $t("login.local") }}</span
|
||
>
|
||
<h1>
|
||
{{ $t("login.headline") }}<br /><span>{{
|
||
$t("login.headlineAccent")
|
||
}}</span>
|
||
</h1>
|
||
<p>{{ $t("login.description") }}</p>
|
||
<div class="login-principle">
|
||
<ShieldCheck :size="22" /><span>{{ $t("login.privacy") }}</span>
|
||
</div>
|
||
</div>
|
||
<GlassCard class="login-card"
|
||
><div class="login-icon"><Video :size="27" /></div>
|
||
<h2>{{ $t(setup ? "login.setupTitle" : "login.title") }}</h2>
|
||
<p class="muted">
|
||
{{ $t(setup ? "login.setupDescription" : "login.subtitle") }}
|
||
</p>
|
||
<form @submit.prevent="submit">
|
||
<label
|
||
>{{ $t("accounts.username")
|
||
}}<input
|
||
v-model="username"
|
||
autocomplete="username"
|
||
required
|
||
minlength="3"
|
||
maxlength="40"
|
||
pattern="[A-Za-z0-9_.\-]+"
|
||
:placeholder="$t('login.usernamePlaceholder')"
|
||
/></label>
|
||
<label
|
||
>{{ $t("accounts.password")
|
||
}}<input
|
||
v-model="password"
|
||
type="password"
|
||
:autocomplete="setup ? 'new-password' : 'current-password'"
|
||
required
|
||
:minlength="setup ? 12 : 1"
|
||
maxlength="256"
|
||
:pattern="
|
||
setup ? '(?=.*[A-Za-zА-Яа-яЁёÀ-ÿ])(?=.*[0-9]).{12,}' : undefined
|
||
"
|
||
:placeholder="$t('login.passwordPlaceholder')"
|
||
/></label>
|
||
<template v-if="setup"
|
||
><p class="field-help">{{ $t("accounts.passwordHint") }}</p>
|
||
<label
|
||
>{{ $t("login.setupToken")
|
||
}}<input v-model="token" required autocomplete="off"
|
||
/></label>
|
||
<p class="field-help">{{ $t("login.tokenHint") }}</p></template
|
||
>
|
||
<p v-if="error" role="alert" class="error">{{ error }}</p>
|
||
<GlassButton
|
||
variant="primary"
|
||
type="submit"
|
||
class="full-width"
|
||
:disabled="loading"
|
||
>{{
|
||
$t(
|
||
loading
|
||
? "common.working"
|
||
: setup
|
||
? "login.create"
|
||
: "login.signin",
|
||
)
|
||
}}<ArrowUpRight :size="19"
|
||
/></GlassButton>
|
||
</form>
|
||
<div class="login-foot">
|
||
<LockKeyhole :size="14" />{{ $t("login.hostAccounts") }}
|
||
</div>
|
||
</GlassCard>
|
||
<div class="login-bottom">
|
||
WEBKA <span>01 / {{ $t("login.localUtility") }}</span>
|
||
</div>
|
||
</div>
|
||
</template>
|