feat(sync): 首次登录后固定随机初始凭据
This commit is contained in:
@@ -11,6 +11,11 @@ const username = ref('')
|
||||
const password = ref('')
|
||||
const deviceName = ref('OpenNexus Web Console')
|
||||
const sessionLabel = ref('')
|
||||
const credentialsRequired = ref(false)
|
||||
const currentPassword = ref('')
|
||||
const newUsername = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const newVaultName = ref('')
|
||||
const vaults = ref<Vault[]>([])
|
||||
const devices = ref<Device[]>([])
|
||||
@@ -72,6 +77,10 @@ function leaveConsole() {
|
||||
password.value = ''
|
||||
vaults.value = []
|
||||
devices.value = []
|
||||
credentialsRequired.value = false
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
}
|
||||
|
||||
async function signIn() {
|
||||
@@ -82,17 +91,41 @@ async function signIn() {
|
||||
const device = deviceName.value.trim()
|
||||
password.value = ''
|
||||
try {
|
||||
await api.login(account, secret, device)
|
||||
credentialsRequired.value = await api.login(account, secret, device)
|
||||
sessionLabel.value = `${account} · ${device}`
|
||||
newUsername.value = account
|
||||
signedIn.value = true
|
||||
await loadAccount()
|
||||
notify('设备会话已建立')
|
||||
if (!credentialsRequired.value) await loadAccount()
|
||||
notify(credentialsRequired.value ? '请立即固定账户与密码' : '设备会话已建立')
|
||||
} catch (error) {
|
||||
leaveConsole()
|
||||
notify(error instanceof Error ? error.message : 'LOGIN_FAILED', true)
|
||||
} finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function fixCredentials() {
|
||||
if (busy.value) return
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
notify('两次输入的新密码不一致', true)
|
||||
return
|
||||
}
|
||||
busy.value = true
|
||||
const oldSecret = currentPassword.value
|
||||
const nextSecret = newPassword.value
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
try {
|
||||
const result = await api.changeCredentials(oldSecret, newUsername.value.trim(), nextSecret)
|
||||
credentialsRequired.value = false
|
||||
sessionLabel.value = `${result.username} · ${deviceName.value.trim()}`
|
||||
await loadAccount()
|
||||
notify('账户与密码已固定;以后重启不会再随机更换')
|
||||
} catch (error) {
|
||||
notify(error instanceof Error ? error.message : 'CREDENTIAL_CHANGE_FAILED', true)
|
||||
} finally { busy.value = false }
|
||||
}
|
||||
|
||||
async function createVault() {
|
||||
const name = newVaultName.value.trim()
|
||||
if (!name || busy.value) return
|
||||
@@ -198,6 +231,22 @@ onBeforeUnmount(() => {
|
||||
<button class="secondary-button" type="button" :disabled="busy" @click="logout">退出登录</button>
|
||||
</div>
|
||||
|
||||
<section v-if="credentialsRequired" class="login-card credential-card" aria-labelledby="credential-title">
|
||||
<div class="card-heading">
|
||||
<span class="lock-mark" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="11" rx="3" /><path d="M8 10V7a4 4 0 0 1 8 0v3" /></svg></span>
|
||||
<div><p>首次登录</p><h2 id="credential-title">固定账户凭据</h2></div>
|
||||
</div>
|
||||
<p class="surface-intro">当前密码只对本次服务启动有效。修改账户和密码后,凭据将写入数据库并在后续重启中保持不变。</p>
|
||||
<form @submit.prevent="fixCredentials">
|
||||
<label>当前随机密码<input v-model="currentPassword" type="password" autocomplete="current-password" minlength="12" maxlength="256" required></label>
|
||||
<label>新账户<input v-model="newUsername" autocomplete="username" maxlength="80" required></label>
|
||||
<label>新密码<input v-model="newPassword" type="password" autocomplete="new-password" minlength="12" maxlength="256" required></label>
|
||||
<label>确认新密码<input v-model="confirmPassword" type="password" autocomplete="new-password" minlength="12" maxlength="256" required></label>
|
||||
<button class="primary-button" type="submit" :disabled="busy || !currentPassword || !newPassword || !confirmPassword">{{ busy ? '正在保存…' : '保存并固定凭据' }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<template v-else>
|
||||
<div class="metric-grid">
|
||||
<article><span>远端 Vault</span><strong>{{ vaults.length }}</strong><small>当前账户可访问</small></article>
|
||||
<article><span>已使用空间</span><strong>{{ formatBytes(used) }}</strong><small>总配额 {{ formatBytes(quota) }}</small></article>
|
||||
@@ -242,6 +291,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Session {
|
||||
refresh_token: string
|
||||
expires_in: number
|
||||
device_id: string
|
||||
must_change_credentials: boolean
|
||||
}
|
||||
|
||||
export interface Vault {
|
||||
@@ -39,6 +40,7 @@ export class SyncApi {
|
||||
private access = ''
|
||||
private refresh = ''
|
||||
deviceId = ''
|
||||
mustChangeCredentials = false
|
||||
|
||||
get signedIn() { return Boolean(this.access) }
|
||||
|
||||
@@ -80,6 +82,7 @@ export class SyncApi {
|
||||
this.access = session.access_token
|
||||
this.refresh = session.refresh_token
|
||||
this.deviceId = session.device_id
|
||||
this.mustChangeCredentials = Boolean(session.must_change_credentials)
|
||||
}
|
||||
|
||||
async status(): Promise<ServiceStatus> {
|
||||
@@ -103,7 +106,7 @@ export class SyncApi {
|
||||
}
|
||||
}
|
||||
|
||||
async login(username: string, password: string, deviceName: string): Promise<void> {
|
||||
async login(username: string, password: string, deviceName: string): Promise<boolean> {
|
||||
const response = await this.raw('/sync/v1/auth/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password, device_name: deviceName }),
|
||||
@@ -115,6 +118,17 @@ export class SyncApi {
|
||||
const session = await safeJson<Session>(response)
|
||||
if (!session) throw new Error('INVALID_RESPONSE')
|
||||
this.accept(session)
|
||||
return this.mustChangeCredentials
|
||||
}
|
||||
|
||||
async changeCredentials(currentPassword: string, username: string, password: string) {
|
||||
const result = await this.request<{ username: string; credentials_fixed: boolean }>(
|
||||
'/sync/v1/account/credentials', {
|
||||
method: 'PUT', body: JSON.stringify({ current_password: currentPassword, username, password }),
|
||||
}, false,
|
||||
)
|
||||
this.mustChangeCredentials = false
|
||||
return result
|
||||
}
|
||||
|
||||
vaults() { return this.request<{ items: Vault[] }>('/sync/v1/vaults') }
|
||||
@@ -136,5 +150,6 @@ export class SyncApi {
|
||||
this.access = ''
|
||||
this.refresh = ''
|
||||
this.deviceId = ''
|
||||
this.mustChangeCredentials = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ main { width: min(1180px, calc(100% - 48px)); margin: 0 auto; position: relative
|
||||
.protocol-grid strong { font-size: 18px; font-weight: 600; }
|
||||
.protocol-grid span { color: var(--muted); font-size: 11px; }
|
||||
.login-card { padding: 31px; border: 1px solid var(--line); border-radius: 18px; background: var(--surface); box-shadow: var(--shadow); position: relative; overflow: hidden; }
|
||||
.credential-card { width: min(100%, 560px); margin: 32px auto 0; }
|
||||
.card-glow { position: absolute; width: 210px; height: 210px; right: -100px; top: -120px; border-radius: 50%; background: var(--accent); filter: blur(60px); opacity: .08; }
|
||||
.card-heading { display: flex; gap: 14px; align-items: center; margin-bottom: 28px; position: relative; }
|
||||
.card-heading p, .surface-heading p { margin: 0 0 3px; color: var(--accent); font-size: 10px; font-weight: 700; letter-spacing: .13em; text-transform: uppercase; }
|
||||
|
||||
Reference in New Issue
Block a user