feat(auth): add user profile, phone login, and account deletion
This commit is contained in:
@@ -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>
|
||||
@@ -4,7 +4,9 @@
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
|
||||
let mode = $state<'login' | 'register'>('login');
|
||||
let identifier = $state('');
|
||||
let email = $state('');
|
||||
let phone = $state('');
|
||||
let password = $state('');
|
||||
let name = $state('');
|
||||
let error = $state('');
|
||||
@@ -13,12 +15,16 @@
|
||||
async function submit(ev: Event) {
|
||||
ev.preventDefault();
|
||||
error = '';
|
||||
if (mode === 'register' && !email.trim() && !phone.trim()) {
|
||||
error = 'Укажите email или телефон';
|
||||
return;
|
||||
}
|
||||
busy = true;
|
||||
try {
|
||||
const data =
|
||||
mode === 'login'
|
||||
? await authApi.login(email, password)
|
||||
: await authApi.register(email, password, name);
|
||||
? await authApi.login(identifier, password)
|
||||
: await authApi.register(email, phone, password, name);
|
||||
auth.setTokens(data.tokens);
|
||||
auth.setUser(data.user);
|
||||
goto('/');
|
||||
@@ -43,16 +49,34 @@
|
||||
class="border-border bg-bg-elevated 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">Email (необязательно)</span>
|
||||
<input
|
||||
bind:value={email}
|
||||
type="email"
|
||||
class="border-border bg-bg-elevated 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={phone}
|
||||
type="tel"
|
||||
placeholder="+15551234567"
|
||||
class="border-border bg-bg-elevated text-text w-full rounded-lg border px-3 py-2"
|
||||
/>
|
||||
</label>
|
||||
<p class="text-text-muted text-xs">Нужен хотя бы один из email или телефона.</p>
|
||||
{:else}
|
||||
<label class="block">
|
||||
<span class="text-text-muted mb-1 block text-sm">Email или телефон</span>
|
||||
<input
|
||||
bind:value={identifier}
|
||||
required
|
||||
class="border-border bg-bg-elevated text-text w-full rounded-lg border px-3 py-2"
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
<label class="block">
|
||||
<span class="text-text-muted mb-1 block text-sm">Email</span>
|
||||
<input
|
||||
bind:value={email}
|
||||
type="email"
|
||||
required
|
||||
class="border-border bg-bg-elevated 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
|
||||
|
||||
Reference in New Issue
Block a user