summaryrefslogtreecommitdiff
path: root/frontend/src/pages/AccountPage.svelte
blob: ef6d1a33dd4aceaca136aae2bf26cf5028be0921 (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
<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';

  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">
    <section class="card stack">
      <h2>个人信息</h2>
      <form onsubmit={saveProfile} class="row">
        <label style="flex:1 1 200px">
          姓名
          <input bind:value={nameInput} autocomplete="name" />
        </label>
        <div>
          邮箱
          <div class="fixed">{session.me?.email ?? '—'}</div>
        </div>
        <div>
          角色
          <div>{session.me?.role === 'admin' ? '管理员' : '成员'}{session.me?.active === false ? ' · 已停用' : ''}</div>
        </div>
        <button class="btn primary" disabled={profileBusy || nameInput.trim().length === 0}>
          {profileBusy ? '保存中…' : '保存资料'}
        </button>
      </form>
      <Status busy={false} error={profileErr} empty={profileOk} />
    </section>

    <section class="card stack">
      <h2>修改密码</h2>
      <form onsubmit={changePassword} class="stack">
        <div class="grid3 wrap-3">
          <label>
            当前密码
            <input type="password" bind:value={curPw} required autocomplete="current-password" />
          </label>
          <label>
            新密码
            <input type="password" bind:value={newPw} required autocomplete="new-password" minlength={8} />
          </label>
          <label>
            确认新密码
            <input type="password" bind:value={newPw2} required autocomplete="new-password" minlength={8} />
          </label>
        </div>
        <div class="row">
          <button class="btn primary" disabled={pwBusy}>
            {pwBusy ? '更新中…' : '更新密码'}
          </button>
        </div>
        <Status busy={false} error={pwErr} empty={pwOk} />
      </form>
    </section>

    <section class="card stack">
      <h2>登录会话</h2>
      <Status busy={sessBusy} error={sessErr} busyText="载入会话…" empty={null} />
      {#if !sessBusy && !sessErr && sessions.length === 0}
        <Status busy={false} empty="没有其他会话记录" />
      {/if}
      {#if sessions.length > 0}
        <table class="data">
          <thead>
            <tr>
              <th>创建</th>
              <th>最近活跃</th>
              <th>设备/UA</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {#each sessions as s (s.id)}
              <tr>
                <td>{fmtDateTime(s.created_at)}</td>
                <td>{fmtDateTime(s.last_seen_at)}</td>
                <td class="hint">{s.user_agent?.slice(0, 60) ?? '—'}</td>
                <td>
                  <div class="row">
                    {#if s.current}<span class="badge teal">当前会话</span>{/if}
                    <button class="btn small danger" disabled={s.current && sessions.length === 1} onclick={() => void revoke(s)}>
                      {s.current ? '注销当前' : '撤销'}
                    </button>
                  </div>
                </td>
              </tr>
            {/each}
          </tbody>
        </table>
      {/if}
      <Status busy={false} error={null} empty={sessMsg} />
      <p class="hint">更改密码或被管理员停用时,其他会话将自动失效。</p>
    </section>
  </div>
</Page>