Files
Emailsorter/client/src/lib/api.ts
ANDJ cbb225c001 feat: Gitea Webhook, IMAP, Settings & Deployment docs
- Webhook route and Gitea integration
- IMAP service and Nextcloud/Porkbun setup docs
- Settings UI improvements and API updates
- SSH/Webhook fix prompt for emailsorter.webklar.com
- Bootstrap, config and AI sorter updates
2026-01-31 15:00:00 +01:00

590 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const API_BASE = import.meta.env.VITE_API_URL || '/api'
interface ApiResponse<T> {
success?: boolean
data?: T
error?: {
code: string
message: string
fields?: Record<string, string[]>
limit?: number
used?: number
}
}
async function fetchApi<T>(
endpoint: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
const data = await response.json()
if (!response.ok || data.success === false) {
return {
error: data.error || {
code: 'UNKNOWN',
message: `HTTP ${response.status}`
}
}
}
return { success: true, data: data.data || data }
} catch (error) {
return {
error: {
code: 'NETWORK_ERROR',
message: error instanceof Error ? error.message : 'Network error'
}
}
}
}
export const api = {
// ═══════════════════════════════════════════════════════════════════════════
// EMAIL ACCOUNTS
// ═══════════════════════════════════════════════════════════════════════════
async getEmailAccounts(userId: string) {
return fetchApi<Array<{
id: string
email: string
provider: 'gmail' | 'outlook' | 'imap'
connected: boolean
lastSync?: string
}>>(`/email/accounts?userId=${userId}`)
},
async connectEmailAccount(userId: string, provider: 'gmail' | 'outlook', email: string, accessToken: string, refreshToken?: string) {
return fetchApi<{ accountId: string }>('/email/connect', {
method: 'POST',
body: JSON.stringify({ userId, provider, email, accessToken, refreshToken }),
})
},
async connectImapAccount(
userId: string,
params: { email: string; password: string; imapHost?: string; imapPort?: number; imapSecure?: boolean }
) {
return fetchApi<{ accountId: string }>('/email/connect', {
method: 'POST',
body: JSON.stringify({
userId,
provider: 'imap',
email: params.email,
accessToken: params.password,
imapHost: params.imapHost,
imapPort: params.imapPort,
imapSecure: params.imapSecure,
}),
})
},
async disconnectEmailAccount(accountId: string, userId: string) {
return fetchApi<{ success: boolean }>(`/email/accounts/${accountId}?userId=${userId}`, {
method: 'DELETE',
})
},
// ═══════════════════════════════════════════════════════════════════════════
// EMAIL STATS & SORTING
// ═══════════════════════════════════════════════════════════════════════════
async getEmailStats(userId: string) {
return fetchApi<{
totalSorted: number
todaySorted: number
weekSorted: number
categories: Record<string, number>
timeSaved: number
}>(`/email/stats?userId=${userId}`)
},
async sortEmails(userId: string, accountId: string, maxEmails?: number, processAll?: boolean) {
return fetchApi<{
sorted: number
inboxCleared: number
categories: Record<string, number>
timeSaved: { minutes: number; formatted: string }
highlights: Array<{ type: string; count: number; message: string }>
suggestions: Array<{ type: string; message: string }>
provider?: string
isDemo?: boolean
isFirstRun?: boolean
suggestedRules?: Array<{
type: string
name: string
description: string
confidence: number
action?: { name?: string }
}>
}>('/email/sort', {
method: 'POST',
body: JSON.stringify({ userId, accountId, maxEmails, processAll }),
})
},
// Demo sorting without account (for quick tests)
async sortDemo(count: number = 10) {
return fetchApi<{
sorted: number
emails: Array<{
from: string
subject: string
snippet: string
category: string
categoryName: string
confidence?: number
reason?: string
}>
categories: Record<string, number>
aiEnabled: boolean
}>('/email/sort-demo', {
method: 'POST',
body: JSON.stringify({ count }),
})
},
// Connect demo account
async connectDemoAccount(userId: string) {
return fetchApi<{
accountId: string
email: string
provider: string
message?: string
}>('/email/connect-demo', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
// Get categories
async getCategories() {
return fetchApi<Array<{
id: string
name: string
description: string
color: string
action: string
priority: number
}>>('/email/categories')
},
// Get today's digest
async getDigest(userId: string) {
return fetchApi<{
date: string
totalSorted: number
inboxCleared: number
timeSavedMinutes: number
stats: Record<string, number>
highlights: Array<{ type: string; count: number; message: string }>
suggestions: Array<{ type: string; message: string }>
hasData: boolean
}>(`/email/digest?userId=${userId}`)
},
// Get digest history
async getDigestHistory(userId: string, days: number = 7) {
return fetchApi<{
days: number
digests: Array<{
date: string
totalSorted: number
inboxCleared: number
timeSavedMinutes: number
stats: Record<string, number>
}>
totals: {
totalSorted: number
inboxCleared: number
timeSavedMinutes: number
}
}>(`/email/digest/history?userId=${userId}&days=${days}`)
},
// ═══════════════════════════════════════════════════════════════════════════
// OAUTH
// ═══════════════════════════════════════════════════════════════════════════
async getOAuthUrl(provider: 'gmail' | 'outlook', userId: string) {
return fetchApi<{ url: string }>(`/oauth/${provider}/connect?userId=${userId}`)
},
async getOAuthStatus() {
return fetchApi<{
gmail: { enabled: boolean; scopes: string[] }
outlook: { enabled: boolean; scopes: string[] }
}>('/oauth/status')
},
// ═══════════════════════════════════════════════════════════════════════════
// SUBSCRIPTION
// ═══════════════════════════════════════════════════════════════════════════
async getSubscriptionStatus(userId: string) {
return fetchApi<{
status: string
plan: string
isFreeTier: boolean
emailsUsedThisMonth?: number
emailsLimit?: number
features: {
emailAccounts: number
emailsPerDay: number
historicalSync: boolean
customRules: boolean
prioritySupport: boolean
}
currentPeriodEnd?: string
cancelAtPeriodEnd?: boolean
}>(`/subscription/status?userId=${userId}`)
},
async createSubscriptionCheckout(plan: string, userId: string, email?: string) {
return fetchApi<{ url: string; sessionId: string }>('/subscription/checkout', {
method: 'POST',
body: JSON.stringify({ userId, plan, email }),
})
},
async createPortalSession(userId: string) {
return fetchApi<{ url: string }>('/subscription/portal', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
async cancelSubscription(userId: string) {
return fetchApi<{ success: boolean }>('/subscription/cancel', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
async reactivateSubscription(userId: string) {
return fetchApi<{ success: boolean }>('/subscription/reactivate', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
// ═══════════════════════════════════════════════════════════════════════════
// USER PREFERENCES
// ═══════════════════════════════════════════════════════════════════════════
async getUserPreferences(userId: string) {
return fetchApi<{
vipSenders: Array<{ email: string; name?: string }>
blockedSenders: string[]
customRules: Array<{ condition: string; category: string }>
priorityTopics: string[]
}>(`/preferences?userId=${userId}`)
},
async saveUserPreferences(userId: string, preferences: {
vipSenders?: Array<{ email: string; name?: string }>
blockedSenders?: string[]
customRules?: Array<{ condition: string; category: string }>
priorityTopics?: string[]
companyLabels?: Array<{ name: string; condition?: string; category: string; enabled: boolean }>
}) {
return fetchApi<{ success: boolean }>('/preferences', {
method: 'POST',
body: JSON.stringify({ userId, ...preferences }),
})
},
// ═══════════════════════════════════════════════════════════════════════════
// AI CONTROL
// ═══════════════════════════════════════════════════════════════════════════
async getAIControlSettings(userId: string) {
return fetchApi<{
enabledCategories: string[]
categoryActions: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies: boolean
cleanup?: unknown
categoryAdvanced?: Record<string, unknown>
version?: number
}>(`/preferences/ai-control?userId=${userId}`)
},
async saveAIControlSettings(userId: string, settings: {
enabledCategories?: string[]
categoryActions?: Record<string, 'inbox' | 'archive_read' | 'star'>
autoDetectCompanies?: boolean
cleanup?: unknown
categoryAdvanced?: Record<string, unknown>
version?: number
}) {
return fetchApi<{ success: boolean }>('/preferences/ai-control', {
method: 'POST',
body: JSON.stringify({ userId, ...settings }),
})
},
// Cleanup Preview - shows what would be cleaned up without actually doing it
// TODO: Backend endpoint needs to be implemented
// GET /api/preferences/ai-control/cleanup/preview?userId=xxx
// Response: { preview: Array<{id, subject, from, date, reason}> }
async getCleanupPreview(userId: string) {
// TODO: Implement backend endpoint
return fetchApi<{
preview: Array<{
id: string
subject: string
from: string
date: string
reason: 'read' | 'promotion'
}>
}>(`/preferences/ai-control/cleanup/preview?userId=${userId}`)
},
// Run cleanup now - executes cleanup for the user
// POST /api/preferences/ai-control/cleanup/run
// Body: { userId: string }
// Response: { success: boolean, data: { readItems: number, promotions: number } }
async runCleanup(userId: string) {
// Uses existing /api/email/cleanup endpoint
return fetchApi<{
usersProcessed: number
emailsProcessed: {
readItems: number
promotions: number
}
errors: Array<{ userId: string; error: string }>
}>('/email/cleanup', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
// Get cleanup status - last run info and counts
// TODO: Backend endpoint needs to be implemented
// GET /api/preferences/ai-control/cleanup/status?userId=xxx
// Response: { lastRun?: string, lastRunCounts?: { readItems: number, promotions: number } }
async getCleanupStatus(userId: string) {
// TODO: Implement backend endpoint
return fetchApi<{
lastRun?: string
lastRunCounts?: {
readItems: number
promotions: number
}
}>(`/preferences/ai-control/cleanup/status?userId=${userId}`)
},
// ═══════════════════════════════════════════════════════════════════════════
// COMPANY LABELS
// ═══════════════════════════════════════════════════════════════════════════
async getCompanyLabels(userId: string) {
return fetchApi<Array<{
id?: string
name: string
condition: string
enabled: boolean
category?: string
}>>(`/preferences/company-labels?userId=${userId}`)
},
async saveCompanyLabel(userId: string, companyLabel: {
id?: string
name: string
condition: string
enabled: boolean
category?: string
}) {
return fetchApi<{
id?: string
name: string
condition: string
enabled: boolean
category?: string
}>('/preferences/company-labels', {
method: 'POST',
body: JSON.stringify({ userId, companyLabel }),
})
},
async deleteCompanyLabel(userId: string, labelId: string) {
return fetchApi<{ success: boolean }>(`/preferences/company-labels/${labelId}?userId=${userId}`, {
method: 'DELETE',
})
},
// ═══════════════════════════════════════════════════════════════════════════
// ME / ADMIN
// ═══════════════════════════════════════════════════════════════════════════
async getMe(email: string) {
return fetchApi<{ isAdmin: boolean }>(`/me?email=${encodeURIComponent(email)}`)
},
// ═══════════════════════════════════════════════════════════════════════════
// NAME LABELS (Workers Admin only)
// ═══════════════════════════════════════════════════════════════════════════
async getNameLabels(userId: string, email: string) {
return fetchApi<Array<{
id?: string
name: string
email?: string
keywords?: string[]
enabled: boolean
}>>(`/preferences/name-labels?userId=${userId}&email=${encodeURIComponent(email)}`)
},
async saveNameLabel(
userId: string,
userEmail: string,
nameLabel: { id?: string; name: string; email?: string; keywords?: string[]; enabled: boolean }
) {
return fetchApi<{ id?: string; name: string; email?: string; keywords?: string[]; enabled: boolean }>(
'/preferences/name-labels',
{
method: 'POST',
body: JSON.stringify({ userId, email: userEmail, nameLabel }),
}
)
},
async deleteNameLabel(userId: string, userEmail: string, labelId: string) {
return fetchApi<{ success: boolean }>(
`/preferences/name-labels/${labelId}?userId=${userId}&email=${encodeURIComponent(userEmail)}`,
{ method: 'DELETE' }
)
},
// ═══════════════════════════════════════════════════════════════════════════
// PRODUCTS & QUESTIONS (Legacy)
// ═══════════════════════════════════════════════════════════════════════════
async getProducts() {
return fetchApi<unknown[]>('/products')
},
async getQuestions(productSlug: string) {
return fetchApi<unknown[]>(`/questions?productSlug=${productSlug}`)
},
async createSubmission(productSlug: string, answers: Record<string, unknown>) {
return fetchApi<{ submissionId: string }>('/submissions', {
method: 'POST',
body: JSON.stringify({ productSlug, answers }),
})
},
async createCheckout(submissionId: string) {
return fetchApi<{ url: string; sessionId: string }>('/checkout', {
method: 'POST',
body: JSON.stringify({ submissionId }),
})
},
// ═══════════════════════════════════════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════════════════════════════════════
async getConfig() {
return fetchApi<{
features: {
gmail: boolean
outlook: boolean
ai: boolean
}
pricing: {
basic: { price: number; currency: string; accounts: number }
pro: { price: number; currency: string; accounts: number }
business: { price: number; currency: string; accounts: number }
}
}>('/config')
},
async healthCheck() {
return fetchApi<{
status: string
timestamp: string
version: string
environment: string
uptime: number
}>('/health')
},
// ═══════════════════════════════════════════════════════════════════════════
// ONBOARDING
// ═══════════════════════════════════════════════════════════════════════════
async getOnboardingStatus(userId: string) {
return fetchApi<{
onboarding_step: string
completedSteps: string[]
first_value_seen_at?: string
skipped_at?: string
}>(`/onboarding/status?userId=${userId}`)
},
async updateOnboardingStep(userId: string, step: string, completedSteps: string[] = []) {
return fetchApi<{ step: string; completedSteps: string[] }>('/onboarding/step', {
method: 'POST',
body: JSON.stringify({ userId, step, completedSteps }),
})
},
async skipOnboarding(userId: string) {
return fetchApi<{ skipped: boolean }>('/onboarding/skip', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
async resumeOnboarding(userId: string) {
return fetchApi<{
onboarding_step: string
completedSteps: string[]
}>('/onboarding/resume', {
method: 'POST',
body: JSON.stringify({ userId }),
})
},
// ═══════════════════════════════════════════════════════════════════════════
// ACCOUNT MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
async deleteAccount(userId: string) {
return fetchApi<{ success: boolean }>('/account/delete', {
method: 'DELETE',
body: JSON.stringify({ userId }),
})
},
// ═══════════════════════════════════════════════════════════════════════════
// REFERRALS
// ═══════════════════════════════════════════════════════════════════════════
async getReferralCode(userId: string) {
return fetchApi<{
referralCode: string
referralCount: number
}>(`/referrals/code?userId=${userId}`)
},
async trackReferral(userId: string, referralCode: string) {
return fetchApi<{ success: boolean }>('/referrals/track', {
method: 'POST',
body: JSON.stringify({ userId, referralCode }),
})
},
}
export default api