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:
- Buka
dashboard.html, masukkan API Key User. - Catat kuota (misal: 0/100).
- Panggil API
/generateberkali-kali (gunakan Postman atau frontend). - Refresh
dashboard.html, angka "Terkirim" harus bertambah. - Jika mencapai 100, API akan mengembalikan Error 429.