UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities

812 lines (684 loc) 22.4 kB
/** * SPAPS Local Server Documentation HTML Generator * Generates comprehensive documentation page */ function generateDocsHTML(port = 3300, notice) { return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SPAPS Documentation - Local Development Mode</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #333; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } .container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .header { background: white; border-radius: 12px; padding: 2rem; margin-bottom: 2rem; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .header h1 { color: #764ba2; font-size: 2.5rem; margin-bottom: 0.5rem; } .header .subtitle { color: #666; font-size: 1.1rem; } .status-badge { display: inline-block; background: #48bb78; color: white; padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.9rem; margin-top: 1rem; } .nav { background: white; border-radius: 12px; padding: 1rem; margin-bottom: 2rem; box-shadow: 0 5px 15px rgba(0,0,0,0.08); position: sticky; top: 1rem; z-index: 100; } .nav ul { list-style: none; display: flex; gap: 2rem; overflow-x: auto; padding: 0.5rem; } .nav a { color: #764ba2; text-decoration: none; font-weight: 500; white-space: nowrap; transition: color 0.3s; } .nav a:hover { color: #667eea; } .content { background: white; border-radius: 12px; padding: 2rem; margin-bottom: 2rem; box-shadow: 0 5px 15px rgba(0,0,0,0.08); } h2 { color: #764ba2; margin-top: 2rem; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid #f0f0f0; } h3 { color: #667eea; margin-top: 1.5rem; margin-bottom: 0.75rem; } pre { background: #f7f7f7; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; overflow-x: auto; margin: 1rem 0; position: relative; } code { font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace; font-size: 0.9rem; background: #f7f7f7; padding: 0.2rem 0.4rem; border-radius: 4px; } pre code { background: none; padding: 0; } .endpoint { background: #f9f9f9; border-left: 4px solid #667eea; padding: 1rem; margin: 1rem 0; border-radius: 4px; } .endpoint strong { color: #764ba2; font-family: 'SF Mono', Monaco, monospace; font-size: 0.95rem; } .endpoint .method { display: inline-block; padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: bold; font-size: 0.85rem; margin-right: 0.5rem; } .endpoint .method.get { background: #48bb78; color: white; } .endpoint .method.post { background: #4299e1; color: white; } .endpoint .method.put { background: #ed8936; color: white; } .endpoint .method.delete { background: #f56565; color: white; } .tabs { display: flex; gap: 1rem; margin-bottom: 1rem; border-bottom: 2px solid #f0f0f0; } .tab { padding: 0.5rem 1rem; cursor: pointer; border: none; background: none; color: #666; font-size: 1rem; transition: all 0.3s; } .tab.active { color: #764ba2; border-bottom: 2px solid #764ba2; margin-bottom: -2px; } .tab-content { display: none; } .tab-content.active { display: block; } .feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin: 2rem 0; } .feature-card { background: #f9f9f9; padding: 1.5rem; border-radius: 8px; transition: transform 0.3s; } .feature-card:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .feature-card h3 { margin-top: 0; } .copy-button { position: absolute; top: 0.5rem; right: 0.5rem; background: #667eea; color: white; border: none; padding: 0.25rem 0.75rem; border-radius: 4px; cursor: pointer; font-size: 0.85rem; transition: background 0.3s; } .copy-button:hover { background: #764ba2; } .copy-button.copied { background: #48bb78; } .alert { background: #fef5e7; border: 1px solid #f9e79f; border-radius: 8px; padding: 1rem; margin: 1rem 0; } .alert.info { background: #e8f4fd; border-color: #bee5eb; } .alert.success { background: #d4edda; border-color: #c3e6cb; } .footer { text-align: center; color: white; padding: 2rem; } .footer a { color: white; text-decoration: underline; } </style> </head> <body> <div class="container"> <div class="header"> <h1>🍠 SPAPS Documentation</h1> <p class="subtitle">Sweet Potato Authentication & Payment Service</p> <span class="status-badge">✅ Local Mode Active - Port ${port}</span> </div> ${notice ? `<div class="alert">${notice}</div>` : ''} <nav class="nav"> <ul> <li><a href="#quickstart">Quick Start</a></li> <li><a href="#sdk">SDK Setup</a></li> <li><a href="#authentication">Authentication</a></li> <li><a href="#payments">Payments</a></li> <li><a href="#endpoints">API Endpoints</a></li> <li><a href="#examples">Examples</a></li> <li><a href="#testing">Testing</a></li> </ul> </nav> <div class="content"> <div class="alert info"> <strong>🚀 Local Development Mode</strong><br> You're running in local mode. No API keys, database, or external services required! All responses are mocked for rapid development. </div> <section id="quickstart"> <h2>Quick Start</h2> <h3>1. Install the SDK</h3> <pre><code>npm install spaps-sdk # or yarn add spaps-sdk</code></pre> <h3>2. Initialize the Client</h3> <div class="tabs"> <button class="tab active" onclick="showTab(event, 'js-init')">JavaScript</button> <button class="tab" onclick="showTab(event, 'ts-init')">TypeScript</button> <button class="tab" onclick="showTab(event, 'react-init')">React</button> </div> <div id="js-init" class="tab-content active"> <pre><code>const { SPAPSClient } = require('spaps-sdk'); // Auto-detects local mode - no config needed! const spaps = new SPAPSClient(); // Login const { data } = await spaps.login('user@example.com', 'password'); console.log('User:', data.user); console.log('Token:', data.access_token);</code></pre> </div> <div id="ts-init" class="tab-content"> <pre><code>import { SPAPSClient } from 'spaps-sdk'; // TypeScript types included const spaps = new SPAPSClient({ apiUrl: 'http://localhost:${port}' }); // Fully typed responses const { data } = await spaps.login('user@example.com', 'password'); console.log('User ID:', data.user.id);</code></pre> </div> <div id="react-init" class="tab-content"> <pre><code>import { SPAPSClient } from 'spaps-sdk'; import { createContext, useContext } from 'react'; const spaps = new SPAPSClient(); const SpapsContext = createContext(spaps); export function SpapsProvider({ children }) { return ( &lt;SpapsContext.Provider value={spaps}&gt; {children} &lt;/SpapsContext.Provider&gt; ); } export const useSpaps = () => useContext(SpapsContext);</code></pre> </div> </section> <section id="sdk"> <h2>SDK Setup & Configuration</h2> <h3>Installation</h3> <pre><code>npm install spaps-sdk</code></pre> <h3>Configuration Options</h3> <pre><code>const spaps = new SPAPSClient({ // API endpoint (auto-detected from env) apiUrl: 'http://localhost:${port}', // API key (not needed for localhost) apiKey: 'spaps_live_abc123...', // Request timeout in milliseconds timeout: 10000, // Auto-detect local mode (default: true) autoDetect: true });</code></pre> <h3>Environment Variables</h3> <pre><code># .env SPAPS_API_URL=http://localhost:${port} SPAPS_API_KEY=your_api_key_here # Next.js NEXT_PUBLIC_SPAPS_API_URL=http://localhost:${port}</code></pre> <div class="feature-grid"> <div class="feature-card"> <h3>🔌 Auto-Detection</h3> <p>SDK automatically detects local mode when URL contains localhost or 127.0.0.1</p> </div> <div class="feature-card"> <h3>🔑 No API Key</h3> <p>Local mode doesn't require API keys - perfect for rapid development</p> </div> <div class="feature-card"> <h3>📦 TypeScript</h3> <p>Full TypeScript support with type definitions included</p> </div> <div class="feature-card"> <h3>🔄 Auto-Refresh</h3> <p>Tokens automatically refresh when expired</p> </div> <div class="feature-card"> <h3>💳 Mock Stripe</h3> <p>Checkout & webhooks work instantly - no Stripe account needed!</p> </div> <div class="feature-card"> <h3>⚡ Auto Webhooks</h3> <p>Webhooks fire automatically 2 seconds after payment actions</p> </div> </div> </section> <section id="authentication"> <h2>Authentication</h2> <h3>Email/Password Authentication</h3> <pre><code>// Register new user const { data } = await spaps.register('user@example.com', 'password'); console.log('New user:', data.user); // Login existing user const { data } = await spaps.login('user@example.com', 'password'); console.log('Access token:', data.access_token); // Get current user const user = await spaps.getUser(); console.log('Current user:', user.data); // Logout await spaps.logout();</code></pre> <h3>Wallet Authentication</h3> <pre><code>// Solana wallet await spaps.walletSignIn( walletAddress, signature, message, 'solana' ); // Ethereum wallet await spaps.walletSignIn( walletAddress, signature, message, 'ethereum' );</code></pre> <h3>Token Management</h3> <pre><code>// Check if authenticated if (spaps.isAuthenticated()) { console.log('User is logged in'); } // Get access token const token = spaps.getAccessToken(); // Set token manually spaps.setAccessToken(token); // Refresh token await spaps.refresh();</code></pre> </section> <section id="payments"> <h2>Payment Integration</h2> <div class="alert" style="background: #e0f2fe; border-color: #0369a1; margin-bottom: 2rem;"> <strong>🎉 New in v0.3.1:</strong> Local Stripe testing with automatic webhook simulation! <br>• No Stripe account required for local development <br>• Webhooks fire automatically after payments <br>• Mock checkout page included <br>• Test webhook UI at <code>/api/stripe/webhooks/test</code> </div> <h3>Stripe Checkout (Works in Local Mode!)</h3> <pre><code>// Create checkout session - works instantly in local mode! const session = await spaps.createCheckoutSession( 'price_local_validate', // Use local price IDs 'http://localhost:3000/success', // Success URL 'http://localhost:3000/cancel' // Cancel URL (optional) ); // In local mode: Returns mock checkout URL // In production: Returns real Stripe URL window.location.href = session.data.url; // Webhook fires automatically after 2 seconds in local mode!</code></pre> <h3>Local Mode Test Products</h3> <pre><code>// Pre-configured test products (Buildooor tiers) const prices = { 'price_local_validate': '$500 - Validate tier', 'price_local_prototype': '$2,500 - Prototype tier', 'price_local_strategy': '$10,000 - Strategy tier', 'price_local_build': '$25,000 - Build tier' };</code></pre> <h3>Webhook Testing</h3> <pre><code>// Your webhook handler (same code for local & production!) app.post('/webhook', (req, res) => { const event = req.body; switch(event.type) { case 'checkout.session.completed': // In local: fires 2 seconds after checkout // In production: fires when payment completes console.log('Payment successful!'); break; } res.json({ received: true }); }); // Test webhooks via UI (local only) // Visit: http://localhost:${port}/api/stripe/webhooks/test</code></pre> <h3>Subscription Management</h3> <pre><code>// Get current subscription const subscription = await spaps.getSubscription(); console.log('Status:', subscription.data.status); console.log('Plan:', subscription.data.plan); // Cancel subscription await spaps.cancelSubscription();</code></pre> <h3>Usage Tracking</h3> <pre><code>// Check balance const balance = await spaps.getUsageBalance(); console.log('Credits:', balance.data.balance); // Record usage await spaps.recordUsage('api-call', 1); await spaps.recordUsage('image-generation', 10);</code></pre> </section> <section id="endpoints"> <h2>API Endpoints</h2> <h3>Authentication</h3> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/auth/register</strong> - Register new user </div> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/auth/login</strong> - Login with email/password </div> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/auth/wallet-sign-in</strong> - Wallet authentication </div> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/auth/refresh</strong> - Refresh access token </div> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/auth/logout</strong> - Logout user </div> <div class="endpoint"> <span class="method get">GET</span> <strong>/api/auth/user</strong> - Get current user </div> <h3>Payments</h3> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/stripe/create-checkout-session</strong> - Create Stripe checkout </div> <div class="endpoint"> <span class="method get">GET</span> <strong>/api/stripe/subscription</strong> - Get subscription status </div> <div class="endpoint"> <span class="method delete">DELETE</span> <strong>/api/stripe/subscription</strong> - Cancel subscription </div> <h3>Usage</h3> <div class="endpoint"> <span class="method get">GET</span> <strong>/api/usage/balance</strong> - Get usage balance </div> <div class="endpoint"> <span class="method post">POST</span> <strong>/api/usage/record</strong> - Record usage event </div> <h3>Health</h3> <div class="endpoint"> <span class="method get">GET</span> <strong>/health</strong> - Health check </div> <div class="endpoint"> <span class="method get">GET</span> <strong>/health/local-mode</strong> - Local mode status </div> </section> <section id="examples"> <h2>Complete Examples</h2> <h3>React Login Component</h3> <pre><code>import { useState } from 'react'; import { useSpaps } from './contexts/SpapsContext'; function LoginForm() { const spaps = useSpaps(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); setLoading(true); try { const { data } = await spaps.login(email, password); console.log('Logged in:', data.user); // Redirect to dashboard } catch (error) { console.error('Login failed:', error); } finally { setLoading(false); } }; return ( &lt;form onSubmit={handleSubmit}&gt; &lt;input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required /&gt; &lt;input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required /&gt; &lt;button type="submit" disabled={loading}&gt; {loading ? 'Logging in...' : 'Login'} &lt;/button&gt; &lt;/form&gt; ); }</code></pre> <h3>Express.js Middleware</h3> <pre><code>const express = require('express'); const { SPAPSClient } = require('spaps-sdk'); const app = express(); const spaps = new SPAPSClient(); // Auth middleware async function requireAuth(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token' }); } try { spaps.setAccessToken(token); const { data } = await spaps.getUser(); req.user = data; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } } // Protected route app.get('/api/profile', requireAuth, (req, res) => { res.json(req.user); });</code></pre> <h3>Next.js Server Action</h3> <pre><code>'use server'; import { SPAPSClient } from 'spaps-sdk'; import { cookies } from 'next/headers'; const spaps = new SPAPSClient(); export async function loginAction(email: string, password: string) { try { const { data } = await spaps.login(email, password); // Store token in cookie cookies().set('spaps_token', data.access_token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7 // 1 week }); return { success: true, user: data.user }; } catch (error) { return { success: false, error: 'Invalid credentials' }; } }</code></pre> </section> <section id="testing"> <h2>Testing</h2> <h3>Test with cURL</h3> <pre><code># Health check curl http://localhost:${port}/health # Login curl -X POST http://localhost:${port}/api/auth/login \\ -H "Content-Type: application/json" \\ -d '{"email":"test@example.com","password":"password"}' # Get user (with token) curl http://localhost:${port}/api/auth/user \\ -H "Authorization: Bearer YOUR_TOKEN"</code></pre> <h3>Test with SDK</h3> <pre><code>// test.js const { SPAPSClient } = require('spaps-sdk'); async function test() { const spaps = new SPAPSClient(); // Test login const { data } = await spaps.login('test@example.com', 'password'); console.log('✅ Login successful:', data.user); // Test authenticated request const user = await spaps.getUser(); console.log('✅ Got user:', user.data); // Test Stripe const session = await spaps.createCheckoutSession( 'price_123', 'http://localhost:3000/success' ); console.log('✅ Checkout URL:', session.data.url); } test().catch(console.error);</code></pre> <div class="alert success"> <strong>💡 Pro Tip:</strong> In local mode, all endpoints return successful mock responses. Use any email/password combination for testing. </div> </section> </div> <div class="footer"> <p> <strong>SPAPS v0.2.6</strong> | <a href="https://github.com/yourusername/sweet-potato">GitHub</a> | <a href="https://sweetpotato.dev">Website</a> | <a href="https://discord.gg/sweetpotato">Discord</a> </p> <p>Made with 🍠 by the Sweet Potato team</p> </div> </div> <script> function showTab(event, tabId) { // Hide all tab contents const contents = document.querySelectorAll('.tab-content'); contents.forEach(content => content.classList.remove('active')); // Remove active from all tabs const tabs = document.querySelectorAll('.tab'); tabs.forEach(tab => tab.classList.remove('active')); // Show selected tab document.getElementById(tabId).classList.add('active'); event.target.classList.add('active'); } // Add copy buttons to code blocks document.addEventListener('DOMContentLoaded', () => { const codeBlocks = document.querySelectorAll('pre'); codeBlocks.forEach(block => { const button = document.createElement('button'); button.className = 'copy-button'; button.textContent = 'Copy'; button.onclick = () => { const code = block.querySelector('code').textContent; navigator.clipboard.writeText(code).then(() => { button.textContent = 'Copied!'; button.classList.add('copied'); setTimeout(() => { button.textContent = 'Copy'; button.classList.remove('copied'); }, 2000); }); }; block.appendChild(button); }); }); </script> </body> </html>`; } module.exports = { generateDocsHTML };