feat: WhatsApp Nucleo con Nuxt 4 + Baileys v7
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 6m46s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 6m46s
Reemplazo completo de Evolution API por implementación directa con Baileys. Características: - Dashboard completo con Nuxt UI v4 - Soporte para múltiples instancias de WhatsApp - Conexión via QR code o pairing code - Persistencia de mensajes en PostgreSQL - API REST para integraciones externas - Webhooks con firma HMAC - SSE para actualizaciones en tiempo real - Autenticación con Authentik
This commit is contained in:
68
app/composables/useAuthentik.ts
Normal file
68
app/composables/useAuthentik.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Composable para leer información de usuario de Authentik
|
||||
* Los headers son inyectados por Authentik Proxy Outpost
|
||||
*/
|
||||
|
||||
interface AuthentikUser {
|
||||
username: string
|
||||
email: string | undefined
|
||||
name: string | undefined
|
||||
groups: string[]
|
||||
uid: string | undefined
|
||||
avatar: string
|
||||
}
|
||||
|
||||
export const useAuthentik = () => {
|
||||
const authentikUser = useState<AuthentikUser | null>('authentikUser', () => {
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders()
|
||||
|
||||
const username = headers['x-authentik-username']
|
||||
const email = headers['x-authentik-email']
|
||||
const name = headers['x-authentik-name']
|
||||
const groups = headers['x-authentik-groups']
|
||||
const uid = headers['x-authentik-uid']
|
||||
|
||||
if (!username) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
email,
|
||||
name,
|
||||
groups: groups ? groups.split('|').filter(g => g.trim()) : [],
|
||||
uid,
|
||||
avatar: `https://ui-avatars.com/api/?name=${encodeURIComponent(name || username)}&background=075e54&color=fff&size=128`
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const user = computed(() => authentikUser.value)
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
const logout = () => {
|
||||
const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com'
|
||||
navigateTo(`${authentikUrl}/flows/-/default/invalidation/`, { external: true })
|
||||
}
|
||||
|
||||
const goToProfile = () => {
|
||||
const authentikUrl = useRuntimeConfig().public.authentikUrl || 'https://authentik.nucleoriofrio.com'
|
||||
navigateTo(`${authentikUrl}/if/user/`, { external: true, open: { target: '_blank' } })
|
||||
}
|
||||
|
||||
const hasGroup = (groupName: string): boolean => {
|
||||
if (!user.value) return false
|
||||
return user.value.groups.includes(groupName)
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isAuthenticated,
|
||||
logout,
|
||||
goToProfile,
|
||||
hasGroup
|
||||
}
|
||||
}
|
||||
124
app/composables/useInstances.ts
Normal file
124
app/composables/useInstances.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Composable for managing WhatsApp instances
|
||||
*/
|
||||
|
||||
export interface Instance {
|
||||
id: string
|
||||
name: string
|
||||
phoneNumber: string | null
|
||||
status: 'disconnected' | 'connecting' | 'connected' | 'qr_ready' | 'pairing'
|
||||
qrCode: string | null
|
||||
pairingCode: string | null
|
||||
lastConnectedAt: Date | null
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export const useInstances = () => {
|
||||
const instances = useState<Instance[]>('instances', () => [])
|
||||
const loading = useState('instancesLoading', () => false)
|
||||
const error = useState<string | null>('instancesError', () => null)
|
||||
|
||||
const fetchInstances = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const data = await $fetch<Instance[]>('/api/instances')
|
||||
instances.value = data
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message
|
||||
console.error('Error fetching instances:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const createInstance = async (name: string): Promise<Instance | null> => {
|
||||
try {
|
||||
const instance = await $fetch<Instance>('/api/instances', {
|
||||
method: 'POST',
|
||||
body: { name }
|
||||
})
|
||||
instances.value.push(instance)
|
||||
return instance
|
||||
} catch (e) {
|
||||
console.error('Error creating instance:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const deleteInstance = async (id: string) => {
|
||||
try {
|
||||
await $fetch(`/api/instances/${id}`, { method: 'DELETE' })
|
||||
instances.value = instances.value.filter(i => i.id !== id)
|
||||
} catch (e) {
|
||||
console.error('Error deleting instance:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const connectInstance = async (id: string): Promise<{ qrCode: string | null; status: string }> => {
|
||||
const result = await $fetch<{ qrCode: string | null; status: string }>(`/api/instances/${id}/connect`, {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
// Update local state
|
||||
const idx = instances.value.findIndex(i => i.id === id)
|
||||
if (idx !== -1) {
|
||||
instances.value[idx].status = result.status as Instance['status']
|
||||
instances.value[idx].qrCode = result.qrCode
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const disconnectInstance = async (id: string) => {
|
||||
await $fetch(`/api/instances/${id}/disconnect`, { method: 'POST' })
|
||||
|
||||
// Update local state
|
||||
const idx = instances.value.findIndex(i => i.id === id)
|
||||
if (idx !== -1) {
|
||||
instances.value[idx].status = 'disconnected'
|
||||
instances.value[idx].qrCode = null
|
||||
}
|
||||
}
|
||||
|
||||
const getQRCode = async (id: string): Promise<string | null> => {
|
||||
const result = await $fetch<{ qrCode: string | null }>(`/api/instances/${id}/qr`)
|
||||
return result.qrCode
|
||||
}
|
||||
|
||||
const requestPairingCode = async (id: string, phoneNumber: string): Promise<string> => {
|
||||
const result = await $fetch<{ code: string }>(`/api/instances/${id}/pairing-code`, {
|
||||
method: 'POST',
|
||||
body: { phoneNumber }
|
||||
})
|
||||
return result.code
|
||||
}
|
||||
|
||||
const getInstanceStatus = async (id: string) => {
|
||||
return await $fetch(`/api/instances/${id}/status`)
|
||||
}
|
||||
|
||||
// Computed helpers
|
||||
const connectedCount = computed(() =>
|
||||
instances.value.filter(i => i.status === 'connected').length
|
||||
)
|
||||
|
||||
const totalCount = computed(() => instances.value.length)
|
||||
|
||||
return {
|
||||
instances,
|
||||
loading,
|
||||
error,
|
||||
fetchInstances,
|
||||
createInstance,
|
||||
deleteInstance,
|
||||
connectInstance,
|
||||
disconnectInstance,
|
||||
getQRCode,
|
||||
requestPairingCode,
|
||||
getInstanceStatus,
|
||||
connectedCount,
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
22
app/composables/useSidebarState.ts
Normal file
22
app/composables/useSidebarState.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Composable para gestionar el estado del sidebar
|
||||
*/
|
||||
export const useSidebarState = () => {
|
||||
const isOpen = useState('sidebarOpen', () => true)
|
||||
const isCollapsed = useState('sidebarCollapsed', () => false)
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const collapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isCollapsed,
|
||||
toggle,
|
||||
collapse
|
||||
}
|
||||
}
|
||||
4
app/composables/useToast.ts
Normal file
4
app/composables/useToast.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Re-export useToast from Nuxt UI
|
||||
*/
|
||||
export { useToast } from '#imports'
|
||||
Reference in New Issue
Block a user