Manajemen Kuota (Billing)

Serial Bagian 6: Membatasi penggunaan API berdasarkan paket pengguna dan membuat dashboard agar user bisa memantau sisa kuota mereka.

21. Update Database Model (Quota)

Kita perlu menambahkan kolom quota_limit (maksimal request) dan quota_used (terpakai) ke tabel User.

CATATAN MIGRASI: Karena kita menggunakan SQLite sederhana, cara termudah adalah menghapus file db.sqlite3 dan biarkan sistem membuatnya ulang secara otomatis. Data user lama akan hilang, jadi Admin harus membuat ulang user.
File: /www/wwwroot/app-collection/ao/app/models.py
from sqlalchemy import Column, Integer, String, Boolean
from core.db import Base

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    email = Column(String, unique=True, index=True)
    api_key = Column(String, unique=True, index=True)
    is_active = Column(Boolean, default=True)
    
    # KOLOM BARU UNTUK QUOTA
    quota_limit = Column(Integer, default=100)  # Default free tier 100 request
    quota_used = Column(Integer, default=0)

Jangan lupa update logika pembuatan user di admin.py agar bisa menentukan quota awal jika diperlukan.

22. Quota Middleware

Setiap kali user memanggil API Chat, kita harus mengecek apakah kuota masih cukup. Jika cukup, tambahkan penggunaannya.

Update: /www/wwwroot/app-collection/ao/app/api/v1/endpoints/chat.py
from fastapi import APIRouter, Depends, HTTPException, Header
from sqlalchemy.orm import Session
from typing import Annotated

from core.db import get_db
import core.models as models
# ... import AI model logic

router = APIRouter()

# Dependency untuk cek API Key DAN Kuota
async def get_current_user(
    x_api_key: str = Header(...),
    db: Session = Depends(get_db)
):
    user = db.query(models.User).filter(models.User.api_key == x_api_key).first()
    if not user:
        raise HTTPException(status_code=403, detail="Invalid API Key")
    
    if user.quota_used >= user.quota_limit:
        raise HTTPException(
            status_code=429, 
            detail=f"Quota Habis (Limit: {user.quota_limit}). Silakan upgrade."
        )
    
    return user

@router.post("/generate")
def generate_text(
    prompt: str,
    current_user: Annotated[models.User, Depends(get_current_user)],
    db: Session = Depends(get_db)
):
    # 1. Generate AI Content (Simulasi)
    result_text = f"AI Response to: {prompt}"
    
    # 2. Update Kuota
    current_user.quota_used += 1
    db.commit()
    
    return {
        "text": result_text,
        "quota_remaining": current_user.quota_limit - current_user.quota_used
    }

23. User Dashboard UI

Admin sudah punya panel, sekarang user juga perlu tahu berapa sisa kuotanya. Kita buat halaman dashboard sederhana.

File: /www/wwwroot/ao.baktimakmur.com/dashboard.html
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>SaaS Dashboard</title>
    <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>Dashboard Pengguna</h1>
        </header>

        <section class="card">
            <h2>Statistik API</h2>
            <p>Masukkan API Key Anda untuk melihat status.</p>
            
            <div class="form-group">
                <label>API Key Anda</label>
                <input type="text" id="apiKeyInput" placeholder="prod-sk-...">
            </div>
            <button id="checkBtn">Cek Status</button>
        </section>

        <section class="card" id="resultCard" style="display:none;">
            <h2>Detail Kuota</h2>
            <div style="background:#f1f5f9; height:20px; border-radius:10px; margin: 20px 0; overflow:hidden;">
                <div id="progressBar" style="background:var(--primary); height:100%; width:0%; transition:width 0.5s;"></div>
            </div>
            
            <p><strong>Terkirim:</strong> <span id="usedVal">0</span> / <span id="limitVal">0</span></p>
            <p id="statusMsg"></p>
        </section>
    </div>

    <script src="/static/js/dashboard.js"></script>
</body>
</html>
File: /www/wwwroot/ao.baktimakmur.com/static/js/dashboard.js
const API_URL = '/api/v1/chat/me'; // Kita buat endpoint /me baru

document.getElementById('checkBtn').addEventListener('click', async () => {
    const apiKey = document.getElementById('apiKeyInput').value;
    const resultCard = document.getElementById('resultCard');
    const progressBar = document.getElementById('progressBar');
    
    if(!apiKey) return alert("Masukkan API Key");

    try {
        // Panggil endpoint /me (perlu dibuat di Python)
        // Untuk demo ini, kita anggap endpoint GET /users/ dengan header API Key
        const response = await fetch('/api/v1/admin/users', {
            headers: { 'X-Api-Key': apiKey } // Note: Ini bypass admin key, idealnya buat endpoint /me khusus
        });

        // CATATAN: Di sistem nyata, buat endpoint GET /api/v1/users/me yang hanya return data sendiri
        // Di sini kita simulasi parsing user dari list (kurang aman tapi untuk tutorial OK)
        const users = await response.json();
        const user = users.find(u => u.api_key === apiKey);

        if(user) {
            resultCard.style.display = 'block';
            document.getElementById('usedVal').textContent = user.quota_used;
            document.getElementById('limitVal').textContent = user.quota_limit;
            
            const percentage = (user.quota_used / user.quota_limit) * 100;
            progressBar.style.width = percentage + '%';
            
            if(user.quota_used >= user.quota_limit) {
                document.getElementById('statusMsg').innerHTML = "<span style='color:red'>Kuota Habis!</span>";
                progressBar.style.backgroundColor = "red";
            } else {
                document.getElementById('statusMsg').innerHTML = "<span style='color:green'>Aktif</span>";
            }
        } else {
            alert("API Key tidak valid");
        }

    } catch (error) {
        console.error(error);
        alert("Gagal mengambil data. Pastikan endpoint tersedia.");
    }
});

Validasi Sistem

Untuk menguji apakah sistem kuota berfungsi:

  1. Buka dashboard.html, masukkan API Key User.
  2. Catat kuota (misal: 0/100).
  3. Panggil API /generate berkali-kali (gunakan Postman atau frontend).
  4. Refresh dashboard.html, angka "Terkirim" harus bertambah.
  5. Jika mencapai 100, API akan mengembalikan Error 429.