commit ab112c1c5b372df9876cbe89e8a245986b082f64 Author: Dita Aji Pratama Date: Mon Jun 8 17:11:32 2026 +0700 First commit diff --git a/API_DOCS.md b/API_DOCS.md new file mode 100644 index 0000000..7f6d56a --- /dev/null +++ b/API_DOCS.md @@ -0,0 +1,357 @@ +# ๐Ÿ“– API Enroll Documentation + +**Base URL:** `http://localhost:8080` + +**Endpoint:** `/api/enroll` + +Seluruh operasi CRUD dilakukan melalui **satu endpoint** ini, cukup dengan `GET` dan `POST`. + +--- + +## ๐Ÿ” Authentication + +Semua request ke `/api/enroll` **wajib** menyertakan header: + +``` +Authorization: Bearer +``` + +| Info | Nilai | +|-------------|--------------| +| Token default | `secret123` | +| Letak config | `app.py` โ†’ variabel `API_TOKEN` | + +**Response `401 Unauthorized`** (tanpa header) + +```json +{ + "error": "missing or invalid Authorization header" +} +``` + +**Response `401 Unauthorized`** (token salah) + +```json +{ + "error": "invalid token" +} +``` + +--- + +## ๐Ÿ“Š Database Schema + +| Kolom | Tipe | Keterangan | +|-----------|---------|---------------------------------------------------------| +| `id` | INTEGER | Primary key, auto-increment | +| `type` | TEXT | Tipe enrollment โ€” **hanya boleh `Annually` atau `Monthly`** | +| `service` | TEXT | Paket layanan โ€” **hanya boleh `Lite`, `Value`, atau `Pro`** | +| `user` | TEXT | Nama pengguna (contoh: `alice`) | + +--- + +## ๐Ÿ  Routes + +| Route | Method | Auth | Fungsi | +|----------------|--------|------|--------------------------------| +| `/` | GET | โŒ | Halaman playground (UI web) | +| `/api/enroll` | GET | โœ… | List / Detail enrollment | +| `/api/enroll` | POST | โœ… | Add / Edit / Remove enrollment | + +--- + +## 1๏ธโƒฃ List โ€” Mendapatkan semua data + +**Request** + +``` +GET /api/enroll +Authorization: Bearer secret123 +``` + +**Contoh curl** + +```bash +curl -H "Authorization: Bearer secret123" \ + http://localhost:8080/api/enroll +``` + +**Response `200 OK`** + +```json +{ + "data": [ + { + "id": 2, + "type": "Monthly", + "service": "Lite", + "user": "bob" + }, + { + "id": 1, + "type": "Annually", + "service": "Pro", + "user": "alice" + } + ] +} +``` + +--- + +## 2๏ธโƒฃ Detail โ€” Mendapatkan satu data + +**Request** + +``` +GET /api/enroll?id= +Authorization: Bearer secret123 +``` + +**Parameter Query** + +| Parameter | Tipe | Wajib | Keterangan | +|-----------|---------|-------|---------------| +| `id` | integer | โœ… | ID enrollment | + +**Contoh curl** + +```bash +curl -H "Authorization: Bearer secret123" \ + http://localhost:8080/api/enroll?id=1 +``` + +**Response `200 OK`** + +```json +{ + "data": { + "id": 1, + "type": "Annually", + "service": "Pro", + "user": "alice" + } +} +``` + +**Response `404 Not Found`** + +```json +{ + "error": "not found" +} +``` + +--- + +## 3๏ธโƒฃ Add โ€” Menambah data baru + +**Request** + +``` +POST /api/enroll +Authorization: Bearer secret123 +Content-Type: application/x-www-form-urlencoded +``` + +**Form Data** + +| Field | Tipe | Wajib | Keterangan | +|-----------|--------|-------|---------------------------------------------------------| +| `type` | string | โœ… | **Hanya boleh `Annually` atau `Monthly`** | +| `service` | string | โœ… | **Hanya boleh `Lite`, `Value`, atau `Pro`** | +| `user` | string | โœ… | Nama pengguna | + +> โš ๏ธ Jangan kirim field `id` โ€” kalau `id` terkirim, ini akan dianggap **Edit**. + +**Contoh curl** + +```bash +curl -X POST http://localhost:8080/api/enroll \ + -H "Authorization: Bearer secret123" \ + -d "type=Annually" \ + -d "service=Pro" \ + -d "user=alice" +``` + +**Response `201 Created`** + +```json +{ + "message": "created", + "id": 1 +} +``` + +**Response `400 Bad Request`** (type/service tidak valid) + +```json +{ + "error": "type must be one of: Annually, Monthly" +} +``` + +```json +{ + "error": "service must be one of: Lite, Value, Pro" +} +``` + +--- + +## 4๏ธโƒฃ Edit โ€” Mengubah data yang sudah ada + +**Request** + +``` +POST /api/enroll +Authorization: Bearer secret123 +Content-Type: application/x-www-form-urlencoded +``` + +**Form Data** + +| Field | Tipe | Wajib | Keterangan | +|-----------|---------|-------|---------------------------------------------------------| +| `id` | integer | โœ… | ID yang ingin diubah | +| `type` | string | โœ… | **Hanya boleh `Annually` atau `Monthly`** (baru) | +| `service` | string | โœ… | **Hanya boleh `Lite`, `Value`, atau `Pro`** (baru) | +| `user` | string | โœ… | Nama pengguna (baru) | + +> ๐Ÿ’ก Cukup sertakan `id` pada form POST, sistem otomatis tahu ini adalah operasi **Edit**. + +**Contoh curl** + +```bash +curl -X POST http://localhost:8080/api/enroll \ + -H "Authorization: Bearer secret123" \ + -d "id=1" \ + -d "type=Monthly" \ + -d "service=Value" \ + -d "user=bob" +``` + +**Response `200 OK`** + +```json +{ + "message": "updated", + "id": 1 +} +``` + +**Response `400 Bad Request`** (type/service tidak valid) + +```json +{ + "error": "type must be one of: Annually, Monthly" +} +``` + +```json +{ + "error": "service must be one of: Lite, Value, Pro" +} +``` + +--- + +## 5๏ธโƒฃ Remove โ€” Menghapus data + +**Request** + +``` +POST /api/enroll +Authorization: Bearer secret123 +Content-Type: application/x-www-form-urlencoded +``` + +**Form Data** + +| Field | Tipe | Wajib | Keterangan | +|-----------|---------|-------|---------------------------------------------| +| `id` | integer | โœ… | ID yang ingin dihapus | +| `action` | string | โœ… | Harus bernilai `"remove"` (sebagai penanda) | + +**Contoh curl** + +```bash +curl -X POST http://localhost:8080/api/enroll \ + -H "Authorization: Bearer secret123" \ + -d "id=1" \ + -d "action=remove" +``` + +**Response `200 OK`** + +```json +{ + "message": "removed", + "id": 1 +} +``` + +**Response `400 Bad Request`** (tanpa id) + +```json +{ + "error": "id is required for remove" +} +``` + +--- + +## ๐Ÿ“‹ Ringkasan Logika POST + +``` +POST /api/enroll +โ”‚ +โ”œโ”€โ”€ action = "remove" โ†’ hapus data (butuh id) +โ”œโ”€โ”€ ada id โ†’ edit data (butuh type, service, user) +โ””โ”€โ”€ tidak ada id โ†’ tambah data baru (butuh type, service, user) +``` + +## ๐Ÿ”‘ Ringkasan Response Error + +| Status | Kondisi | Body | +|--------|--------------------------------|---------------------------------------------| +| `401` | Tanpa header Authorization | `{"error": "missing or invalid โ€ฆ"}` | +| `401` | Token salah | `{"error": "invalid token"}` | +| `400` | Field wajib kosong | `{"error": "โ€ฆ are required"}` | +| `400` | Type bukan Annually/Monthly | `{"error": "type must be one of: Annually, Monthly"}` | +| `400` | Service bukan Lite/Value/Pro | `{"error": "service must be one of: Lite, Value, Pro"}` | +| `404` | ID tidak ditemukan | `{"error": "not found"}` | + +## ๐ŸŽฎ Playground UI + +Setelah server jalan, buka browser ke: + +``` +http://localhost:8080 +``` + +Anda akan diarahkan ke halaman **Playground** yang memudahkan pengujian semua endpoint langsung dari browser: + +1. **Isi token** di bar atas (default: `secret123`) +2. Status otomatis berubah: โœ… authenticated / โŒ wrong token +3. Isi form, klik tombol, lihat response JSON secara realtime + +--- + +## ๐Ÿš€ Quick Start + +```bash +# masuk ke folder project +cd api-playground + +# install dependency +pip3 install -r requirements.txt + +# jalankan server +python3 app.py + +# buka di browser +# http://localhost:8080 +``` + +Server akan berjalan di `http://0.0.0.0:8080` dengan `reloader=True` (auto-restart saat kode diubah). diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..dfb043d Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/db.cpython-313.pyc b/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000..dc1f306 Binary files /dev/null and b/__pycache__/db.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..0b55dc2 --- /dev/null +++ b/app.py @@ -0,0 +1,117 @@ +from bottle import Bottle, request, response, static_file +import json +import os +import db + +app = Bottle() +db.init_db() + +# โ”€โ”€ auth config โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Token simpel, mudah diingat. Ganti sesuai kebutuhan. +API_TOKEN = "secret123" + + +# โ”€โ”€ helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +def json_response(data, status=200): + response.content_type = "application/json" + response.status = status + return json.dumps(data) + + +def check_auth(): + """ + Cek Bearer token. + Return None jika OK, atau string JSON error jika gagal. + """ + auth = request.get_header("Authorization", "") + if not auth.startswith("Bearer "): + return json.dumps({"error": "missing or invalid Authorization header"}) + token = auth.split(" ", 1)[1].strip() + if token != API_TOKEN: + return json.dumps({"error": "invalid token"}) + return None # โœ… token valid + + +# โ”€โ”€ static playground page (tanpa auth) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +@app.get("/") +def index(): + return static_file("playground.html", root=os.path.join(os.path.dirname(__file__), "templates")) + + +# โ”€โ”€ API: /api/enroll (butuh auth) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +@app.get("/api/enroll") +def api_enroll(): + """ + GET /api/enroll โ†’ list all + GET /api/enroll?id=3 โ†’ detail id=3 + """ + err = check_auth() + if err: + return json_response(json.loads(err), 401) + + enroll_id = request.query.id + if enroll_id: + row = db.get_enroll(int(enroll_id)) + if not row: + return json_response({"error": "not found"}, 404) + return json_response({"data": row}) + rows = db.list_enrolls() + return json_response({"data": rows}) + + +@app.post("/api/enroll") +def api_enroll_post(): + """ + POST /api/enroll (form fields + Bearer token) + id โ†’ if present โ†’ edit, otherwise โ†’ add + type + service + user + action โ†’ "remove" to delete (needs id) + """ + err = check_auth() + if err: + return json_response(json.loads(err), 401) + + action = request.forms.get("action", "").strip() + enroll_id = request.forms.get("id", "").strip() + type_ = request.forms.get("type", "").strip() + service = request.forms.get("service", "").strip() + user = request.forms.get("user", "").strip() + + # โ”€โ”€ remove โ”€โ”€ + if action == "remove": + if not enroll_id: + return json_response({"error": "id is required for remove"}, 400) + db.remove_enroll(int(enroll_id)) + return json_response({"message": "removed", "id": int(enroll_id)}) + + # โ”€โ”€ validate type โ”€โ”€ + if type_ not in db.VALID_TYPES: + return json_response( + {"error": f"type must be one of: {', '.join(db.VALID_TYPES)}"}, 400 + ) + + # โ”€โ”€ validate service โ”€โ”€ + if service not in db.VALID_SERVICES: + return json_response( + {"error": f"service must be one of: {', '.join(db.VALID_SERVICES)}"}, 400 + ) + + # โ”€โ”€ edit โ”€โ”€ + if enroll_id: + if not type_ or not service or not user: + return json_response({"error": "type, service, user are required"}, 400) + db.edit_enroll(int(enroll_id), type_, service, user) + return json_response({"message": "updated", "id": int(enroll_id)}) + + # โ”€โ”€ add โ”€โ”€ + if not type_ or not service or not user: + return json_response({"error": "type, service, user are required"}, 400) + new_id = db.add_enroll(type_, service, user) + return json_response({"message": "created", "id": new_id}, 201) + + +# โ”€โ”€ run โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080, debug=True, reloader=True) diff --git a/db.py b/db.py new file mode 100644 index 0000000..53ba725 --- /dev/null +++ b/db.py @@ -0,0 +1,70 @@ +import sqlite3 +import os + +DB_PATH = os.path.join(os.path.dirname(__file__), "enroll.db") + +# โ”€โ”€ allowed values โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +VALID_TYPES = ("Annually", "Monthly") +VALID_SERVICES = ("Lite", "Value", "Pro") + + +def get_conn(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def init_db(): + conn = get_conn() + conn.execute(""" + CREATE TABLE IF NOT EXISTS enroll ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + service TEXT NOT NULL, + user TEXT NOT NULL + ) + """) + conn.commit() + conn.close() + + +def list_enrolls(): + conn = get_conn() + rows = conn.execute("SELECT * FROM enroll ORDER BY id DESC").fetchall() + conn.close() + return [dict(r) for r in rows] + + +def get_enroll(enroll_id): + conn = get_conn() + row = conn.execute("SELECT * FROM enroll WHERE id = ?", (enroll_id,)).fetchone() + conn.close() + return dict(row) if row else None + + +def add_enroll(type_, service, user): + conn = get_conn() + cur = conn.execute( + "INSERT INTO enroll (type, service, user) VALUES (?, ?, ?)", + (type_, service, user), + ) + conn.commit() + conn.close() + return cur.lastrowid + + +def edit_enroll(enroll_id, type_, service, user): + conn = get_conn() + conn.execute( + "UPDATE enroll SET type = ?, service = ?, user = ? WHERE id = ?", + (type_, service, user, enroll_id), + ) + conn.commit() + conn.close() + + +def remove_enroll(enroll_id): + conn = get_conn() + conn.execute("DELETE FROM enroll WHERE id = ?", (enroll_id,)) + conn.commit() + conn.close() diff --git a/enroll.db b/enroll.db new file mode 100644 index 0000000..dcf49b7 Binary files /dev/null and b/enroll.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fc1fc4f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +bottle==0.13.4 diff --git a/templates/playground.html b/templates/playground.html new file mode 100644 index 0000000..8beb4da --- /dev/null +++ b/templates/playground.html @@ -0,0 +1,297 @@ + + + + + +API Enroll Playground + + + + +

๐Ÿš€ API Enroll Playground

+ + +
+ + + no token + default: secret123 +
+ +
+ + +
+

โž• Add (POST)

+ + + + + +
+ +
+
โ€”
+
+ + +
+

๐Ÿ“‹ List (GET)

+
+ +
+
โ€”
+
+ + +
+

๐Ÿ” Detail (GET)

+ + +
+ +
+
โ€”
+
+ + +
+

โœ๏ธ Edit (POST)

+ + + + + + +
+ +
+
โ€”
+
+ + +
+

๐Ÿ—‘๏ธ Remove (POST)

+ + +
+ +
+
โ€”
+
+ + +
+

๐Ÿ“Š Table Preview

+
+ +
+ + + +
IDTypeServiceUser
Click Refresh
+
+ +
+ + + +