DUNGEONS-34: implement Webka local streaming app

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-09-06 16:23:09 +05:00
parent 5540b9a1f4
commit 7c737c1011
36 changed files with 21596 additions and 0 deletions

140
frontend/pages/login.vue Normal file
View File

@@ -0,0 +1,140 @@
<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>