blob: 9e90e78f069e2ef3a3483765daa78fdcf4d68096 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
<script lang="ts">
import Page from '../components/Page.svelte';
import Status from '../components/Status.svelte';
import { session } from '../lib/session.svelte';
import { getSessions, patchProfile, postPassword, deleteSession } from '../lib/client';
import { extractStatusMessage } from '../lib/state';
import { fmtDateTime } from '../lib/format';
import type { SessionInfo } from '../lib/types';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import * as Table from '$lib/components/ui/table';
let nameInput = $state(session.me?.name ?? '');
let profileBusy = $state(false);
let profileOk = $state<string | null>(null);
let profileErr = $state<string | null>(null);
let curPw = $state('');
let newPw = $state('');
let newPw2 = $state('');
let pwBusy = $state(false);
let pwErr = $state<string | null>(null);
let pwOk = $state<string | null>(null);
let sessions: SessionInfo[] = $state([]);
let sessBusy = $state(true);
let sessErr = $state<string | null>(null);
let sessMsg = $state<string | null>(null);
$effect(() => {
nameInput = session.me?.name ?? '';
void loadSessions();
});
async function loadSessions() {
sessBusy = true;
sessErr = null;
try {
const { items } = await getSessions();
sessions = items;
} catch (e) {
sessErr = extractStatusMessage(e);
} finally {
sessBusy = false;
}
}
async function saveProfile(e: SubmitEvent) {
e.preventDefault();
profileBusy = true;
profileOk = null;
profileErr = null;
try {
await patchProfile(nameInput.trim());
profileOk = '资料已更新';
await session.refresh();
} catch (err) {
profileErr = extractStatusMessage(err);
} finally {
profileBusy = false;
}
}
async function changePassword(e: SubmitEvent) {
e.preventDefault();
pwErr = null;
if (newPw.length < 8) {
pwErr = '新密码长度需至少 8 位';
return;
}
if (newPw !== newPw2) {
pwErr = '两次输入的新密码不一致';
return;
}
pwBusy = true;
try {
await postPassword(curPw, newPw);
pwOk = '密码已更新;其他会话已被登出,请用新密码重新登录本机外的应用。';
curPw = '';
newPw = '';
newPw2 = '';
} catch (err) {
pwErr = extractStatusMessage(err);
} finally {
pwBusy = false;
}
}
async function revoke(s: SessionInfo) {
sessMsg = null;
sessErr = null;
try {
await deleteSession(s.id);
sessMsg = s.current ? '该会话已注销;如果你注销的是当前会话,会稍后自动退出。' : '会话已撤销';
await loadSessions();
await session.refresh();
} catch (e) {
sessErr = extractStatusMessage(e);
}
}
</script>
<Page title="账户与安全" subtitle="资料、密码与个人会话管理">
<div class="stack">
<Card.Root size="sm" >
<Card.Header><Card.Title>个人信息</Card.Title></Card.Header>
<form onsubmit={saveProfile} class="row">
<div style="flex:1 1 200px">
<Label for="acc-name">姓名</Label>
<Input id="acc-name" bind:value={nameInput} autocomplete="name" />
</div>
<div>
邮箱
<div class="text-muted-foreground">{session.me?.email ?? "—"}</div>
</div>
<div>
角色
<div>{session.me?.role === 'admin' ? '管理员' : '成员'}{session.me?.active === false ? ' · 已停用' : ''}</div>
</div>
<Button type="submit" disabled={profileBusy || nameInput.trim().length === 0}>
{profileBusy ? '保存中…' : '保存资料'}
</Button>
</form>
<Status busy={false} error={profileErr} empty={profileOk} />
</Card.Root>
<Card.Root size="sm" >
<Card.Header><Card.Title>修改密码</Card.Title></Card.Header>
<form onsubmit={changePassword} class="stack">
<div class="grid3 wrap-3">
<div>
<Label for="pw-cur">当前密码</Label>
<Input id="pw-cur" type="password" bind:value={curPw} required autocomplete="current-password" />
</div>
<div>
<Label for="pw-new">新密码</Label>
<Input id="pw-new" type="password" bind:value={newPw} required autocomplete="new-password" minlength={8} />
</div>
<div>
<Label for="pw-new2">确认新密码</Label>
<Input id="pw-new2" type="password" bind:value={newPw2} required autocomplete="new-password" minlength={8} />
</div>
</div>
<div class="row">
<Button type="submit" disabled={pwBusy}>
{pwBusy ? '更新中…' : '更新密码'}
</Button>
</div>
<Status busy={false} error={pwErr} empty={pwOk} />
</form>
</Card.Root>
<Card.Root size="sm" >
<Card.Header><Card.Title>登录会话</Card.Title></Card.Header>
<Status busy={sessBusy} error={sessErr} busyText="载入会话…" empty={null} />
{#if !sessBusy && !sessErr && sessions.length === 0}
<Status busy={false} empty="没有其他会话记录" />
{/if}
{#if sessions.length > 0}
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>创建</Table.Head>
<Table.Head>最近活跃</Table.Head>
<Table.Head>设备/UA</Table.Head>
<Table.Head></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each sessions as s (s.id)}
<Table.Row>
<Table.Cell>{fmtDateTime(s.created_at)}</Table.Cell>
<Table.Cell>{fmtDateTime(s.last_seen_at)}</Table.Cell>
<Table.Cell class="hint">{s.user_agent?.slice(0, 60) ?? '—'}</Table.Cell>
<Table.Cell>
<div class="row">
{#if s.current}<Badge variant="secondary">当前会话</Badge>{/if}
<Button variant="outline" size="sm" class="text-destructive" disabled={s.current && sessions.length === 1} onclick={() => void revoke(s)}>
{s.current ? '注销当前' : '撤销'}
</Button>
</div>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
{/if}
<Status busy={false} error={null} empty={sessMsg} />
<p class="hint">更改密码或被管理员停用时,其他会话将自动失效。</p>
</Card.Root>
</div>
</Page>
|