Autentikasi Mandiri (SaaS Standard)

Serial Bagian 9: Menghapus ketergantungan pada Admin. User bisa mendaftar sendiri, login, dan mendapatkan API Key secara otomatis.

30. Setup Library Keamanan (Hashing)

Kita akan menggunakan passlib untuk hashing password (agar tidak disimpan mentah) dan python-jose untuk manajemen JWT Token.

Update: requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
pydantic==2.5.0
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-dotenv==1.0.0

Jalankan install:

pip install -r requirements.txt

31. API Login & Register

Kita buat endpoint baru khusus autentikasi. Logikanya: Saat Register, password di-hash, user dibuat, dan API Key digenerate otomatis.

File: /www/wwwroot/app-collection/ao/app/api/v1/endpoints/auth.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta

from core.db import get_db
import core.models as models

router = APIRouter()

# --- CONFIG ---
SECRET_KEY = "rahasia_super_ganti_ini_di_env" # Untuk menandatangani JWT
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
    return pwd_context.hash(password)

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

@router.post("/register")
def register(username: str, email: str, password: str, db: Session = Depends(get_db)):
    # Cek Email Duplikat
    db_user = db.query(models.User).filter(models.User.email == email).first()
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")

    # Hash Password
    hashed_pw = get_password_hash(password)
    
    # Generate API Key Otomatis (Format sama seperti Part 5)
    import secrets
    api_key = f"prod-sk-{secrets.token_hex(16)}"
    
    # Create User
    new_user = models.User(
        username=username,
        email=email,
        hashed_password=hashed_pw, # Simpan hash, bukan plain text
        api_key=api_key,
        quota_limit=10, # Free Trial Quota
        quota_used=0
    )
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    
    return {"message": "User created successfully", "api_key": api_key}

@router.post("/login")
def login(email: str, password: str, db: Session = Depends(get_db)):
    user = db.query(models.User).filter(models.User.email == email).first()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    if not verify_password(password, user.hashed_password):
        raise HTTPException(status_code=401, detail="Incorrect password")
    
    # Kembalikan API Key user ini agar bisa langsung dipakai di Frontend
    return {"access_token": create_access_token({"sub": user.email}), "api_key": user.api_key}
PENTING UPDATE MODEL: Jangan lupa update models.py untuk menambah kolom hashed_password (bukan password plain text).
Update: /www/wwwroot/app-collection/ao/app/models.py
# ... import Column, String ...
# Add this column:
hashed_password = Column(String)

32. Halaman Login / Register

Frontend sederhana untuk menangani pendaftaran mandiri.

File: /www/wwwroot/ao.baktimakmur.com/login.html
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>Login / Register - AI SaaS</title>
    <link rel="stylesheet" href="/static/css/style.css">
    <style>
        .auth-container { max-width: 400px; margin: 50px auto; text-align: center; }
        .form-group { margin-bottom: 15px; text-align: left; }
        input, button { width: 100%; padding: 10px; margin-top: 5px; }
        .toggle-link { color: var(--primary); cursor: pointer; font-size: 0.9rem; }
        #apiResult { margin-top: 20px; padding: 10px; background: #f0fdf4; border: 1px solid #22c55e; display: none; }
    </style>
</head>
<body>
    <div class="container auth-container">
        <h2 id="formTitle">Login</h2>
        
        <!-- Form Login -->
        <form id="loginForm">
            <div class="form-group">
                <label>Email</label>
                <input type="email" id="logEmail" required>
            </div>
            <div class="form-group">
                <label>Password</label>
                <input type="password" id="logPass" required>
            </div>
            <button type="submit">Masuk</button>
        </form>

        <!-- Form Register (Hidden by default) -->
        <form id="registerForm" style="display:none;">
            <div class="form-group">
                <label>Username</label>
                <input type="text" id="regUser" required>
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" id="regEmail" required>
            </div>
            <div class="form-group">
                <label>Password</label>
                <input type="password" id="regPass" required>
            </div>
            <button type="submit" style="background:var(--primary); color:white;">Daftar Akun</button>
        </form>

        <p style="margin-top:20px;">
            <span id="toggleText">Belum punya akun?</span> 
            <span id="toggleBtn" class="toggle-link">Daftar disini</span>
        </p>

        <!-- Result Box (API Key) -->
        <div id="apiResult">
            <strong>Berhasil!</strong><br>
            API Key Anda: <code id="displayKey"></code>
            <br><small>Simpan Key ini untuk digunakan di Aplikasi Utama.</small>
            <br>
            <a href="/index.html" style="display:inline-block; margin-top:10px; font-weight:bold;">Ke Aplikasi Utama →</a>
        </div>
    </div>

    <script src="/static/js/auth.js"></script>
</body>
</html>
File: /www/wwwroot/ao.baktimakmur.com/static/js/auth.js
const API_BASE = '/api/v1/auth';

// Toggle UI
const toggleBtn = document.getElementById('toggleBtn');
let isLogin = true;

toggleBtn.addEventListener('click', () => {
    isLogin = !isLogin;
    document.getElementById('loginForm').style.display = isLogin ? 'block' : 'none';
    document.getElementById('registerForm').style.display = isLogin ? 'none' : 'block';
    document.getElementById('formTitle').textContent = isLogin ? 'Login' : 'Register';
    document.getElementById('toggleText').textContent = isLogin ? 'Belum punya akun?' : 'Sudah punya akun?';
    toggleBtn.textContent = isLogin ? 'Daftar disini' : 'Login disini';
});

// Handle Login
document.getElementById('loginForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    const email = document.getElementById('logEmail').value;
    const password = document.getElementById('logPass').value;

    try {
        const res = await fetch(`${API_BASE}/login?email=${email}&password=${password}`, { method: 'POST' });
        const data = await res.json();
        if(res.ok) showKey(data.api_key);
        else alert(data.detail);
    } catch(err) { console.error(err); }
});

// Handle Register
document.getElementById('registerForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    const username = document.getElementById('regUser').value;
    const email = document.getElementById('regEmail').value;
    const password = document.getElementById('regPass').value;

    try {
        const res = await fetch(`${API_BASE}/register?username=${username}&email=${email}&password=${password}`, { method: 'POST' });
        const data = await res.json();
        if(res.ok) showKey(data.api_key);
        else alert(data.detail);
    } catch(err) { console.error(err); }
});

function showKey(key) {
    document.getElementById('displayKey').textContent = key;
    document.getElementById('apiResult').style.display = 'block';
    // Simpan otomatis ke localStorage
    localStorage.setItem('saas_api_key', key);
}