Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
268
marketing-agent/frontend/src/stores/plugins/persistedState.js
Normal file
268
marketing-agent/frontend/src/stores/plugins/persistedState.js
Normal file
@@ -0,0 +1,268 @@
|
||||
// Pinia plugin for persisted state with performance optimizations
|
||||
|
||||
import { debounce } from '@/utils/performance'
|
||||
|
||||
const STORAGE_KEY_PREFIX = 'marketing_agent_'
|
||||
|
||||
/**
|
||||
* Create persisted state plugin for Pinia
|
||||
*/
|
||||
export function createPersistedState(options = {}) {
|
||||
const {
|
||||
storage = localStorage,
|
||||
paths = [],
|
||||
debounceTime = 1000,
|
||||
serializer = JSON,
|
||||
filter = () => true
|
||||
} = options
|
||||
|
||||
return (context) => {
|
||||
const { store } = context
|
||||
const storeId = store.$id
|
||||
const storageKey = STORAGE_KEY_PREFIX + storeId
|
||||
|
||||
// Load initial state
|
||||
try {
|
||||
const savedState = storage.getItem(storageKey)
|
||||
if (savedState) {
|
||||
const parsed = serializer.parse(savedState)
|
||||
// Only restore specified paths or all if paths is empty
|
||||
if (paths.length === 0) {
|
||||
store.$patch(parsed)
|
||||
} else {
|
||||
const filtered = {}
|
||||
paths.forEach(path => {
|
||||
const keys = path.split('.')
|
||||
let source = parsed
|
||||
let target = filtered
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
if (source && key in source) {
|
||||
target[key] = source[key]
|
||||
}
|
||||
} else {
|
||||
if (!target[key]) target[key] = {}
|
||||
target = target[key]
|
||||
source = source ? source[key] : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
store.$patch(filtered)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load persisted state for ${storeId}:`, error)
|
||||
}
|
||||
|
||||
// Save state changes with debouncing
|
||||
const saveState = debounce(() => {
|
||||
try {
|
||||
const state = store.$state
|
||||
let toSave = state
|
||||
|
||||
// Filter state if paths are specified
|
||||
if (paths.length > 0) {
|
||||
toSave = {}
|
||||
paths.forEach(path => {
|
||||
const keys = path.split('.')
|
||||
let source = state
|
||||
let target = toSave
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (index === keys.length - 1) {
|
||||
if (source && key in source) {
|
||||
target[key] = source[key]
|
||||
}
|
||||
} else {
|
||||
if (!target[key]) target[key] = {}
|
||||
target = target[key]
|
||||
source = source ? source[key] : undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Apply custom filter
|
||||
if (filter(toSave)) {
|
||||
storage.setItem(storageKey, serializer.stringify(toSave))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to save state for ${storeId}:`, error)
|
||||
}
|
||||
}, debounceTime)
|
||||
|
||||
// Subscribe to state changes
|
||||
store.$subscribe((mutation, state) => {
|
||||
saveState()
|
||||
})
|
||||
|
||||
// Subscribe to actions for immediate save on specific actions
|
||||
store.$onAction(({ name, after }) => {
|
||||
// Save immediately after logout or critical actions
|
||||
if (['logout', 'clearData', 'resetStore'].includes(name)) {
|
||||
after(() => {
|
||||
saveState.cancel()
|
||||
try {
|
||||
if (name === 'logout' || name === 'clearData') {
|
||||
storage.removeItem(storageKey)
|
||||
} else {
|
||||
storage.setItem(storageKey, serializer.stringify(store.$state))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to handle action ${name}:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secure storage wrapper with encryption
|
||||
*/
|
||||
export class SecureStorage {
|
||||
constructor(secretKey) {
|
||||
this.secretKey = secretKey
|
||||
}
|
||||
|
||||
async encrypt(data) {
|
||||
const encoder = new TextEncoder()
|
||||
const dataBuffer = encoder.encode(JSON.stringify(data))
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(this.secretKey),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['encrypt']
|
||||
)
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12))
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
dataBuffer
|
||||
)
|
||||
|
||||
const combined = new Uint8Array(iv.length + encrypted.byteLength)
|
||||
combined.set(iv)
|
||||
combined.set(new Uint8Array(encrypted), iv.length)
|
||||
|
||||
return btoa(String.fromCharCode(...combined))
|
||||
}
|
||||
|
||||
async decrypt(encryptedData) {
|
||||
const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0))
|
||||
const iv = combined.slice(0, 12)
|
||||
const encrypted = combined.slice(12)
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(this.secretKey),
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['decrypt']
|
||||
)
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
key,
|
||||
encrypted
|
||||
)
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
return JSON.parse(decoder.decode(decrypted))
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
return this.encrypt(value).then(encrypted => {
|
||||
localStorage.setItem(key, encrypted)
|
||||
})
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
const encrypted = localStorage.getItem(key)
|
||||
if (!encrypted) return Promise.resolve(null)
|
||||
|
||||
return this.decrypt(encrypted).catch(error => {
|
||||
console.error('Decryption failed:', error)
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IndexedDB storage for large data
|
||||
*/
|
||||
export class IndexedDBStorage {
|
||||
constructor(dbName = 'marketing_agent_store', storeName = 'state') {
|
||||
this.dbName = dbName
|
||||
this.storeName = storeName
|
||||
this.db = null
|
||||
}
|
||||
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async setItem(key, value) {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite')
|
||||
const store = transaction.objectStore(this.storeName)
|
||||
const request = store.put(value, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async getItem(key) {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly')
|
||||
const store = transaction.objectStore(this.storeName)
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async removeItem(key) {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite')
|
||||
const store = transaction.objectStore(this.storeName)
|
||||
const request = store.delete(key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user