feat(api,userbot,frontend): add accounts from the web ui and isolate per-account settings
This commit is contained in:
1 parent
92fd20137e
commit
9c265af3d3
19 files changed
+917
-268
No files matched your search
@@ -0,0 +1,169 @@
|
||||
<script lang="ts">
|
||||
import { Dialog } from "bits-ui";
|
||||
import { ApiError } from "$lib/api/client";
|
||||
import {
|
||||
cancelLogin,
|
||||
startLogin,
|
||||
submitLoginCode,
|
||||
submitLoginPassword,
|
||||
} from "$lib/api/endpoints";
|
||||
import type { LoginState } from "$lib/api/types";
|
||||
import Button from "$lib/components/ui/Button.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import { accounts } from "$lib/stores/accounts.svelte";
|
||||
import { toasts } from "$lib/stores/toasts.svelte";
|
||||
|
||||
type Step = "phone" | "code" | "password";
|
||||
|
||||
let { open = $bindable(false) }: { open?: boolean } = $props();
|
||||
|
||||
let step = $state<Step>("phone");
|
||||
let loginId = $state("");
|
||||
let phone = $state("");
|
||||
let code = $state("");
|
||||
let password = $state("");
|
||||
let busy = $state(false);
|
||||
|
||||
const hints: Record<Step, string> = {
|
||||
phone: "Номер телефона в международном формате, например +79991234567.",
|
||||
code: "Код отправлен в Telegram на этот номер.",
|
||||
password: "Аккаунт защищён двухэтапной аутентификацией.",
|
||||
};
|
||||
|
||||
const filled = $derived.by(() => {
|
||||
if (step === "phone") {
|
||||
return phone.trim().length > 0;
|
||||
}
|
||||
if (step === "code") {
|
||||
return code.trim().length > 0;
|
||||
}
|
||||
return password.length > 0;
|
||||
});
|
||||
|
||||
function reset() {
|
||||
step = "phone";
|
||||
loginId = "";
|
||||
phone = "";
|
||||
code = "";
|
||||
password = "";
|
||||
}
|
||||
|
||||
function next(): Promise<LoginState> {
|
||||
if (step === "phone") {
|
||||
return startLogin(phone.trim());
|
||||
}
|
||||
if (step === "code") {
|
||||
return submitLoginCode(loginId, code.trim());
|
||||
}
|
||||
return submitLoginPassword(loginId, password);
|
||||
}
|
||||
|
||||
async function apply(state: LoginState) {
|
||||
if (state.stage !== "done") {
|
||||
loginId = state.login_id;
|
||||
step = state.stage;
|
||||
return;
|
||||
}
|
||||
loginId = "";
|
||||
await accounts.load();
|
||||
if (state.account) {
|
||||
accounts.select(state.account.account_id);
|
||||
}
|
||||
toasts.success("Аккаунт добавлен");
|
||||
open = false;
|
||||
}
|
||||
|
||||
async function submit(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (busy || !filled) {
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
await apply(await next());
|
||||
} catch (error) {
|
||||
toasts.error(
|
||||
error instanceof ApiError ? error.detail : "Не удалось войти"
|
||||
);
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onOpenChange(value: boolean) {
|
||||
if (!value && loginId) {
|
||||
cancelLogin(loginId).catch(() => undefined);
|
||||
}
|
||||
reset();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {onOpenChange}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="dialog-overlay" />
|
||||
<Dialog.Content class="dialog-content">
|
||||
<header class="dialog-head">
|
||||
<Dialog.Title class="dialog-title">Добавить аккаунт</Dialog.Title>
|
||||
<Dialog.Close class="dialog-close" aria-label="Закрыть">
|
||||
<Icon name="close" size="1.25rem" />
|
||||
</Dialog.Close>
|
||||
</header>
|
||||
<form onsubmit={submit}>
|
||||
<div class="dialog-body">
|
||||
<p class="hint">{hints[step]}</p>
|
||||
{#if step === "phone"}
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="login-phone"
|
||||
class="form-control"
|
||||
type="tel"
|
||||
autocomplete="tel"
|
||||
placeholder="+7 999 123-45-67"
|
||||
bind:value={phone}
|
||||
>
|
||||
<label for="login-phone">Номер телефона</label>
|
||||
</div>
|
||||
{:else if step === "code"}
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="login-code"
|
||||
class="form-control"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
placeholder="12345"
|
||||
bind:value={code}
|
||||
>
|
||||
<label for="login-code">Код подтверждения</label>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="login-password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="Пароль"
|
||||
bind:value={password}
|
||||
>
|
||||
<label for="login-password">Облачный пароль</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<Button type="submit" pill loading={busy} disabled={busy || !filled}>
|
||||
{step === "phone" ? "Отправить код" : "Продолжить"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
||||
<style lang="scss">
|
||||
.hint {
|
||||
margin: 0 0 1.5rem;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user