feat: initial NFC Attendance Hub implementation
- 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
This commit is contained in:
58
lib/semaphore.ts
Normal file
58
lib/semaphore.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user