- Multi-tenant client management with unique API keys - Semaphore SMS integration (per-client key + sender name + credit balance) - DB-based SMS queue with 3-attempt retry and exponential backoff - Failed SMS dashboard with manual retry button - Reports page with date/client filter and CSV export - Admin auth via NextAuth (email/password) - Docker Compose setup (app + PostgreSQL 16) - Prisma 5 schema with SmsQueue, SmsLog, Client, User models Tech: Next.js 14 App Router + TypeScript + Tailwind CSS + Prisma + PostgreSQL
59 lines
1.4 KiB
TypeScript
59 lines
1.4 KiB
TypeScript
import axios from 'axios'
|
|
|
|
const SEMAPHORE_API_URL = 'https://api.semaphore.co/api/v4'
|
|
|
|
export interface SemaphoreResponse {
|
|
success: boolean
|
|
messageId?: string
|
|
error?: string
|
|
}
|
|
|
|
export async function sendSms(
|
|
apiKey: string,
|
|
senderName: string,
|
|
phone: string,
|
|
message: string
|
|
): Promise<SemaphoreResponse> {
|
|
try {
|
|
const response = await axios.post(
|
|
`${SEMAPHORE_API_URL}/messages`,
|
|
{
|
|
apikey: apiKey,
|
|
number: phone,
|
|
message,
|
|
sendername: senderName,
|
|
},
|
|
{ timeout: 15000 }
|
|
)
|
|
|
|
const data = response.data
|
|
if (Array.isArray(data) && data.length > 0) {
|
|
const msg = data[0]
|
|
if (msg.status === 'Queued' || msg.status === 'Sent') {
|
|
return { success: true, messageId: String(msg.message_id) }
|
|
}
|
|
return { success: false, error: msg.status || 'Unknown status' }
|
|
}
|
|
return { success: false, error: 'Empty response from Semaphore' }
|
|
} catch (err: any) {
|
|
const errorMsg =
|
|
err.response?.data?.message ||
|
|
err.response?.data ||
|
|
err.message ||
|
|
'SMS send failed'
|
|
return { success: false, error: String(errorMsg) }
|
|
}
|
|
}
|
|
|
|
export async function getBalance(apiKey: string): Promise<number | null> {
|
|
try {
|
|
const response = await axios.get(`${SEMAPHORE_API_URL}/account`, {
|
|
params: { apikey: apiKey },
|
|
timeout: 10000,
|
|
})
|
|
return response.data?.credit_balance ?? null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|