feat(desktop): remember login credentials
Desktop Client Build / Resolve Build Metadata (push) Successful in 49s
Frontend CI / Frontend (push) Successful in 5m47s
Desktop Client Build / Build Desktop Client (push) Successful in 42m28s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 2m38s
Desktop Client Build / Resolve Build Metadata (push) Successful in 49s
Frontend CI / Frontend (push) Successful in 5m47s
Desktop Client Build / Build Desktop Client (push) Successful in 42m28s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 2m38s
This commit is contained in:
@@ -37,6 +37,11 @@ import {
|
|||||||
registerObservedRequestRendererTarget,
|
registerObservedRequestRendererTarget,
|
||||||
} from './network-observer'
|
} from './network-observer'
|
||||||
import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from './playwright-cdp'
|
import { getPlaywrightCDPPort, startHiddenPlaywrightReaper } from './playwright-cdp'
|
||||||
|
import {
|
||||||
|
clearSavedLoginCredentials,
|
||||||
|
getSavedLoginCredentials,
|
||||||
|
saveLoginCredentials,
|
||||||
|
} from './login-credentials'
|
||||||
import { initProcessMetricsSampler } from './process-metrics'
|
import { initProcessMetricsSampler } from './process-metrics'
|
||||||
import { registerRendererDevtoolsProxyTarget } from './renderer-devtools-proxy'
|
import { registerRendererDevtoolsProxyTarget } from './renderer-devtools-proxy'
|
||||||
import {
|
import {
|
||||||
@@ -925,6 +930,16 @@ function registerBridgeHandlers(): void {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
ipcMain.handle('desktop:get-app-settings', () => getDesktopAppSettings())
|
ipcMain.handle('desktop:get-app-settings', () => getDesktopAppSettings())
|
||||||
|
ipcMain.handle('desktop:get-login-credentials', () => getSavedLoginCredentials())
|
||||||
|
safeHandle(
|
||||||
|
'desktop:save-login-credentials',
|
||||||
|
(_event, credentials: { identifier?: unknown; password?: unknown }) =>
|
||||||
|
saveLoginCredentials({
|
||||||
|
identifier: typeof credentials?.identifier === 'string' ? credentials.identifier : '',
|
||||||
|
password: typeof credentials?.password === 'string' ? credentials.password : '',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
ipcMain.handle('desktop:clear-login-credentials', () => clearSavedLoginCredentials())
|
||||||
safeHandle(
|
safeHandle(
|
||||||
'desktop:set-app-setting',
|
'desktop:set-app-setting',
|
||||||
async (_event, key: DesktopAppSettingKey, value: boolean) => {
|
async (_event, key: DesktopAppSettingKey, value: boolean) => {
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
import { app, safeStorage } from 'electron/main'
|
||||||
|
|
||||||
|
export interface DesktopLoginCredentials {
|
||||||
|
identifier: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PersistedLoginCredentials {
|
||||||
|
version?: number
|
||||||
|
identifier?: unknown
|
||||||
|
encryptedPassword?: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
function credentialsPath(): string {
|
||||||
|
return join(app.getPath('userData'), 'login-credentials.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCredentials(input: unknown): PersistedLoginCredentials {
|
||||||
|
if (!input || typeof input !== 'object') {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return input as PersistedLoginCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSavedLoginCredentials(): DesktopLoginCredentials | null {
|
||||||
|
try {
|
||||||
|
if (!existsSync(credentialsPath())) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = normalizeCredentials(JSON.parse(readFileSync(credentialsPath(), 'utf8')))
|
||||||
|
const identifier = typeof persisted.identifier === 'string' ? persisted.identifier : ''
|
||||||
|
const encryptedPassword =
|
||||||
|
typeof persisted.encryptedPassword === 'string' ? persisted.encryptedPassword : ''
|
||||||
|
|
||||||
|
if (!identifier || !encryptedPassword || !safeStorage.isEncryptionAvailable()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = safeStorage.decryptString(Buffer.from(encryptedPassword, 'base64'))
|
||||||
|
return password ? { identifier, password } : null
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[login-credentials] read failed', {
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveLoginCredentials(credentials: DesktopLoginCredentials): DesktopLoginCredentials {
|
||||||
|
const identifier = credentials.identifier.trim()
|
||||||
|
if (!identifier || !credentials.password) {
|
||||||
|
clearSavedLoginCredentials()
|
||||||
|
return { identifier: '', password: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) {
|
||||||
|
throw new Error('login_credentials_encryption_unavailable')
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedPassword = safeStorage.encryptString(credentials.password).toString('base64')
|
||||||
|
writeFileSync(
|
||||||
|
credentialsPath(),
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
identifier,
|
||||||
|
encryptedPassword,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
return { identifier, password: credentials.password }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSavedLoginCredentials(): null {
|
||||||
|
try {
|
||||||
|
rmSync(credentialsPath(), { force: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[login-credentials] clear failed', {
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -33,6 +33,11 @@ interface DesktopAppSettings {
|
|||||||
keepRunningInBackground: boolean
|
keepRunningInBackground: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopLoginCredentials {
|
||||||
|
identifier: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
const rendererProxyRequestChannel = 'desktop:renderer-devtools-proxy:request'
|
const rendererProxyRequestChannel = 'desktop:renderer-devtools-proxy:request'
|
||||||
const rendererProxyResponseChannel = 'desktop:renderer-devtools-proxy:response'
|
const rendererProxyResponseChannel = 'desktop:renderer-devtools-proxy:response'
|
||||||
|
|
||||||
@@ -137,6 +142,14 @@ const desktopBridge = {
|
|||||||
openSettingsWindow: () => ipcRenderer.invoke('desktop:open-settings-window') as Promise<null>,
|
openSettingsWindow: () => ipcRenderer.invoke('desktop:open-settings-window') as Promise<null>,
|
||||||
getAppSettings: () =>
|
getAppSettings: () =>
|
||||||
ipcRenderer.invoke('desktop:get-app-settings') as Promise<DesktopAppSettings>,
|
ipcRenderer.invoke('desktop:get-app-settings') as Promise<DesktopAppSettings>,
|
||||||
|
getLoginCredentials: () =>
|
||||||
|
ipcRenderer.invoke('desktop:get-login-credentials') as Promise<DesktopLoginCredentials | null>,
|
||||||
|
saveLoginCredentials: (credentials: DesktopLoginCredentials) =>
|
||||||
|
ipcRenderer.invoke('desktop:save-login-credentials', credentials) as Promise<
|
||||||
|
DesktopLoginCredentials
|
||||||
|
>,
|
||||||
|
clearLoginCredentials: () =>
|
||||||
|
ipcRenderer.invoke('desktop:clear-login-credentials') as Promise<null>,
|
||||||
setAppSetting: (key: keyof DesktopAppSettings, value: boolean) =>
|
setAppSetting: (key: keyof DesktopAppSettings, value: boolean) =>
|
||||||
ipcRenderer.invoke('desktop:set-app-setting', key, value) as Promise<DesktopAppSettings>,
|
ipcRenderer.invoke('desktop:set-app-setting', key, value) as Promise<DesktopAppSettings>,
|
||||||
syncRuntimeSession: (session: DesktopRuntimeSessionSyncRequest | null) =>
|
syncRuntimeSession: (session: DesktopRuntimeSessionSyncRequest | null) =>
|
||||||
|
|||||||
+10
@@ -17,6 +17,11 @@ declare global {
|
|||||||
keepRunningInBackground: boolean
|
keepRunningInBackground: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopLoginCredentials {
|
||||||
|
identifier: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
desktopBridge: {
|
desktopBridge: {
|
||||||
app: {
|
app: {
|
||||||
@@ -54,6 +59,11 @@ declare global {
|
|||||||
openExternalUrl(url: string): Promise<null>
|
openExternalUrl(url: string): Promise<null>
|
||||||
openSettingsWindow(): Promise<null>
|
openSettingsWindow(): Promise<null>
|
||||||
getAppSettings(): Promise<DesktopAppSettings>
|
getAppSettings(): Promise<DesktopAppSettings>
|
||||||
|
getLoginCredentials(): Promise<DesktopLoginCredentials | null>
|
||||||
|
saveLoginCredentials(
|
||||||
|
credentials: DesktopLoginCredentials,
|
||||||
|
): Promise<DesktopLoginCredentials>
|
||||||
|
clearLoginCredentials(): Promise<null>
|
||||||
setAppSetting(key: keyof DesktopAppSettings, value: boolean): Promise<DesktopAppSettings>
|
setAppSetting(key: keyof DesktopAppSettings, value: boolean): Promise<DesktopAppSettings>
|
||||||
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>
|
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>
|
||||||
rotateRuntimeClientToken(): Promise<DesktopClientRotateResponse>
|
rotateRuntimeClientToken(): Promise<DesktopClientRotateResponse>
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession()
|
|||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
const showServerDialog = ref(false)
|
const showServerDialog = ref(false)
|
||||||
const showPassword = ref(false)
|
const showPassword = ref(false)
|
||||||
const rememberAccount = ref(!!localStorage.getItem('geo_rankly_saved_identifier'))
|
const legacySavedIdentifier =
|
||||||
const savedIdentifier =
|
|
||||||
localStorage.getItem('geo_rankly_saved_identifier') ??
|
localStorage.getItem('geo_rankly_saved_identifier') ??
|
||||||
localStorage.getItem('geo_rankly_saved_email') ??
|
localStorage.getItem('geo_rankly_saved_email') ??
|
||||||
''
|
''
|
||||||
|
const rememberCredentials = ref(!!legacySavedIdentifier)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
apiBaseURL: apiBaseURL.value,
|
apiBaseURL: apiBaseURL.value,
|
||||||
identifier: savedIdentifier,
|
identifier: legacySavedIdentifier,
|
||||||
password: '',
|
password: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -25,8 +25,11 @@ function toggleMenu() {
|
|||||||
menuOpen.value = !menuOpen.value
|
menuOpen.value = !menuOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRememberChange(event: Event) {
|
async function onRememberChange(event: Event) {
|
||||||
rememberAccount.value = (event.target as HTMLInputElement).checked
|
rememberCredentials.value = (event.target as HTMLInputElement).checked
|
||||||
|
if (!rememberCredentials.value) {
|
||||||
|
await clearSavedCredentials()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOutsideClick(event: MouseEvent) {
|
function handleOutsideClick(event: MouseEvent) {
|
||||||
@@ -53,22 +56,58 @@ function saveServerSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitLogin() {
|
async function submitLogin() {
|
||||||
localStorage.removeItem('geo_rankly_saved_password')
|
|
||||||
if (rememberAccount.value) {
|
|
||||||
localStorage.setItem('geo_rankly_saved_identifier', form.identifier)
|
|
||||||
localStorage.removeItem('geo_rankly_saved_email')
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('geo_rankly_saved_identifier')
|
|
||||||
localStorage.removeItem('geo_rankly_saved_email')
|
|
||||||
}
|
|
||||||
|
|
||||||
await login({
|
await login({
|
||||||
identifier: form.identifier,
|
identifier: form.identifier,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
})
|
})
|
||||||
|
await persistCredentialsPreference()
|
||||||
await window.desktopBridge?.app?.setWindowMode?.('main', { source: 'login' })
|
await window.desktopBridge?.app?.setWindowMode?.('main', { source: 'login' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hydrateSavedCredentials() {
|
||||||
|
try {
|
||||||
|
const saved = await window.desktopBridge?.app?.getLoginCredentials?.()
|
||||||
|
if (!saved) return
|
||||||
|
form.identifier = saved.identifier
|
||||||
|
form.password = saved.password
|
||||||
|
rememberCredentials.value = true
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[login-view] load saved credentials failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearSavedCredentials() {
|
||||||
|
localStorage.removeItem('geo_rankly_saved_identifier')
|
||||||
|
localStorage.removeItem('geo_rankly_saved_email')
|
||||||
|
localStorage.removeItem('geo_rankly_saved_password')
|
||||||
|
try {
|
||||||
|
await window.desktopBridge?.app?.clearLoginCredentials?.()
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[login-view] clear saved credentials failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistCredentialsPreference() {
|
||||||
|
if (!rememberCredentials.value) {
|
||||||
|
await clearSavedCredentials()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier = form.identifier.trim()
|
||||||
|
localStorage.setItem('geo_rankly_saved_identifier', identifier)
|
||||||
|
localStorage.removeItem('geo_rankly_saved_email')
|
||||||
|
localStorage.removeItem('geo_rankly_saved_password')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.desktopBridge?.app?.saveLoginCredentials?.({
|
||||||
|
identifier,
|
||||||
|
password: form.password,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[login-view] save credentials failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const lockCountdown = ref(0)
|
const lockCountdown = ref(0)
|
||||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
@@ -103,6 +142,7 @@ const displayError = computed(() => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('mousedown', handleOutsideClick)
|
document.addEventListener('mousedown', handleOutsideClick)
|
||||||
|
void hydrateSavedCredentials()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -178,8 +218,8 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<div class="options-row">
|
<div class="options-row">
|
||||||
<label class="check-label">
|
<label class="check-label">
|
||||||
<input type="checkbox" :checked="rememberAccount" @change="onRememberChange" />
|
<input type="checkbox" :checked="rememberCredentials" @change="onRememberChange" />
|
||||||
<span>记住账号</span>
|
<span>记住账号和密码</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user