feat(auth): add user profile, phone login, and account deletion

This commit is contained in:
iu
2026-10-12 14:26:32 +03:00
parent 1cb9aaf198
commit 86aff695df
46 changed files with 2893 additions and 134 deletions
@@ -0,0 +1,226 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { authApi } from '$lib/api';
import Icon from '$lib/components/Icon.svelte';
import { auth } from '$lib/stores/auth.svelte';
let user = $state(auth.user);
let name = $state(auth.user?.name ?? '');
let newEmail = $state('');
let newPhone = $state('');
let currentPassword = $state('');
let newPassword = $state('');
let deletePassword = $state('');
let error = $state('');
let notice = $state('');
$effect(() => {
load();
});
async function load() {
try {
const data = await authApi.me();
user = data.user;
auth.setUser(data.user);
name = data.user.name;
} catch (e) {
error = (e as Error).message;
}
}
function applyUser(updated: typeof user) {
user = updated;
if (updated) auth.setUser(updated);
}
async function saveName(ev: Event) {
ev.preventDefault();
error = '';
notice = '';
try {
const data = await authApi.updateProfile(name);
applyUser(data.user);
notice = 'Имя сохранено';
} catch (e) {
error = (e as Error).message;
}
}
async function addIdentifier(ev: Event) {
ev.preventDefault();
error = '';
notice = '';
try {
const data = await authApi.addIdentifier(newEmail, newPhone);
applyUser(data.user);
newEmail = '';
newPhone = '';
notice = 'Идентификатор добавлен';
} catch (e) {
error = (e as Error).message;
}
}
async function changePassword(ev: Event) {
ev.preventDefault();
error = '';
notice = '';
try {
await authApi.changePassword(currentPassword, newPassword);
auth.clear();
goto('/login');
} catch (e) {
error = (e as Error).message;
}
}
async function removeAccount() {
error = '';
if (!confirm('Удалить аккаунт со всем содержимым? Действие необратимо.')) return;
try {
await authApi.deleteAccount(deletePassword);
auth.clear();
goto('/login');
} catch (e) {
error = (e as Error).message;
}
}
</script>
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">Профиль</h1>
<a href="/" class="text-primary inline-flex items-center gap-1 text-sm">
<Icon name="arrow_back" class="size-4" />
К альбомам
</a>
</div>
{#if error}<p class="text-danger mb-4 text-sm">{error}</p>{/if}
{#if notice}<p class="text-success mb-4 text-sm">{notice}</p>{/if}
<div class="border-border bg-bg-elevated mb-6 rounded-xl border p-4">
<h2 class="mb-2 font-semibold">Идентификаторы</h2>
<ul class="text-text-muted space-y-1 text-sm">
<li>Email: {user?.email || '—'}</li>
<li>Телефон: {user?.phone || '—'}</li>
</ul>
</div>
<form class="border-border bg-bg-elevated mb-6 space-y-3 rounded-xl border p-4" onsubmit={saveName}>
<h2 class="font-semibold">Имя</h2>
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Отображаемое имя</span>
<input
bind:value={name}
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
<button
class="bg-primary text-primary-fg inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm"
type="submit"
>
<Icon name="save" class="size-4" />
Сохранить
</button>
</form>
{#if !user?.email || !user?.phone}
<form
class="border-border bg-bg-elevated mb-6 space-y-3 rounded-xl border p-4"
onsubmit={addIdentifier}
>
<h2 class="font-semibold">Добавить идентификатор</h2>
{#if !user?.email}
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Email</span>
<input
bind:value={newEmail}
type="email"
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
{/if}
{#if !user?.phone}
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Телефон (E.164)</span>
<input
bind:value={newPhone}
type="tel"
placeholder="+15551234567"
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
{/if}
<p class="text-text-muted text-xs">Заполните одно поле — заданный идентификатор изменить нельзя.</p>
<button
class="bg-primary text-primary-fg inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm"
type="submit"
>
<Icon name="add" class="size-4" />
Добавить
</button>
</form>
{:else}
<div class="border-border bg-bg-elevated mb-6 rounded-xl border p-4">
<h2 class="mb-2 font-semibold">Добавить идентификатор</h2>
<p class="text-text-muted text-sm">Оба идентификатора заданы.</p>
</div>
{/if}
<form
class="border-border bg-bg-elevated mb-6 space-y-3 rounded-xl border p-4"
onsubmit={changePassword}
>
<h2 class="font-semibold">Смена пароля</h2>
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Текущий пароль</span>
<input
bind:value={currentPassword}
type="password"
required
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Новый пароль</span>
<input
bind:value={newPassword}
type="password"
required
minlength="8"
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
<p class="text-text-muted text-xs">После смены пароля все сессии будут завершены.</p>
<button
class="bg-primary text-primary-fg inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm"
type="submit"
>
<Icon name="save" class="size-4" />
Сменить пароль
</button>
</form>
<div class="border-danger/50 bg-bg-elevated space-y-3 rounded-xl border p-4">
<h2 class="text-danger font-semibold">Удаление аккаунта</h2>
<p class="text-text-muted text-sm">
Аккаунт и всё его содержимое удаляются немедленно и необратимо.
</p>
<label class="block">
<span class="text-text-muted mb-1 block text-sm">Подтвердите пароль</span>
<input
bind:value={deletePassword}
type="password"
class="border-border bg-bg text-text w-full rounded-lg border px-3 py-2"
/>
</label>
<button
type="button"
class="border-danger text-danger inline-flex items-center gap-1.5 rounded-lg border px-4 py-2 text-sm"
onclick={removeAccount}
>
<Icon name="delete_forever" class="size-4" />
Удалить аккаунт
</button>
</div>