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:
249
marketing-agent/frontend/src/stores/abTesting.js
Normal file
249
marketing-agent/frontend/src/stores/abTesting.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
export const useABTestingStore = defineStore('abTesting', () => {
|
||||
// State
|
||||
const experiments = ref([])
|
||||
const currentExperiment = ref(null)
|
||||
const experimentStatus = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// Actions
|
||||
const fetchExperiments = async (params = {}) => {
|
||||
try {
|
||||
const response = await api.get('/api/experiments', { params })
|
||||
experiments.value = response.data.experiments
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiments:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchExperiment = async (experimentId) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await api.get(`/api/experiments/${experimentId}`)
|
||||
currentExperiment.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiment:', error)
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createExperiment = async (experimentData) => {
|
||||
try {
|
||||
const response = await api.post('/api/experiments', experimentData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to create experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const updateExperiment = async (experimentId, updates) => {
|
||||
try {
|
||||
const response = await api.put(`/api/experiments/${experimentId}`, updates)
|
||||
currentExperiment.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to update experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const deleteExperiment = async (experimentId) => {
|
||||
try {
|
||||
await api.delete(`/api/experiments/${experimentId}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to delete experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const startExperiment = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.post(`/api/experiments/${experimentId}/start`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to start experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const stopExperiment = async (experimentId, reason) => {
|
||||
try {
|
||||
const response = await api.post(`/api/experiments/${experimentId}/stop`, { reason })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to stop experiment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchExperimentStatus = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/experiments/${experimentId}/status`)
|
||||
experimentStatus.value = response.data
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiment status:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Tracking APIs
|
||||
const allocateUser = async (experimentId, userId, userContext = {}) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/allocate', {
|
||||
experimentId,
|
||||
userId,
|
||||
userContext
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to allocate user:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const recordConversion = async (experimentId, userId, value = 1, metadata = {}) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/convert', {
|
||||
experimentId,
|
||||
userId,
|
||||
value,
|
||||
metadata
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to record conversion:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const batchAllocate = async (experimentId, users) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/batch/allocate', {
|
||||
experimentId,
|
||||
users
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to batch allocate users:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const batchRecordConversions = async (experimentId, conversions) => {
|
||||
try {
|
||||
const response = await api.post('/api/tracking/batch/convert', {
|
||||
experimentId,
|
||||
conversions
|
||||
})
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to batch record conversions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Results APIs
|
||||
const fetchExperimentResults = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch experiment results:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchVariantDetails = async (experimentId, variantId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}/variants/${variantId}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch variant details:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSegmentAnalysis = async (experimentId) => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}/segments`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch segment analysis:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const exportResults = async (experimentId, format = 'csv') => {
|
||||
try {
|
||||
const response = await api.get(`/api/results/${experimentId}/export`, {
|
||||
params: { format },
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', `experiment-${experimentId}-results.${format}`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to export results:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Reset store
|
||||
const reset = () => {
|
||||
experiments.value = []
|
||||
currentExperiment.value = null
|
||||
experimentStatus.value = null
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
experiments,
|
||||
currentExperiment,
|
||||
experimentStatus,
|
||||
loading,
|
||||
|
||||
// Actions
|
||||
fetchExperiments,
|
||||
fetchExperiment,
|
||||
createExperiment,
|
||||
updateExperiment,
|
||||
deleteExperiment,
|
||||
startExperiment,
|
||||
stopExperiment,
|
||||
fetchExperimentStatus,
|
||||
|
||||
// Tracking
|
||||
allocateUser,
|
||||
recordConversion,
|
||||
batchAllocate,
|
||||
batchRecordConversions,
|
||||
|
||||
// Results
|
||||
fetchExperimentResults,
|
||||
fetchVariantDetails,
|
||||
fetchSegmentAnalysis,
|
||||
exportResults,
|
||||
|
||||
// Utils
|
||||
reset
|
||||
}
|
||||
})
|
||||
102
marketing-agent/frontend/src/stores/auth.js
Normal file
102
marketing-agent/frontend/src/stores/auth.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import api from '@/api'
|
||||
import router from '@/router'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const loading = ref(false)
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
|
||||
async function login(credentials) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.auth.login(credentials)
|
||||
const { data } = response
|
||||
|
||||
token.value = data.accessToken
|
||||
user.value = data.user
|
||||
localStorage.setItem('token', data.accessToken)
|
||||
localStorage.setItem('refreshToken', data.refreshToken)
|
||||
api.setAuthToken(data.accessToken)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.error || 'Login failed'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function register(userData) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.auth.register(userData)
|
||||
const { token: newToken, user: newUser } = response.data
|
||||
|
||||
token.value = newToken
|
||||
user.value = newUser
|
||||
localStorage.setItem('token', newToken)
|
||||
api.setAuthToken(newToken)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.error || 'Registration failed'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await api.auth.logout()
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
} finally {
|
||||
token.value = ''
|
||||
user.value = null
|
||||
localStorage.removeItem('token')
|
||||
api.setAuthToken('')
|
||||
router.push({ name: 'Login' })
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchProfile() {
|
||||
if (!token.value) return
|
||||
|
||||
try {
|
||||
const response = await api.auth.getProfile()
|
||||
user.value = response.data.user
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
if (error.response?.status === 401) {
|
||||
await logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize auth token
|
||||
if (token.value) {
|
||||
api.setAuthToken(token.value)
|
||||
fetchProfile()
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
loading,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
fetchProfile
|
||||
}
|
||||
})
|
||||
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