/** * API client for pounce backend * * API URL is determined dynamically based on the current hostname: * - localhost/127.0.0.1 → http://localhost:8000/api/v1 * - Local network IPs (10.x, 192.168.x) → http://{hostname}:8000/api/v1 * - Production (any other domain) → https://{hostname}/api/v1 (requires reverse proxy) */ const getApiBase = (): string => { // Server-side rendering: use environment variable or localhost if (typeof window === 'undefined') { const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1' return baseUrl.replace(/\/$/, '') } const { protocol, hostname, port } = window.location // Localhost development if (hostname === 'localhost' || hostname === '127.0.0.1') { return 'http://localhost:8000/api/v1' } // Local network (10.x.x.x, 192.168.x.x, 172.16-31.x.x) if (/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(hostname)) { return `http://${hostname}:8000/api/v1` } // Production: use same protocol and domain with /api/v1 path // This requires a reverse proxy (nginx/caddy) to route /api/v1 to the backend return `${protocol}//${hostname}/api/v1` } // Lazy-evaluated to ensure window is available on client let _apiBase: string | null = null const getApiBaseUrl = (): string => { if (_apiBase === null) { _apiBase = getApiBase() } return _apiBase } interface ApiError { detail: string } class ApiClient { private token: string | null = null setToken(token: string | null) { this.token = token if (token) { localStorage.setItem('token', token) } else { localStorage.removeItem('token') } } getToken(): string | null { if (typeof window === 'undefined') return null if (!this.token) { this.token = localStorage.getItem('token') } return this.token } protected async request( endpoint: string, options: RequestInit = {} ): Promise { const url = `${getApiBaseUrl()}${endpoint}` const headers: Record = { 'Content-Type': 'application/json', ...options.headers as Record, } const token = this.getToken() if (token) { headers['Authorization'] = `Bearer ${token}` } const response = await fetch(url, { ...options, headers, }) if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'An error occurred' })) const errorMessage = typeof errorData.detail === 'string' ? errorData.detail : (errorData.message || JSON.stringify(errorData.detail) || 'An error occurred') throw new Error(errorMessage) } if (response.status === 204) { return {} as T } return response.json() } // Auth async register(email: string, password: string, name?: string) { return this.request<{ id: number; email: string }>('/auth/register', { method: 'POST', body: JSON.stringify({ email, password, name }), }) } async login(email: string, password: string) { const response = await this.request<{ access_token: string; expires_in: number }>( '/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), } ) this.setToken(response.access_token) return response } async logout() { this.setToken(null) } async getMe() { return this.request<{ id: number email: string name: string | null is_active: boolean is_admin: boolean is_verified: boolean created_at: string }>('/auth/me') } async updateMe(data: { name?: string }) { return this.request<{ id: number email: string name: string | null is_active: boolean created_at: string }>('/auth/me', { method: 'PUT', body: JSON.stringify(data), }) } // Password Reset async forgotPassword(email: string) { return this.request<{ message: string; success: boolean }>('/auth/forgot-password', { method: 'POST', body: JSON.stringify({ email }), }) } async resetPassword(token: string, newPassword: string) { return this.request<{ message: string; success: boolean }>('/auth/reset-password', { method: 'POST', body: JSON.stringify({ token, new_password: newPassword }), }) } // Email Verification async verifyEmail(token: string) { return this.request<{ message: string; success: boolean }>('/auth/verify-email', { method: 'POST', body: JSON.stringify({ token }), }) } async resendVerification(email: string) { return this.request<{ message: string; success: boolean }>('/auth/resend-verification', { method: 'POST', body: JSON.stringify({ email }), }) } // OAuth async getOAuthProviders() { return this.request<{ google_enabled: boolean; github_enabled: boolean }>('/oauth/providers') } getGoogleLoginUrl(redirect?: string) { const params = redirect ? `?redirect=${encodeURIComponent(redirect)}` : '' return `${this.baseUrl}/oauth/google/login${params}` } getGitHubLoginUrl(redirect?: string) { const params = redirect ? `?redirect=${encodeURIComponent(redirect)}` : '' return `${this.baseUrl}/oauth/github/login${params}` } // Contact Form async submitContact(name: string, email: string, subject: string, message: string) { return this.request<{ message: string; success: boolean }>('/contact', { method: 'POST', body: JSON.stringify({ name, email, subject, message }), }) } // Newsletter async subscribeNewsletter(email: string) { return this.request<{ message: string; success: boolean }>('/contact/newsletter/subscribe', { method: 'POST', body: JSON.stringify({ email }), }) } async unsubscribeNewsletter(email: string, token?: string) { return this.request<{ message: string; success: boolean }>('/contact/newsletter/unsubscribe', { method: 'POST', body: JSON.stringify({ email, token }), }) } // Subscription - Stripe Integration async createCheckoutSession(plan: string, successUrl?: string, cancelUrl?: string) { return this.request<{ checkout_url: string; session_id: string }>('/subscription/checkout', { method: 'POST', body: JSON.stringify({ plan, success_url: successUrl, cancel_url: cancelUrl, }), }) } async createPortalSession() { return this.request<{ portal_url: string }>('/subscription/portal', { method: 'POST', }) } async cancelSubscription() { return this.request<{ status: string; message: string; new_tier: string }>('/subscription/cancel', { method: 'POST', }) } // Domain Check (public) async checkDomain(domain: string, quick = false) { return this.request<{ domain: string status: string is_available: boolean registrar: string | null expiration_date: string | null creation_date: string | null name_servers: string[] | null error_message: string | null checked_at: string }>('/check', { method: 'POST', body: JSON.stringify({ domain, quick }), }) } // Domains (authenticated) async getDomains(page = 1, perPage = 20) { return this.request<{ domains: Array<{ id: number name: string status: string is_available: boolean registrar: string | null expiration_date: string | null notify_on_available: boolean created_at: string last_checked: string | null }> total: number page: number per_page: number pages: number }>(`/domains?page=${page}&per_page=${perPage}`) } async addDomain(name: string, notify = true) { return this.request<{ id: number name: string status: string is_available: boolean }>('/domains', { method: 'POST', body: JSON.stringify({ name, notify_on_available: notify }), }) } async deleteDomain(id: number) { return this.request(`/domains/${id}`, { method: 'DELETE', }) } async refreshDomain(id: number) { return this.request<{ id: number name: string status: string is_available: boolean }>(`/domains/${id}/refresh`, { method: 'POST', }) } async updateDomainNotify(id: number, notify: boolean) { return this.request<{ id: number name: string notify: boolean }>(`/domains/${id}/notify`, { method: 'PATCH', body: JSON.stringify({ notify }), }) } // Subscription async getSubscription() { return this.request<{ id: number tier: string tier_name: string status: string domain_limit: number domains_used: number portfolio_limit: number check_frequency: string history_days: number features: { email_alerts: boolean priority_alerts: boolean full_whois: boolean expiration_tracking: boolean domain_valuation: boolean market_insights: boolean api_access: boolean webhooks: boolean bulk_tools: boolean seo_metrics: boolean } started_at: string expires_at: string | null }>('/subscription') } async getTiers() { return this.request<{ tiers: Array<{ id: string name: string domain_limit: number price: number features: string[] }> }>('/subscription/tiers') } // Domain History (Professional/Enterprise) async getDomainHistory(domainId: number, limit = 30) { return this.request<{ domain: string total_checks: number history: Array<{ id: number status: string is_available: boolean checked_at: string }> }>(`/domains/${domainId}/history?limit=${limit}`) } // TLD Pricing async getTldOverview( limit = 25, offset = 0, sortBy: 'popularity' | 'price_asc' | 'price_desc' | 'name' = 'popularity', search?: string ) { const params = new URLSearchParams({ limit: limit.toString(), offset: offset.toString(), sort_by: sortBy, }) if (search) { params.append('search', search) } return this.request<{ tlds: Array<{ tld: string type: string description: string avg_registration_price: number min_registration_price: number max_registration_price: number registrar_count: number trend: string popularity_rank?: number }> total: number limit: number offset: number has_more: boolean source: string }>(`/tld-prices/overview?${params.toString()}`) } async getTldHistory(tld: string, days = 90) { return this.request<{ tld: string type: string description: string registry: string current_price: number price_change_7d: number price_change_30d: number price_change_90d: number trend: string trend_reason: string history: Array<{ date: string price: number }> source: string }>(`/tld-prices/${tld}/history?days=${days}`) } async getTldCompare(tld: string) { return this.request<{ tld: string type: string description: string registry: string introduced: number | null registrars: Array<{ name: string registration_price: number renewal_price: number transfer_price: number }> cheapest_registrar: string cheapest_price: number price_range: { min: number max: number avg: number } }>(`/tld-prices/${tld}/compare`) } async getTrendingTlds() { return this.request<{ trending: Array<{ tld: string reason: string price_change: number current_price: number }> }>('/tld-prices/trending') } // ============== Portfolio ============== async getPortfolio( status?: string, sortBy = 'created_at', sortOrder = 'desc', limit = 100, offset = 0 ) { const params = new URLSearchParams({ sort_by: sortBy, sort_order: sortOrder, limit: limit.toString(), offset: offset.toString(), }) if (status) { params.append('status', status) } return this.request(`/portfolio?${params.toString()}`) } async getPortfolioSummary() { return this.request('/portfolio/summary') } async addPortfolioDomain(data: PortfolioDomainCreate) { return this.request('/portfolio', { method: 'POST', body: JSON.stringify(data), }) } async getPortfolioDomain(id: number) { return this.request(`/portfolio/${id}`) } async updatePortfolioDomain(id: number, data: Partial) { return this.request(`/portfolio/${id}`, { method: 'PUT', body: JSON.stringify(data), }) } async deletePortfolioDomain(id: number) { return this.request(`/portfolio/${id}`, { method: 'DELETE', }) } async markDomainSold(id: number, saleDate: string, salePrice: number) { return this.request(`/portfolio/${id}/sell`, { method: 'POST', body: JSON.stringify({ sale_date: saleDate, sale_price: salePrice, }), }) } async refreshDomainValue(id: number) { return this.request(`/portfolio/${id}/refresh-value`, { method: 'POST', }) } async getDomainValuation(domain: string) { return this.request(`/portfolio/valuation/${domain}`) } // ============== Auctions (Smart Pounce) ============== async getAuctions( keyword?: string, tld?: string, platform?: string, minBid?: number, maxBid?: number, endingSoon = false, sortBy = 'ending', limit = 100, offset = 0 ) { const params = new URLSearchParams({ sort_by: sortBy, limit: limit.toString(), offset: offset.toString(), ending_soon: endingSoon.toString(), }) if (keyword) params.append('keyword', keyword) if (tld) params.append('tld', tld) if (platform) params.append('platform', platform) if (minBid !== undefined) params.append('min_bid', minBid.toString()) if (maxBid !== undefined) params.append('max_bid', maxBid.toString()) return this.request<{ auctions: Array<{ domain: string platform: string platform_url: string current_bid: number currency: string num_bids: number end_time: string time_remaining: string buy_now_price: number | null reserve_met: boolean | null traffic: number | null age_years: number | null tld: string affiliate_url: string }> total: number platforms_searched: string[] last_updated: string }>(`/auctions?${params.toString()}`) } async getEndingSoonAuctions(hours = 1, limit = 10) { return this.request>(`/auctions/ending-soon?hours=${hours}&limit=${limit}`) } async getHotAuctions(limit = 10) { return this.request>(`/auctions/hot?limit=${limit}`) } async getAuctionOpportunities() { return this.request<{ opportunities: Array<{ auction: { domain: string platform: string platform_url: string current_bid: number currency: string num_bids: number end_time: string time_remaining: string buy_now_price: number | null reserve_met: boolean | null traffic: number | null age_years: number | null tld: string affiliate_url: string } analysis: { estimated_value: number current_bid: number value_ratio: number potential_profit: number opportunity_score: number recommendation: string } }> strategy_tips: string[] generated_at: string }>('/auctions/opportunities') } async getAuctionPlatformStats() { return this.request>('/auctions/stats') } // ============== Price Alerts ============== async getPriceAlerts(activeOnly = false) { const params = activeOnly ? '?active_only=true' : '' return this.request(`/price-alerts${params}`) } async createPriceAlert(tld: string, targetPrice?: number, thresholdPercent = 5) { return this.request('/price-alerts', { method: 'POST', body: JSON.stringify({ tld: tld.replace(/^\./, ''), target_price: targetPrice, threshold_percent: thresholdPercent, }), }) } async getPriceAlertStatus(tld: string) { return this.request<{ tld: string; has_alert: boolean; is_active: boolean }>( `/price-alerts/status/${tld.replace(/^\./, '')}` ) } async getPriceAlert(tld: string) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}`) } async updatePriceAlert(tld: string, data: { is_active?: boolean; target_price?: number; threshold_percent?: number }) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}`, { method: 'PUT', body: JSON.stringify(data), }) } async deletePriceAlert(tld: string) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}`, { method: 'DELETE', }) } async togglePriceAlert(tld: string) { return this.request(`/price-alerts/${tld.replace(/^\./, '')}/toggle`, { method: 'POST', }) } } // ============== Types ============== export interface PortfolioDomain { id: number domain: string purchase_date: string | null purchase_price: number | null purchase_registrar: string | null registrar: string | null renewal_date: string | null renewal_cost: number | null auto_renew: boolean estimated_value: number | null value_updated_at: string | null is_sold: boolean sale_date: string | null sale_price: number | null status: string notes: string | null tags: string | null roi: number | null created_at: string updated_at: string } export interface PortfolioSummary { total_domains: number active_domains: number sold_domains: number total_invested: number total_value: number total_sold_value: number unrealized_profit: number realized_profit: number overall_roi: number } export interface PortfolioDomainCreate { domain: string purchase_date?: string purchase_price?: number purchase_registrar?: string registrar?: string renewal_date?: string renewal_cost?: number auto_renew?: boolean notes?: string tags?: string } export interface DomainValuation { domain: string estimated_value: number currency: string scores: { length: number tld: number keyword: number brandability: number overall: number } factors: { length: number tld: string has_numbers: boolean has_hyphens: boolean is_dictionary_word: boolean } confidence: string source: string calculated_at: string } export interface PriceAlert { id: number tld: string is_active: boolean target_price: number | null threshold_percent: number last_notified_at: string | null last_notified_price: number | null created_at: string } // ============== Admin API Extension ============== class AdminApiClient extends ApiClient { // Admin Stats async getAdminStats() { return this.request('/admin/stats') } // Admin Users async getAdminUsers(limit: number = 50, offset: number = 0, search?: string) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset) }) if (search) params.append('search', search) return this.request(`/admin/users?${params}`) } async getAdminUser(userId: number) { return this.request(`/admin/users/${userId}`) } async updateAdminUser(userId: number, data: { name?: string; is_active?: boolean; is_verified?: boolean; is_admin?: boolean }) { return this.request(`/admin/users/${userId}`, { method: 'PATCH', body: JSON.stringify(data), }) } async deleteAdminUser(userId: number) { return this.request(`/admin/users/${userId}`, { method: 'DELETE' }) } async upgradeUser(userId: number, tier: string) { return this.request(`/admin/users/${userId}/upgrade?tier=${tier}`, { method: 'POST' }) } // Newsletter async getAdminNewsletter(limit: number = 100, offset: number = 0, activeOnly: boolean = true) { const params = new URLSearchParams({ limit: String(limit), offset: String(offset), active_only: String(activeOnly), }) return this.request(`/admin/newsletter?${params}`) } async exportNewsletterEmails() { return this.request<{ emails: string[]; count: number }>('/admin/newsletter/export') } // TLD Scraping async triggerTldScrape() { return this.request('/admin/scrape-tld-prices', { method: 'POST' }) } async getTldPriceStats() { return this.request('/admin/tld-prices/stats') } // Auction Scraping async triggerAuctionScrape() { return this.request('/auctions/admin/scrape', { method: 'POST' }) } async getAuctionScrapeStatus() { return this.request('/auctions/admin/scrape-status') } // System async getSystemHealth() { return this.request('/admin/system/health') } async makeUserAdmin(email: string) { return this.request(`/admin/system/make-admin?email=${encodeURIComponent(email)}`, { method: 'POST' }) } // Price Alerts async getAdminPriceAlerts(limit = 100, offset = 0) { return this.request<{ alerts: Array<{ id: number tld: string target_price: number | null alert_type: string created_at: string user: { id: number; email: string; name: string | null } }> total: number }>(`/admin/price-alerts?limit=${limit}&offset=${offset}`) } // Domain Health Check async triggerDomainChecks() { return this.request<{ message: string domains_queued: number started_at: string }>('/admin/domains/check-all', { method: 'POST' }) } // Email Test async sendTestEmail() { return this.request<{ message: string sent_to: string timestamp: string }>('/admin/system/test-email', { method: 'POST' }) } // Scheduler Status async getSchedulerStatus() { return this.request<{ scheduler_running: boolean jobs: Array<{ id: string name: string next_run: string | null trigger: string }> last_runs: { tld_scrape: string | null auction_scrape: string | null domain_check: string | null } timestamp: string }>('/admin/system/scheduler') } // User Export async exportUsersCSV() { return this.request<{ csv: string count: number exported_at: string }>('/admin/users/export') } // Bulk Upgrade async bulkUpgradeUsers(userIds: number[], tier: string) { return this.request<{ message: string tier: string upgraded: Array<{ user_id: number; email: string }> failed: Array<{ user_id: number; reason: string }> total_upgraded: number total_failed: number }>('/admin/users/bulk-upgrade', { method: 'POST', body: JSON.stringify({ user_ids: userIds, tier }), }) } // Activity Log async getActivityLog(limit = 50, offset = 0) { return this.request<{ logs: Array<{ id: number action: string details: string created_at: string admin: { id: number; email: string; name: string | null } }> total: number }>(`/admin/activity-log?limit=${limit}&offset=${offset}`) } // ============== Blog ============== async getBlogPosts(limit = 10, offset = 0, category?: string, tag?: string) { let url = `/blog/posts?limit=${limit}&offset=${offset}` if (category) url += `&category=${encodeURIComponent(category)}` if (tag) url += `&tag=${encodeURIComponent(tag)}` return this.request<{ posts: Array<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null tags: string[] is_published: boolean published_at: string | null created_at: string view_count: number author: { id: number; name: string | null } }> total: number limit: number offset: number }>(url) } async getBlogPost(slug: string) { return this.request<{ id: number title: string slug: string excerpt: string | null content: string cover_image: string | null category: string | null tags: string[] meta_title: string | null meta_description: string | null is_published: boolean published_at: string | null created_at: string updated_at: string view_count: number author: { id: number; name: string | null } }>(`/blog/posts/${slug}`) } async getBlogCategories() { return this.request<{ categories: Array<{ name: string; count: number }> }>('/blog/posts/categories') } async getFeaturedPosts(limit = 3) { return this.request<{ posts: Array<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null published_at: string | null }> }>(`/blog/posts/featured?limit=${limit}`) } // Admin Blog async getAdminBlogPosts(limit = 50, offset = 0, status?: 'published' | 'draft') { let url = `/blog/admin/posts?limit=${limit}&offset=${offset}` if (status) url += `&status_filter=${status}` return this.request<{ posts: Array<{ id: number title: string slug: string excerpt: string | null cover_image: string | null category: string | null tags: string[] is_published: boolean published_at: string | null created_at: string view_count: number author: { id: number; name: string | null } }> total: number }>(url) } async createBlogPost(data: { title: string content: string excerpt?: string cover_image?: string category?: string tags?: string[] meta_title?: string meta_description?: string is_published?: boolean }) { return this.request('/blog/admin/posts', { method: 'POST', body: JSON.stringify(data), }) } async updateBlogPost(postId: number, data: { title?: string content?: string excerpt?: string cover_image?: string category?: string tags?: string[] meta_title?: string meta_description?: string is_published?: boolean }) { return this.request(`/blog/admin/posts/${postId}`, { method: 'PATCH', body: JSON.stringify(data), }) } async deleteBlogPost(postId: number) { return this.request(`/blog/admin/posts/${postId}`, { method: 'DELETE', }) } async publishBlogPost(postId: number) { return this.request(`/blog/admin/posts/${postId}/publish`, { method: 'POST', }) } async unpublishBlogPost(postId: number) { return this.request(`/blog/admin/posts/${postId}/unpublish`, { method: 'POST', }) } } export const api = new AdminApiClient()