mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
修复朋友圈封面信息被错误解析的问题;解决了一些安全问题
This commit is contained in:
@@ -1034,6 +1034,11 @@ function registerIpcHandlers() {
|
|||||||
return windowsHelloService.verify(message, targetWin)
|
return windowsHelloService.verify(message, targetWin)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 验证应用锁状态(带签名校验,防篡改)
|
||||||
|
ipcMain.handle('auth:verifyEnabled', async () => {
|
||||||
|
return configService?.verifyAuthEnabled() ?? false
|
||||||
|
})
|
||||||
|
|
||||||
// 导出相关
|
// 导出相关
|
||||||
ipcMain.handle('export:getExportStats', async (_, sessionIds: string[], options: any) => {
|
ipcMain.handle('export:getExportStats', async (_, sessionIds: string[], options: any) => {
|
||||||
return exportService.getExportStats(sessionIds, options)
|
return exportService.getExportStats(sessionIds, options)
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
|
|
||||||
// 认证
|
// 认证
|
||||||
auth: {
|
auth: {
|
||||||
hello: (message?: string) => ipcRenderer.invoke('auth:hello', message)
|
hello: (message?: string) => ipcRenderer.invoke('auth:hello', message),
|
||||||
|
verifyEnabled: () => ipcRenderer.invoke('auth:verifyEnabled')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { app } from 'electron'
|
import { app, safeStorage } from 'electron'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
|
|
||||||
|
// safeStorage 加密后的前缀标记,用于区分明文和密文
|
||||||
|
const SAFE_PREFIX = 'safe:'
|
||||||
|
|
||||||
interface ConfigSchema {
|
interface ConfigSchema {
|
||||||
// 数据库相关
|
// 数据库相关
|
||||||
dbPath: string // 数据库根目录 (xwechat_files)
|
dbPath: string // 数据库根目录 (xwechat_files)
|
||||||
@@ -32,7 +35,7 @@ interface ConfigSchema {
|
|||||||
exportDefaultConcurrency: number
|
exportDefaultConcurrency: number
|
||||||
analyticsExcludedUsernames: string[]
|
analyticsExcludedUsernames: string[]
|
||||||
|
|
||||||
// 安全相关
|
// 安全相关(通过 safeStorage 加密存储,JSON 中为密文)
|
||||||
authEnabled: boolean
|
authEnabled: boolean
|
||||||
authPassword: string // SHA-256 hash
|
authPassword: string // SHA-256 hash
|
||||||
authUseHello: boolean
|
authUseHello: boolean
|
||||||
@@ -48,6 +51,11 @@ interface ConfigSchema {
|
|||||||
wordCloudExcludeWords: string[]
|
wordCloudExcludeWords: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 需要 safeStorage 加密的字段集合
|
||||||
|
const ENCRYPTED_STRING_KEYS: Set<string> = new Set(['decryptKey', 'imageAesKey', 'authPassword'])
|
||||||
|
const ENCRYPTED_BOOL_KEYS: Set<string> = new Set(['authEnabled', 'authUseHello'])
|
||||||
|
const ENCRYPTED_NUMBER_KEYS: Set<string> = new Set(['imageXorKey'])
|
||||||
|
|
||||||
export class ConfigService {
|
export class ConfigService {
|
||||||
private static instance: ConfigService
|
private static instance: ConfigService
|
||||||
private store!: Store<ConfigSchema>
|
private store!: Store<ConfigSchema>
|
||||||
@@ -103,15 +111,220 @@ export class ConfigService {
|
|||||||
wordCloudExcludeWords: []
|
wordCloudExcludeWords: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 首次启动时迁移旧版明文安全字段
|
||||||
|
this.migrateAuthFields()
|
||||||
}
|
}
|
||||||
|
|
||||||
get<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] {
|
get<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] {
|
||||||
return this.store.get(key)
|
const raw = this.store.get(key)
|
||||||
|
|
||||||
|
// 布尔型加密字段:存储为加密字符串,读取时解密还原为布尔值
|
||||||
|
if (ENCRYPTED_BOOL_KEYS.has(key)) {
|
||||||
|
const str = typeof raw === 'string' ? raw : ''
|
||||||
|
if (!str || !str.startsWith(SAFE_PREFIX)) return raw
|
||||||
|
const decrypted = this.safeDecrypt(str)
|
||||||
|
return (decrypted === 'true') as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数字型加密字段:存储为加密字符串,读取时解密还原为数字
|
||||||
|
if (ENCRYPTED_NUMBER_KEYS.has(key)) {
|
||||||
|
const str = typeof raw === 'string' ? raw : ''
|
||||||
|
if (!str || !str.startsWith(SAFE_PREFIX)) return raw
|
||||||
|
const decrypted = this.safeDecrypt(str)
|
||||||
|
const num = Number(decrypted)
|
||||||
|
return (Number.isFinite(num) ? num : 0) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字符串型加密字段
|
||||||
|
if (ENCRYPTED_STRING_KEYS.has(key) && typeof raw === 'string') {
|
||||||
|
return this.safeDecrypt(raw) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
// wxidConfigs 中嵌套的敏感字段
|
||||||
|
if (key === 'wxidConfigs' && raw && typeof raw === 'object') {
|
||||||
|
return this.decryptWxidConfigs(raw as any) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw
|
||||||
}
|
}
|
||||||
|
|
||||||
set<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void {
|
set<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void {
|
||||||
this.store.set(key, value)
|
let toStore = value
|
||||||
|
|
||||||
|
// 布尔型加密字段:序列化为字符串后加密
|
||||||
|
if (ENCRYPTED_BOOL_KEYS.has(key)) {
|
||||||
|
toStore = this.safeEncrypt(String(value)) as ConfigSchema[K]
|
||||||
}
|
}
|
||||||
|
// 数字型加密字段:序列化为字符串后加密
|
||||||
|
else if (ENCRYPTED_NUMBER_KEYS.has(key)) {
|
||||||
|
toStore = this.safeEncrypt(String(value)) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
// 字符串型加密字段
|
||||||
|
else if (ENCRYPTED_STRING_KEYS.has(key) && typeof value === 'string') {
|
||||||
|
toStore = this.safeEncrypt(value) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
// wxidConfigs 中嵌套的敏感字段
|
||||||
|
else if (key === 'wxidConfigs' && value && typeof value === 'object') {
|
||||||
|
toStore = this.encryptWxidConfigs(value as any) as ConfigSchema[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.set(key, toStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === safeStorage 加解密 ===
|
||||||
|
|
||||||
|
private safeEncrypt(plaintext: string): string {
|
||||||
|
if (!plaintext) return ''
|
||||||
|
if (plaintext.startsWith(SAFE_PREFIX)) return plaintext
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) return plaintext
|
||||||
|
const encrypted = safeStorage.encryptString(plaintext)
|
||||||
|
return SAFE_PREFIX + encrypted.toString('base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
private safeDecrypt(stored: string): string {
|
||||||
|
if (!stored) return ''
|
||||||
|
if (!stored.startsWith(SAFE_PREFIX)) {
|
||||||
|
return stored
|
||||||
|
}
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) return ''
|
||||||
|
try {
|
||||||
|
const buf = Buffer.from(stored.slice(SAFE_PREFIX.length), 'base64')
|
||||||
|
return safeStorage.decryptString(buf)
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 旧版本迁移 ===
|
||||||
|
|
||||||
|
// 将旧版明文 auth 字段迁移为 safeStorage 加密格式
|
||||||
|
private migrateAuthFields(): void {
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) return
|
||||||
|
|
||||||
|
// 迁移字符串型字段(decryptKey, imageAesKey, authPassword)
|
||||||
|
for (const key of ENCRYPTED_STRING_KEYS) {
|
||||||
|
const raw = this.store.get(key as keyof ConfigSchema)
|
||||||
|
if (typeof raw === 'string' && raw && !raw.startsWith(SAFE_PREFIX)) {
|
||||||
|
this.store.set(key as any, this.safeEncrypt(raw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移布尔型字段(authEnabled, authUseHello)
|
||||||
|
for (const key of ENCRYPTED_BOOL_KEYS) {
|
||||||
|
const raw = this.store.get(key as keyof ConfigSchema)
|
||||||
|
// 如果是原始布尔值(未加密),转为加密字符串
|
||||||
|
if (typeof raw === 'boolean') {
|
||||||
|
this.store.set(key as any, this.safeEncrypt(String(raw)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移数字型字段(imageXorKey)
|
||||||
|
for (const key of ENCRYPTED_NUMBER_KEYS) {
|
||||||
|
const raw = this.store.get(key as keyof ConfigSchema)
|
||||||
|
// 如果是原始数字值(未加密),转为加密字符串
|
||||||
|
if (typeof raw === 'number') {
|
||||||
|
this.store.set(key as any, this.safeEncrypt(String(raw)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迁移 wxidConfigs 中的嵌套敏感字段
|
||||||
|
const wxidConfigs = this.store.get('wxidConfigs')
|
||||||
|
if (wxidConfigs && typeof wxidConfigs === 'object') {
|
||||||
|
let needsUpdate = false
|
||||||
|
const updated = { ...wxidConfigs }
|
||||||
|
for (const [wxid, cfg] of Object.entries(updated)) {
|
||||||
|
if (cfg.decryptKey && !cfg.decryptKey.startsWith(SAFE_PREFIX)) {
|
||||||
|
updated[wxid] = { ...cfg, decryptKey: this.safeEncrypt(cfg.decryptKey) }
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
if (cfg.imageAesKey && !cfg.imageAesKey.startsWith(SAFE_PREFIX)) {
|
||||||
|
updated[wxid] = { ...updated[wxid], imageAesKey: this.safeEncrypt(cfg.imageAesKey) }
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
if (cfg.imageXorKey !== undefined && typeof cfg.imageXorKey === 'number') {
|
||||||
|
updated[wxid] = { ...updated[wxid], imageXorKey: this.safeEncrypt(String(cfg.imageXorKey)) as any }
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needsUpdate) {
|
||||||
|
this.store.set('wxidConfigs', updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理旧版 authSignature 字段(不再需要)
|
||||||
|
this.store.delete('authSignature' as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// === wxidConfigs 加解密 ===
|
||||||
|
|
||||||
|
private encryptWxidConfigs(configs: ConfigSchema['wxidConfigs']): ConfigSchema['wxidConfigs'] {
|
||||||
|
const result: ConfigSchema['wxidConfigs'] = {}
|
||||||
|
for (const [wxid, cfg] of Object.entries(configs)) {
|
||||||
|
result[wxid] = { ...cfg }
|
||||||
|
if (cfg.decryptKey) result[wxid].decryptKey = this.safeEncrypt(cfg.decryptKey)
|
||||||
|
if (cfg.imageAesKey) result[wxid].imageAesKey = this.safeEncrypt(cfg.imageAesKey)
|
||||||
|
if (cfg.imageXorKey !== undefined) {
|
||||||
|
(result[wxid] as any).imageXorKey = this.safeEncrypt(String(cfg.imageXorKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private decryptWxidConfigs(configs: ConfigSchema['wxidConfigs']): ConfigSchema['wxidConfigs'] {
|
||||||
|
const result: ConfigSchema['wxidConfigs'] = {}
|
||||||
|
for (const [wxid, cfg] of Object.entries(configs)) {
|
||||||
|
result[wxid] = { ...cfg }
|
||||||
|
if (cfg.decryptKey) result[wxid].decryptKey = this.safeDecrypt(cfg.decryptKey)
|
||||||
|
if (cfg.imageAesKey) result[wxid].imageAesKey = this.safeDecrypt(cfg.imageAesKey)
|
||||||
|
if (cfg.imageXorKey !== undefined) {
|
||||||
|
const raw = cfg.imageXorKey as any
|
||||||
|
if (typeof raw === 'string' && raw.startsWith(SAFE_PREFIX)) {
|
||||||
|
const decrypted = this.safeDecrypt(raw)
|
||||||
|
const num = Number(decrypted)
|
||||||
|
result[wxid].imageXorKey = Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 应用锁验证 ===
|
||||||
|
|
||||||
|
// 验证应用锁状态,防篡改:
|
||||||
|
// - 所有 auth 字段都是 safeStorage 密文,删除/修改密文 → 解密失败
|
||||||
|
// - 解密失败时,检查 authPassword 密文是否曾经存在(非空非默认值)
|
||||||
|
// 如果存在则说明被篡改,强制锁定
|
||||||
|
verifyAuthEnabled(): boolean {
|
||||||
|
// 用 as any 绕过泛型推断,因为加密后实际存储的是字符串而非 boolean
|
||||||
|
const rawEnabled: any = this.store.get('authEnabled')
|
||||||
|
const rawPassword: any = this.store.get('authPassword')
|
||||||
|
|
||||||
|
// 情况1:字段是加密密文,正常解密
|
||||||
|
if (typeof rawEnabled === 'string' && rawEnabled.startsWith(SAFE_PREFIX)) {
|
||||||
|
const enabled = this.safeDecrypt(rawEnabled) === 'true'
|
||||||
|
const password = typeof rawPassword === 'string' ? this.safeDecrypt(rawPassword) : ''
|
||||||
|
|
||||||
|
if (!enabled && !password) return false
|
||||||
|
return enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况2:字段是原始布尔值(旧版本,尚未迁移)
|
||||||
|
if (typeof rawEnabled === 'boolean') {
|
||||||
|
return rawEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// 情况3:字段被删除(electron-store 返回默认值 false)或被篡改为无法解密的值
|
||||||
|
// 检查 authPassword 是否有密文残留(说明之前设置过密码)
|
||||||
|
if (typeof rawPassword === 'string' && rawPassword.startsWith(SAFE_PREFIX)) {
|
||||||
|
// 密码密文还在,说明之前启用过应用锁,字段被篡改了 → 强制锁定
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 其他 ===
|
||||||
|
|
||||||
getCacheBasePath(): string {
|
getCacheBasePath(): string {
|
||||||
const configured = this.get('cachePath')
|
const configured = this.get('cachePath')
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ function App() {
|
|||||||
const checkLock = async () => {
|
const checkLock = async () => {
|
||||||
// 并行获取配置,减少等待
|
// 并行获取配置,减少等待
|
||||||
const [enabled, useHello] = await Promise.all([
|
const [enabled, useHello] = await Promise.all([
|
||||||
configService.getAuthEnabled(),
|
window.electronAPI.auth.verifyEnabled(),
|
||||||
configService.getAuthUseHello()
|
configService.getAuthUseHello()
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -105,9 +105,19 @@ export default function LockScreen({ onUnlock, avatar, useHello = false }: LockS
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const storedHash = await configService.getAuthPassword()
|
const storedHash = await configService.getAuthPassword()
|
||||||
|
|
||||||
|
// 兜底:如果没有设置过密码,直接放行并关闭应用锁
|
||||||
|
if (!storedHash) {
|
||||||
|
await configService.setAuthEnabled(false)
|
||||||
|
handleUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const inputHash = await sha256(password)
|
const inputHash = await sha256(password)
|
||||||
|
|
||||||
if (inputHash === storedHash) {
|
if (inputHash === storedHash) {
|
||||||
|
// 解锁成功,重新写入 authEnabled 以修复可能被篡改的签名
|
||||||
|
await configService.setAuthEnabled(true)
|
||||||
handleUnlock()
|
handleUnlock()
|
||||||
} else {
|
} else {
|
||||||
setError('密码错误')
|
setError('密码错误')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { NavLink, useLocation } from 'react-router-dom'
|
import { NavLink, useLocation } from 'react-router-dom'
|
||||||
import { Home, MessageSquare, BarChart3, Users, FileText, Database, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock } from 'lucide-react'
|
import { Home, MessageSquare, BarChart3, Users, FileText, Database, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock } from 'lucide-react'
|
||||||
import { useAppStore } from '../stores/appStore'
|
import { useAppStore } from '../stores/appStore'
|
||||||
import * as configService from '../services/config'
|
|
||||||
import './Sidebar.scss'
|
import './Sidebar.scss'
|
||||||
|
|
||||||
function Sidebar() {
|
function Sidebar() {
|
||||||
@@ -12,7 +12,7 @@ function Sidebar() {
|
|||||||
const setLocked = useAppStore(state => state.setLocked)
|
const setLocked = useAppStore(state => state.setLocked)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
configService.getAuthEnabled().then(setAuthEnabled)
|
window.electronAPI.auth.verifyEnabled().then(setAuthEnabled)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const isActive = (path: string) => {
|
const isActive = (path: string) => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface SnsMedia {
|
|||||||
|
|
||||||
interface SnsMediaGridProps {
|
interface SnsMediaGridProps {
|
||||||
mediaList: SnsMedia[]
|
mediaList: SnsMedia[]
|
||||||
|
postType?: number
|
||||||
onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void
|
onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void
|
||||||
onMediaDeleted?: () => void
|
onMediaDeleted?: () => void
|
||||||
}
|
}
|
||||||
@@ -80,7 +81,7 @@ const extractVideoFrame = async (videoPath: string): Promise<string> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void; onMediaDeleted?: () => void }) => {
|
const MediaItem = ({ media, postType, onPreview, onMediaDeleted }: { media: SnsMedia; postType?: number; onPreview: (src: string, isVideo?: boolean, liveVideoPath?: string) => void; onMediaDeleted?: () => void }) => {
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const [deleted, setDeleted] = useState(false)
|
const [deleted, setDeleted] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@@ -96,6 +97,8 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
const isVideo = isSnsVideoUrl(media.url)
|
const isVideo = isSnsVideoUrl(media.url)
|
||||||
const isLive = !!media.livePhoto
|
const isLive = !!media.livePhoto
|
||||||
const targetUrl = media.thumb || media.url
|
const targetUrl = media.thumb || media.url
|
||||||
|
// type 7 的朋友圈媒体不需要解密,直接使用原始 URL
|
||||||
|
const skipDecrypt = postType === 7
|
||||||
|
|
||||||
// 视频重试:失败时重试最多2次,耗尽才标记删除
|
// 视频重试:失败时重试最多2次,耗尽才标记删除
|
||||||
const videoRetryOrDelete = () => {
|
const videoRetryOrDelete = () => {
|
||||||
@@ -119,7 +122,7 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
// For images, we proxy to get the local path/base64
|
// For images, we proxy to get the local path/base64
|
||||||
const result = await window.electronAPI.sns.proxyImage({
|
const result = await window.electronAPI.sns.proxyImage({
|
||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
key: media.key
|
key: skipDecrypt ? undefined : media.key
|
||||||
})
|
})
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
if (isLive && media.livePhoto?.url) {
|
if (isLive && media.livePhoto?.url) {
|
||||||
window.electronAPI.sns.proxyImage({
|
window.electronAPI.sns.proxyImage({
|
||||||
url: media.livePhoto.url,
|
url: media.livePhoto.url,
|
||||||
key: media.livePhoto.key || media.key
|
key: skipDecrypt ? undefined : (media.livePhoto.key || media.key)
|
||||||
}).then((res: any) => {
|
}).then((res: any) => {
|
||||||
if (!cancelled && res.success && res.videoPath) {
|
if (!cancelled && res.success && res.videoPath) {
|
||||||
setLiveVideoPath(`file://${res.videoPath.replace(/\\/g, '/')}`)
|
setLiveVideoPath(`file://${res.videoPath.replace(/\\/g, '/')}`)
|
||||||
@@ -150,7 +153,7 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
// Usually we need to call proxyImage with the video URL to decrypt it to cache
|
// Usually we need to call proxyImage with the video URL to decrypt it to cache
|
||||||
const result = await window.electronAPI.sns.proxyImage({
|
const result = await window.electronAPI.sns.proxyImage({
|
||||||
url: media.url,
|
url: media.url,
|
||||||
key: media.key
|
key: skipDecrypt ? undefined : media.key
|
||||||
})
|
})
|
||||||
|
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
@@ -201,7 +204,7 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
try {
|
try {
|
||||||
const res = await window.electronAPI.sns.proxyImage({
|
const res = await window.electronAPI.sns.proxyImage({
|
||||||
url: media.url,
|
url: media.url,
|
||||||
key: media.key
|
key: skipDecrypt ? undefined : media.key
|
||||||
})
|
})
|
||||||
if (res.success && res.videoPath) {
|
if (res.success && res.videoPath) {
|
||||||
const local = `file://${res.videoPath.replace(/\\/g, '/')}`
|
const local = `file://${res.videoPath.replace(/\\/g, '/')}`
|
||||||
@@ -229,7 +232,7 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.sns.proxyImage({
|
const result = await window.electronAPI.sns.proxyImage({
|
||||||
url: media.url,
|
url: media.url,
|
||||||
key: media.key
|
key: skipDecrypt ? undefined : media.key
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -334,7 +337,7 @@ const MediaItem = ({ media, onPreview, onMediaDeleted }: { media: SnsMedia; onPr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SnsMediaGrid: React.FC<SnsMediaGridProps> = ({ mediaList, onPreview, onMediaDeleted }) => {
|
export const SnsMediaGrid: React.FC<SnsMediaGridProps> = ({ mediaList, postType, onPreview, onMediaDeleted }) => {
|
||||||
if (!mediaList || mediaList.length === 0) return null
|
if (!mediaList || mediaList.length === 0) return null
|
||||||
|
|
||||||
const count = mediaList.length
|
const count = mediaList.length
|
||||||
@@ -350,7 +353,7 @@ export const SnsMediaGrid: React.FC<SnsMediaGridProps> = ({ mediaList, onPreview
|
|||||||
return (
|
return (
|
||||||
<div className={`sns-media-grid ${gridClass}`}>
|
<div className={`sns-media-grid ${gridClass}`}>
|
||||||
{mediaList.map((media, idx) => (
|
{mediaList.map((media, idx) => (
|
||||||
<MediaItem key={idx} media={media} onPreview={onPreview} onMediaDeleted={onMediaDeleted} />
|
<MediaItem key={idx} media={media} postType={postType} onPreview={onPreview} onMediaDeleted={onMediaDeleted} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ export const SnsPostItem: React.FC<SnsPostItemProps> = ({ post, onPreview, onDeb
|
|||||||
|
|
||||||
{showMediaGrid && (
|
{showMediaGrid && (
|
||||||
<div className="post-media-container">
|
<div className="post-media-container">
|
||||||
<SnsMediaGrid mediaList={post.media} onPreview={onPreview} onMediaDeleted={[1, 54].includes(post.type ?? 0) ? () => setMediaDeleted(true) : undefined} />
|
<SnsMediaGrid mediaList={post.media} postType={post.type} onPreview={onPreview} onMediaDeleted={[1, 54].includes(post.type ?? 0) ? () => setMediaDeleted(true) : undefined} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ function SettingsPage() {
|
|||||||
const savedNotificationFilterMode = await configService.getNotificationFilterMode()
|
const savedNotificationFilterMode = await configService.getNotificationFilterMode()
|
||||||
const savedNotificationFilterList = await configService.getNotificationFilterList()
|
const savedNotificationFilterList = await configService.getNotificationFilterList()
|
||||||
|
|
||||||
const savedAuthEnabled = await configService.getAuthEnabled()
|
const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled()
|
||||||
const savedAuthUseHello = await configService.getAuthUseHello()
|
const savedAuthUseHello = await configService.getAuthUseHello()
|
||||||
setAuthEnabled(savedAuthEnabled)
|
setAuthEnabled(savedAuthEnabled)
|
||||||
setAuthUseHello(savedAuthUseHello)
|
setAuthUseHello(savedAuthUseHello)
|
||||||
@@ -2046,6 +2046,14 @@ function SettingsPage() {
|
|||||||
checked={authEnabled}
|
checked={authEnabled}
|
||||||
onChange={async (e) => {
|
onChange={async (e) => {
|
||||||
const enabled = e.target.checked
|
const enabled = e.target.checked
|
||||||
|
if (enabled) {
|
||||||
|
// 检查是否已设置密码,未设置则阻止开启
|
||||||
|
const storedHash = await configService.getAuthPassword()
|
||||||
|
if (!storedHash) {
|
||||||
|
showMessage('请先设置密码再启用应用锁', false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
setAuthEnabled(enabled)
|
setAuthEnabled(enabled)
|
||||||
await configService.setAuthEnabled(enabled)
|
await configService.setAuthEnabled(enabled)
|
||||||
}}
|
}}
|
||||||
|
|||||||
4
src/types/electron.d.ts
vendored
4
src/types/electron.d.ts
vendored
@@ -19,6 +19,10 @@ export interface ElectronAPI {
|
|||||||
set: (key: string, value: unknown) => Promise<void>
|
set: (key: string, value: unknown) => Promise<void>
|
||||||
clear: () => Promise<boolean>
|
clear: () => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
auth: {
|
||||||
|
hello: (message?: string) => Promise<{ success: boolean; error?: string }>
|
||||||
|
verifyEnabled: () => Promise<boolean>
|
||||||
|
}
|
||||||
dialog: {
|
dialog: {
|
||||||
openFile: (options?: Electron.OpenDialogOptions) => Promise<Electron.OpenDialogReturnValue>
|
openFile: (options?: Electron.OpenDialogOptions) => Promise<Electron.OpenDialogReturnValue>
|
||||||
openDirectory: (options?: Electron.OpenDialogOptions) => Promise<Electron.OpenDialogReturnValue>
|
openDirectory: (options?: Electron.OpenDialogOptions) => Promise<Electron.OpenDialogReturnValue>
|
||||||
|
|||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -5,6 +5,7 @@ interface Window {
|
|||||||
// ... other methods ...
|
// ... other methods ...
|
||||||
auth: {
|
auth: {
|
||||||
hello: (message?: string) => Promise<{ success: boolean; error?: string }>
|
hello: (message?: string) => Promise<{ success: boolean; error?: string }>
|
||||||
|
verifyEnabled: () => Promise<boolean>
|
||||||
}
|
}
|
||||||
// For brevity, using 'any' for other parts or properly importing types if available.
|
// For brevity, using 'any' for other parts or properly importing types if available.
|
||||||
// In a real scenario, you'd likely want to keep the full interface definition consistent with preload.ts
|
// In a real scenario, you'd likely want to keep the full interface definition consistent with preload.ts
|
||||||
|
|||||||
Reference in New Issue
Block a user