Tentang SIMAPI
SIMAPI (Sistem Informasi Manajemen API) adalah platform manajemen API canggih yang memungkinkan organisasi Anda meng-expose data internal (pegawai, user, absensi, dll) ke aplikasi lain dengan aman, terstruktur, dan teraudit.
Dirancang untuk kebutuhan internal enterprise — di mana aplikasi seperti HRIS, Absensi, Payroll, dan Dashboard perlu mengakses data induk tanpa harus langsung query ke database production.
v2.0 kini mendukung:
- 6 jenis database — MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, SQLite
- Refresh token otomatis — client tidak perlu login ulang tiap jam
- Token expiry per-client — setiap client bisa punya durasi token sendiri
- Token rotation & reuse detection — keamanan tingkat enterprise
- Multi-platform — backend, web, mobile (Android/iOS), desktop, IoT (ESP32/ESP8266)
Manfaat & Keunggulan
Keamanan Berlapis
JWT access token, refresh token rotation, reuse detection, scope permission, IP whitelist, rate limiting.
Kontrol Granular
Atur akses per aplikasi, per endpoint, bahkan per field (basic/sensitive/protected).
Monitoring Real-time
Dashboard statistik, audit log lengkap, top clients, error tracking, dan usage analytics.
Multi-Database Support
Satu panel untuk MySQL, MariaDB, PostgreSQL, Oracle, SQL Server, dan SQLite.
Refresh Token Otomatis
Access token pendek + refresh token panjang. Client tidak perlu login ulang tiap jam.
Multi-Platform + IoT
Contoh kode untuk backend, web, mobile (Android/iOS), desktop, dan IoT (ESP32/ESP8266).
Database yang Didukung
SIMAPI v2.0 mendukung 6 jenis database — relasional klasik hingga modern. Anda tinggal pilih jenisnya saat menambah koneksi di panel admin.
Cara Memulai
Admin Mendaftarkan Anda
Hubungi admin SIMAPI untuk mendaftarkan aplikasi Anda. Admin akan:
- Menambahkan koneksi database (MySQL / PostgreSQL / Oracle / SQL Server / MariaDB / SQLite)
- Memilih tabel yang di-expose sebagai API
- Mendaftarkan aplikasi Anda sebagai Client
- Memberikan kredensial lewat channel aman
Terima Kredensial dari Admin
Admin akan memberikan:
client_id— identitas aplikasi Andaclient_secret— kunci rahasia (simpan aman!)- Daftar scope yang diizinkan
- URL API endpoint
- Durasi token (default: access 60 menit, refresh 30 hari)
Simpan Kredensial di Tempat Aman
Simpan client_id & client_secret di environment variable (file .env), bukan di source code.
API_CLIENT_ID=app_absensi_abc123
API_CLIENT_SECRET=sk_live_xxxxxxxxxxxxx
API_TOKEN_URL=https://api.anda.com/oauth/token
API_BASE_URL=https://api.anda.com/api/v1
Login Pertama — Dapat Access + Refresh Token
Tukar kredensial menjadi access token + refresh token.
Akses API
Kirim request dengan header Authorization: Bearer <access_token> ke endpoint yang diizinkan (sesuai scope Anda).
Refresh Otomatis
Setelah access token expired, jangan login ulang. Cukup kirim refresh token ke /oauth/token dengan grant_type=refresh_token. Server akan kasih access token baru + refresh token baru (rotation).
Otentikasi (Token)
Ringkasan
SIMAPI menggunakan OAuth2 dengan dua jenis token:
- Access Token (JWT) — default 1 jam, dipakai untuk akses API
- Refresh Token (opaque) — default 30 hari, dipakai untuk memperbarui access token
Durasi bisa berbeda per client — diatur admin sesuai kebutuhan aplikasi.
1. Login — Dapat Token Pertama
/oauth/tokenRequest Body
{
"client_id": "app_absensi_abc123",
"client_secret": "sk_live_xxxxxxxxxxxxx",
"grant_type": "client_credentials"
}
Response Sukses
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "pegawai:read:basic absensi:read:basic",
"refresh_token": "rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_expires_in": 2592000
}
Nilai expires_in dan refresh_expires_in bisa berbeda per client.
2. Refresh — Dapat Access Token Baru
/oauth/token{
"refresh_token": "rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"grant_type": "refresh_token"
}
refresh_token baru. Simpan yang baru, buang yang lama. Kalau token lama dipakai lagi (tanda pencurian), semua token client akan di-revoke otomatis.
3. Logout — Revoke Refresh Token
/oauth/revoke{
"refresh_token": "rt_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
access_token di memory dan refresh 60 detik sebelum expired. Refresh token hanya bisa dipakai sekali (rotation). Kalau request gagal 401, retry 1x dengan token baru.
Endpoint API
Semua endpoint resource mengikuti pola yang sama:
| Method | Endpoint | Scope | Deskripsi |
|---|---|---|---|
| GET | /api/v1/{resource} | {resource}:read:basic | List data |
| GET | /api/v1/{resource}/{id} | {resource}:read:basic | Detail satu record |
| POST | /api/v1/{resource} | {resource}:create | Tambah data |
| PUT | /api/v1/{resource}/{id} | {resource}:update | Update data |
| DELETE | /api/v1/{resource}/{id} | {resource}:delete | Hapus data |
Query Parameters
| Parameter | Contoh | Deskripsi |
|---|---|---|
page | ?page=2 | Halaman (default: 1) |
limit | ?limit=20 | Per halaman (default 20, max 100) |
search | ?search=budi | Cari di field searchable |
sort | ?sort=nama | Urutkan berdasarkan field |
order | ?order=desc | asc / desc |
filter[field] | ?filter[jabatan]=Manager | Filter nilai field |
Contoh Penggunaan
Pilih platform Anda di bawah. Semua contoh sudah menggunakan refresh token otomatis — tidak perlu login ulang tiap jam.
client_secret di aplikasi mobile/SPA. Untuk platform tersebut, gunakan backend proxy yang menyimpan client_secret dan mem-proxy request ke SIMAPI (contoh di tab "Backend Proxy untuk Mobile/SPA").
Backend (Server-to-Server)
# 1. Login — dapat access + refresh token
RESP=$(curl -s -X POST https://api.anda.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "app_absensi_abc123",
"client_secret": "sk_live_xxxxxxxxxxxxx",
"grant_type": "client_credentials"
}')
ACCESS_TOKEN=$(echo "$RESP" | jq -r '.access_token')
REFRESH_TOKEN=$(echo "$RESP" | jq -r '.refresh_token')
# 2. Akses API
curl -X GET https://api.anda.com/api/v1/pegawai \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 3. Refresh (setelah access token expired)
RESP2=$(curl -s -X POST https://api.anda.com/oauth/token \
-H "Content-Type: application/json" \
-d "{
\"refresh_token\": \"$REFRESH_TOKEN\",
\"grant_type\": \"refresh_token\"
}")
ACCESS_TOKEN=$(echo "$RESP2" | jq -r '.access_token')
REFRESH_TOKEN=$(echo "$RESP2" | jq -r '.refresh_token')
import requests
from datetime import datetime, timedelta
class SIMAPIClient:
"""Client SIMAPI dengan auto-refresh token."""
def __init__(self, client_id, client_secret, base_url):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url.rstrip('/')
self.access_token = None
self.refresh_token = None
self.expires_at = None
def _login(self):
resp = requests.post(f"{self.base_url}/oauth/token", json={
"client_id": self.client_id,
"client_secret": self.client_secret,
"grant_type": "client_credentials",
})
data = resp.json()
self.access_token = data["access_token"]
self.refresh_token = data["refresh_token"]
self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
def _refresh(self):
if not self.refresh_token:
return self._login()
resp = requests.post(f"{self.base_url}/oauth/token", json={
"refresh_token": self.refresh_token,
"grant_type": "refresh_token",
})
if resp.status_code != 200:
return self._login()
data = resp.json()
self.access_token = data["access_token"]
self.refresh_token = data["refresh_token"] # rotation
self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
def _ensure_token(self):
if not self.access_token or datetime.now() >= self.expires_at:
self._refresh()
return self.access_token
def get(self, path, **params):
headers = {"Authorization": f"Bearer {self._ensure_token()}"}
resp = requests.get(f"{self.base_url}{path}", headers=headers, params=params)
if resp.status_code == 401:
self._refresh()
headers = {"Authorization": f"Bearer {self._ensure_token()}"}
resp = requests.get(f"{self.base_url}{path}", headers=headers, params=params)
return resp.json()
# Pemakaian
api = SIMAPIClient(
client_id="app_absensi_abc123",
client_secret="sk_live_xxxxxxxxxxxxx",
base_url="https://api.anda.com",
)
data = api.get("/api/v1/pegawai", page=1, limit=10)
print(data)
class SIMAPIClient {
constructor(clientId, clientSecret, baseUrl) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = baseUrl.replace(/\/$/, '');
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
}
async _login() {
const resp = await fetch(`${this.baseUrl}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: this.clientId,
client_secret: this.clientSecret,
grant_type: 'client_credentials',
}),
});
const data = await resp.json();
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
this.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
}
async _refresh() {
if (!this.refreshToken) return this._login();
const resp = await fetch(`${this.baseUrl}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: this.refreshToken,
grant_type: 'refresh_token',
}),
});
if (!resp.ok) return this._login();
const data = await resp.json();
this.accessToken = data.access_token;
this.refreshToken = data.refresh_token;
this.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
}
async _ensureToken() {
if (!this.accessToken || Date.now() >= this.expiresAt) {
await this._refresh();
}
return this.accessToken;
}
async get(path, params = {}) {
const token = await this._ensureToken();
const qs = new URLSearchParams(params).toString();
const url = `${this.baseUrl}${path}${qs ? '?' + qs : ''}`;
let resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (resp.status === 401) {
await this._refresh();
resp = await fetch(url, {
headers: { Authorization: `Bearer ${await this._ensureToken()}` },
});
}
return resp.json();
}
}
// Pemakaian
(async () => {
const api = new SIMAPIClient(
'app_absensi_abc123',
'sk_live_xxxxxxxxxxxxx',
'https://api.anda.com'
);
const data = await api.get('/api/v1/pegawai', { page: 1, limit: 10 });
console.log(data);
})();
<?php
class SIMAPIClient {
private $clientId, $clientSecret, $baseUrl;
private $accessToken = null, $refreshToken = null, $expiresAt = 0;
public function __construct($clientId, $clientSecret, $baseUrl) {
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
$this->baseUrl = rtrim($baseUrl, '/');
}
private function login() {
$ch = curl_init("{$this->baseUrl}/oauth/token");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'grant_type' => 'client_credentials',
]),
]);
$data = json_decode(curl_exec($ch), true);
$this->accessToken = $data['access_token'];
$this->refreshToken = $data['refresh_token'];
$this->expiresAt = time() + $data['expires_in'] - 60;
}
private function refresh() {
if (!$this->refreshToken) return $this->login();
$ch = curl_init("{$this->baseUrl}/oauth/token");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'refresh_token' => $this->refreshToken,
'grant_type' => 'refresh_token',
]),
]);
$data = json_decode(curl_exec($ch), true);
if (!isset($data['access_token'])) return $this->login();
$this->accessToken = $data['access_token'];
$this->refreshToken = $data['refresh_token'];
$this->expiresAt = time() + $data['expires_in'] - 60;
}
private function ensureToken() {
if (!$this->accessToken || time() >= $this->expiresAt) {
$this->refresh();
}
return $this->accessToken;
}
public function get($path, $params = []) {
$url = "{$this->baseUrl}{$path}";
if ($params) $url .= '?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer {$this->ensureToken()}"],
]);
return json_decode(curl_exec($ch), true);
}
}
$api = new SIMAPIClient(
'app_absensi_abc123',
'sk_live_xxxxxxxxxxxxx',
'https://api.anda.com'
);
$data = $api->get('/api/v1/pegawai', ['page' => 1, 'limit' => 10]);
print_r($data);
package simapi
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
type SIMAPIClient struct {
ClientID string
ClientSecret string
BaseURL string
mu sync.Mutex
accessToken string
refreshToken string
expiresAt time.Time
httpClient *http.Client
}
func NewSIMAPIClient(clientID, clientSecret, baseURL string) *SIMAPIClient {
return &SIMAPIClient{
ClientID: clientID,
ClientSecret: clientSecret,
BaseURL: baseURL,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
type tokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int `json:"expires_in"`
RefreshExpiresIn int `json:"refresh_expires_in"`
}
func (c *SIMAPIClient) login() error {
body, _ := json.Marshal(map[string]string{
"client_id": c.ClientID,
"client_secret": c.ClientSecret,
"grant_type": "client_credentials",
})
resp, err := c.httpClient.Post(c.BaseURL+"/oauth/token", "application/json", bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()
var data tokenResp
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
c.accessToken = data.AccessToken
c.refreshToken = data.RefreshToken
c.expiresAt = time.Now().Add(time.Duration(data.ExpiresIn-60) * time.Second)
return nil
}
func (c *SIMAPIClient) refresh() error {
if c.refreshToken == "" {
return c.login()
}
body, _ := json.Marshal(map[string]string{
"refresh_token": c.refreshToken,
"grant_type": "refresh_token",
})
resp, err := c.httpClient.Post(c.BaseURL+"/oauth/token", "application/json", bytes.NewBuffer(body))
if err != nil {
return c.login()
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return c.login()
}
var data tokenResp
json.NewDecoder(resp.Body).Decode(&data)
c.accessToken = data.AccessToken
c.refreshToken = data.RefreshToken
c.expiresAt = time.Now().Add(time.Duration(data.ExpiresIn-60) * time.Second)
return nil
}
func (c *SIMAPIClient) ensureToken() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.accessToken == "" || time.Now().After(c.expiresAt) {
return c.refresh()
}
return nil
}
func (c *SIMAPIClient) Get(path string) (map[string]interface{}, error) {
if err := c.ensureToken(); err != nil {
return nil, err
}
req, _ := http.NewRequest("GET", c.BaseURL+path, nil)
req.Header.Set("Authorization", "Bearer "+c.accessToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
c.mu.Lock()
c.refresh()
c.mu.Unlock()
req.Header.Set("Authorization", "Bearer "+c.accessToken)
resp, _ = c.httpClient.Do(req)
defer resp.Body.Close()
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
func main() {
api := NewSIMAPIClient("app_absensi_abc123", "sk_live_xxxxxxxxxxxxx", "https://api.anda.com")
data, err := api.Get("/api/v1/pegawai?page=1&limit=10")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(data)
}
import java.net.URI;
import java.net.http.*;
import java.time.Instant;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SIMAPIClient {
private final String clientId, clientSecret, baseUrl;
private final HttpClient http;
private final ObjectMapper mapper = new ObjectMapper();
private String accessToken;
private String refreshToken;
private Instant expiresAt;
public SIMAPIClient(String clientId, String clientSecret, String baseUrl) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = baseUrl;
this.http = HttpClient.newHttpClient();
}
private void login() throws Exception {
String body = mapper.writeValueAsString(Map.of(
"client_id", clientId,
"client_secret", clientSecret,
"grant_type", "client_credentials"
));
var req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/oauth/token"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, HttpResponse.BodyHandlers.ofString());
var data = mapper.readTree(resp.body());
accessToken = data.get("access_token").asText();
refreshToken = data.get("refresh_token").asText();
expiresAt = Instant.now().plusSeconds(data.get("expires_in").asLong() - 60);
}
private void refresh() throws Exception {
if (refreshToken == null) { login(); return; }
String body = mapper.writeValueAsString(Map.of(
"refresh_token", refreshToken,
"grant_type", "refresh_token"
));
var req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/oauth/token"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) { login(); return; }
var data = mapper.readTree(resp.body());
accessToken = data.get("access_token").asText();
refreshToken = data.get("refresh_token").asText();
expiresAt = Instant.now().plusSeconds(data.get("expires_in").asLong() - 60);
}
private String ensureToken() throws Exception {
if (accessToken == null || Instant.now().isAfter(expiresAt)) refresh();
return accessToken;
}
public String get(String path) throws Exception {
var req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("Authorization", "Bearer " + ensureToken())
.GET()
.build();
var resp = http.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 401) {
refresh();
req = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("Authorization", "Bearer " + accessToken)
.GET()
.build();
resp = http.send(req, HttpResponse.BodyHandlers.ofString());
}
return resp.body();
}
public static void main(String[] args) throws Exception {
var api = new SIMAPIClient("app_absensi_abc123", "sk_live_xxxxxxxxxxxxx", "https://api.anda.com");
System.out.println(api.get("/api/v1/pegawai?page=1&limit=10"));
}
}
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
public class SIMAPIClient
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _baseUrl;
private readonly HttpClient _http;
private string _accessToken;
private string _refreshToken;
private DateTime _expiresAt;
public SIMAPIClient(string clientId, string clientSecret, string baseUrl)
{
_clientId = clientId;
_clientSecret = clientSecret;
_baseUrl = baseUrl.TrimEnd('/');
_http = new HttpClient();
}
private async Task LoginAsync()
{
var resp = await _http.PostAsJsonAsync($"{_baseUrl}/oauth/token", new
{
client_id = _clientId,
client_secret = _clientSecret,
grant_type = "client_credentials"
});
var data = await resp.Content.ReadFromJsonAsync();
_accessToken = data.GetProperty("access_token").GetString();
_refreshToken = data.GetProperty("refresh_token").GetString();
var expiresIn = data.GetProperty("expires_in").GetInt32();
_expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - 60);
}
private async Task RefreshAsync()
{
if (string.IsNullOrEmpty(_refreshToken))
{
await LoginAsync();
return;
}
var resp = await _http.PostAsJsonAsync($"{_baseUrl}/oauth/token", new
{
refresh_token = _refreshToken,
grant_type = "refresh_token"
});
if (!resp.IsSuccessStatusCode)
{
await LoginAsync();
return;
}
var data = await resp.Content.ReadFromJsonAsync();
_accessToken = data.GetProperty("access_token").GetString();
_refreshToken = data.GetProperty("refresh_token").GetString();
var expiresIn = data.GetProperty("expires_in").GetInt32();
_expiresAt = DateTime.UtcNow.AddSeconds(expiresIn - 60);
}
private async Task EnsureTokenAsync()
{
if (string.IsNullOrEmpty(_accessToken) || DateTime.UtcNow >= _expiresAt)
await RefreshAsync();
return _accessToken;
}
public async Task GetAsync(string path)
{
var token = await EnsureTokenAsync();
var req = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{path}");
req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
var resp = await _http.SendAsync(req);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
await RefreshAsync();
req = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{path}");
req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
resp = await _http.SendAsync(req);
}
return await resp.Content.ReadAsStringAsync();
}
public static async Task Main()
{
var api = new SIMAPIClient("app_absensi_abc123", "sk_live_xxxxxxxxxxxxx", "https://api.anda.com");
Console.WriteLine(await api.GetAsync("/api/v1/pegawai?page=1&limit=10"));
}
}
Web Frontend (SPA)
client_secret di browser. Kode SPA yang dikirim ke browser bisa dilihat siapa saja. Gunakan backend proxy (lihat tab di bawah) yang menyimpan client_secret dan mem-proxy request ke SIMAPI.
import { useEffect, useState } from 'react';
// REACT — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Backend proxy menyimpan client_secret dan forward request ke SIMAPI.
function usePegawai() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/proxy/pegawai?page=1&limit=10') // backend proxy Anda
.then(r => {
if (!r.ok) throw new Error('Gagal fetch');
return r.json();
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, []);
return { data, loading, error };
}
function PegawaiList() {
const { data, loading, error } = usePegawai();
if (loading) return Memuat...;
if (error) return Error: {error.message};
return (
{data?.data?.map(p => (
- {p.nama} - {p.jabatan}
))}
);
}
export default PegawaiList;
<script setup>
import { ref, onMounted } from 'vue';
// VUE 3 — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
const pegawai = ref([]);
const loading = ref(true);
const error = ref(null);
onMounted(async () => {
try {
const resp = await fetch('/api/proxy/pegawai?page=1&limit=10'); // backend proxy
if (!resp.ok) throw new Error('Gagal fetch');
const result = await resp.json();
pegawai.value = result.data;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
});
</script>
<template>
Memuat...
Error: {{ error }}
-
{{ p.nama }} - {{ p.jabatan }}
</template>
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
// ANGULAR — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
@Injectable({ providedIn: 'root' })
export class PegawaiService {
private proxyUrl = '/api/proxy/pegawai'; // backend proxy Anda
constructor(private http: HttpClient) {}
listPegawai(page = 1, limit = 10): Observable {
return this.http.get(`${this.proxyUrl}?page=${page}&limit=${limit}`);
}
}
// Component
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-pegawai-list',
template: `
Memuat...
Error: {{ error }}
-
{{ p.nama }} - {{ p.jabatan }}
`
})
export class PegawaiListComponent implements OnInit {
pegawai: any[] = [];
loading = true;
error: string | null = null;
constructor(private service: PegawaiService) {}
ngOnInit() {
this.service.listPegawai().subscribe({
next: (res) => { this.pegawai = res.data; this.loading = false; },
error: (err) => { this.error = err.message; this.loading = false; }
});
}
}
# backend_proxy.py — WAJIB untuk web SPA & mobile
# Menyimpan client_secret, mem-proxy request ke SIMAPI.
# Frontend (React/Vue/Angular) hanya bicara dengan proxy ini.
import os
from datetime import datetime, timedelta
import requests
from flask import Flask, jsonify, request
app = Flask(__name__)
SIMAPI_BASE = os.getenv("SIMAPI_BASE", "https://api.anda.com")
CLIENT_ID = os.getenv("SIMAPI_CLIENT_ID")
CLIENT_SECRET = os.getenv("SIMAPI_CLIENT_SECRET")
# Cache token di memory
_token_cache = {"access_token": None, "refresh_token": None, "expires_at": None}
def _ensure_token():
if _token_cache["access_token"] and datetime.now() < _token_cache["expires_at"]:
return _token_cache["access_token"]
# Coba refresh dulu
if _token_cache["refresh_token"]:
resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
"refresh_token": _token_cache["refresh_token"],
"grant_type": "refresh_token",
})
if resp.status_code == 200:
data = resp.json()
_token_cache["access_token"] = data["access_token"]
_token_cache["refresh_token"] = data["refresh_token"]
_token_cache["expires_at"] = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
return _token_cache["access_token"]
# Login ulang
resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"grant_type": "client_credentials",
})
data = resp.json()
_token_cache["access_token"] = data["access_token"]
_token_cache["refresh_token"] = data["refresh_token"]
_token_cache["expires_at"] = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
return _token_cache["access_token"]
@app.route("/api/proxy/", methods=["GET", "POST", "PUT", "DELETE"])
def proxy(subpath):
token = _ensure_token()
headers = {"Authorization": f"Bearer {token}"}
url = f"{SIMAPI_BASE}/api/v1/{subpath}"
resp = requests.request(
method=request.method,
url=url,
headers=headers,
params=request.args,
json=request.get_json(silent=True) if request.method != "GET" else None,
)
# Kalau 401, force refresh & retry
if resp.status_code == 401:
_token_cache["refresh_token"] = None
token = _ensure_token()
headers["Authorization"] = f"Bearer {token}"
resp = requests.request(
method=request.method,
url=url,
headers=headers,
params=request.args,
json=request.get_json(silent=True) if request.method != "GET" else None,
)
return (resp.content, resp.status_code, {"Content-Type": "application/json"})
# ============ Endpoint khusus IoT device ============
# Device pakai X-Device-Key (bukan client_secret) untuk autentikasi.
DEVICE_KEYS = {
"device-abc123": "esp32-gudang-01",
"device-def456": "esp8266-suhu-02",
}
@app.route("/api/device/", methods=["GET"])
def device_proxy(subpath):
"""Endpoint khusus IoT device — auth pakai X-Device-Key."""
device_key = request.headers.get("X-Device-Key", "").strip()
if device_key not in DEVICE_KEYS:
return jsonify({"success": False, "error": "invalid_device_key"}), 401
token = _ensure_token()
url = f"{SIMAPI_BASE}/api/v1/{subpath}"
resp = requests.get(
url,
headers={"Authorization": f"Bearer {token}"},
params=request.args,
timeout=10,
)
if resp.status_code == 401:
_token_cache["refresh_token"] = None
token = _ensure_token()
resp = requests.get(
url,
headers={"Authorization": f"Bearer {token}"},
params=request.args,
timeout=10,
)
return (resp.content, resp.status_code, {"Content-Type": "application/json"})
if __name__ == "__main__":
app.run(port=8000)
Mobile Apps
client_secret di mobile. APK/IPA bisa dibongkar. Gunakan backend proxy (tab sebelumnya) atau minta admin membuatkan endpoint khusus per-user.
// Android — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Karena APK bisa dibongkar, client_secret TIDAK AMAN disimpan di mobile.
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// === Model ===
data class Pegawai(val id: Int, val nama: String, val jabatan: String?)
data class ApiMeta(val pagination: Pagination)
data class Pagination(val page: Int, val limit: Int, val total: Int, val total_pages: Int)
data class ApiResponse(
val success: Boolean,
val data: List,
val meta: ApiMeta?
)
// === Retrofit Interface ===
interface SIMAPIProxyService {
@GET("api/proxy/pegawai")
suspend fun listPegawai(
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 10
): ApiResponse
}
// === Client ===
object ApiClient {
private const val PROXY_BASE = "https://backend-anda.com/" // backend proxy Anda
val service: SIMAPIProxyService by lazy {
Retrofit.Builder()
.baseUrl(PROXY_BASE)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(SIMAPIProxyService::class.java)
}
}
// === Usage di ViewModel ===
class PegawaiViewModel : ViewModel() {
private val _pegawai = MutableLiveData>()
val pegawai: LiveData> = _pegawai
fun loadPegawai() {
viewModelScope.launch {
try {
val resp = withContext(Dispatchers.IO) {
ApiClient.service.listPegawai(page = 1, limit = 10)
}
_pegawai.value = resp.data
} catch (e: Exception) {
Log.e("SIMAPI", "Error: ${e.message}")
}
}
}
}
// iOS — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Karena IPA bisa dibongkar, client_secret TIDAK AMAN disimpan di iOS app.
import Foundation
// === Model ===
struct Pegawai: Codable {
let id: Int
let nama: String
let jabatan: String?
}
struct Pagination: Codable {
let page: Int
let limit: Int
let total: Int
let totalPages: Int
enum CodingKeys: String, CodingKey {
case page, limit, total
case totalPages = "total_pages"
}
}
struct ApiMeta: Codable {
let pagination: Pagination
}
struct ApiResponse: Codable {
let success: Bool
let data: [Pegawai]
let meta: ApiMeta?
}
// === Client ===
class SIMAPIProxyClient {
private let baseURL = "https://backend-anda.com" // backend proxy Anda
private let session = URLSession.shared
func listPegawai(page: Int = 1, limit: Int = 10) async throws -> [Pegawai] {
var components = URLComponents(string: "\(baseURL)/api/proxy/pegawai")!
components.queryItems = [
URLQueryItem(name: "page", value: "\(page)"),
URLQueryItem(name: "limit", value: "\(limit)"),
]
let (data, resp) = try await session.data(from: components.url!)
guard let http = resp as? HTTPURLResponse, http.statusCode == 200 else {
throw NSError(domain: "SIMAPI", code: -1,
userInfo: [NSLocalizedDescriptionKey: "Gagal fetch"])
}
let result = try JSONDecoder().decode(ApiResponse.self, from: data)
return result.data
}
}
// === Usage di SwiftUI ===
import SwiftUI
@MainActor
class PegawaiViewModel: ObservableObject {
@Published var pegawai: [Pegawai] = []
@Published var isLoading = false
@Published var errorMessage: String?
private let client = SIMAPIProxyClient()
func load() async {
isLoading = true
errorMessage = nil
do {
pegawai = try await client.listPegawai()
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
}
struct PegawaiListView: View {
@StateObject private var viewModel = PegawaiViewModel()
var body: some View {
NavigationView {
Group {
if viewModel.isLoading {
ProgressView("Memuat...")
} else if let err = viewModel.errorMessage {
Text("Error: \(err)")
} else {
List(viewModel.pegawai, id: \.id) { p in
VStack(alignment: .leading) {
Text(p.nama).font(.headline)
Text(p.jabatan ?? "-").font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
.navigationTitle("Pegawai")
}
.task { await viewModel.load() }
}
}
// Flutter — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Flutter app bisa di-decompile, client_secret TIDAK AMAN.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
// === Model ===
class Pegawai {
final int id;
final String nama;
final String? jabatan;
Pegawai({required this.id, required this.nama, this.jabatan});
factory Pegawai.fromJson(Map json) {
return Pegawai(
id: json['id'],
nama: json['nama'],
jabatan: json['jabatan'],
);
}
}
// === Client ===
class SIMAPIProxyClient {
static const String baseURL = 'https://backend-anda.com'; // backend proxy Anda
Future> listPegawai({int page = 1, int limit = 10}) async {
final uri = Uri.parse('$baseURL/api/proxy/pegawai')
.replace(queryParameters: {
'page': '$page',
'limit': '$limit',
});
final resp = await http.get(uri);
if (resp.statusCode != 200) {
throw Exception('Gagal fetch: ${resp.statusCode}');
}
final json = jsonDecode(resp.body) as Map;
final data = json['data'] as List;
return data.map((e) => Pegawai.fromJson(e)).toList();
}
}
// === Widget ===
class PegawaiListPage extends StatefulWidget {
@override
_PegawaiListPageState createState() => _PegawaiListPageState();
}
class _PegawaiListPageState extends State {
final _client = SIMAPIProxyClient();
late Future> _future;
@override
void initState() {
super.initState();
_future = _client.listPegawai();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Pegawai')),
body: FutureBuilder>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
}
final list = snapshot.data ?? [];
return ListView.builder(
itemCount: list.length,
itemBuilder: (context, i) {
final p = list[i];
return ListTile(
title: Text(p.nama),
subtitle: Text(p.jabatan ?? '-'),
);
},
);
},
),
);
}
}
// React Native — panggil backend proxy Anda sendiri (BUKAN SIMAPI langsung)
// Bundle JS bisa dibongkar, client_secret TIDAK AMAN.
import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
const PROXY_BASE = 'https://backend-anda.com'; // backend proxy Anda
export default function PegawaiListScreen() {
const [pegawai, setPegawai] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
(async () => {
try {
const resp = await fetch(`${PROXY_BASE}/api/proxy/pegawai?page=1&limit=10`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
setPegawai(json.data);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
})();
}, []);
if (loading) {
return (
Memuat...
);
}
if (error) {
return (
Error: {error}
);
}
return (
String(item.id)}
renderItem={({ item }) => (
{item.nama}
{item.jabatan ?? '-'}
)}
/>
);
}
const styles = StyleSheet.create({
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
error: { color: 'red' },
item: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
title: { fontSize: 16, fontWeight: 'bold' },
subtitle: { fontSize: 14, color: '#666' },
});
Desktop
client_secret di backend app-nya (bukan di UI). Tapi lebih aman pakai backend proxy — sama seperti mobile.
// Electron main process — client_secret aman di sini
// Renderer (UI) hanya komunikasi via IPC ke main process.
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const SIMAPI_BASE = 'https://api.anda.com';
const CLIENT_ID = process.env.SIMAPI_CLIENT_ID;
const CLIENT_SECRET = process.env.SIMAPI_CLIENT_SECRET;
let tokenCache = { access: null, refresh: null, expiresAt: 0 };
async function ensureToken() {
if (tokenCache.access && Date.now() < tokenCache.expiresAt) {
return tokenCache.access;
}
if (tokenCache.refresh) {
const resp = await fetch(`${SIMAPI_BASE}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: tokenCache.refresh,
grant_type: 'refresh_token',
}),
});
if (resp.ok) {
const data = await resp.json();
tokenCache.access = data.access_token;
tokenCache.refresh = data.refresh_token;
tokenCache.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
return tokenCache.access;
}
}
const resp = await fetch(`${SIMAPI_BASE}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: 'client_credentials',
}),
});
const data = await resp.json();
tokenCache.access = data.access_token;
tokenCache.refresh = data.refresh_token;
tokenCache.expiresAt = Date.now() + (data.expires_in - 60) * 1000;
return tokenCache.access;
}
ipcMain.handle('simapi:get', async (event, path) => {
const token = await ensureToken();
let resp = await fetch(`${SIMAPI_BASE}${path}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (resp.status === 401) {
tokenCache.refresh = null;
const token2 = await ensureToken();
resp = await fetch(`${SIMAPI_BASE}${path}`, {
headers: { Authorization: `Bearer ${token2}` },
});
}
return await resp.json();
});
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
# Python Desktop (Tkinter / PyQt) — client_secret aman di app
# Karena app desktop tidak dikirim ke user (hanya executable), secret relatif aman.
import tkinter as tk
from tkinter import ttk
import threading
from datetime import datetime, timedelta
import requests
SIMAPI_BASE = "https://api.anda.com"
CLIENT_ID = "app_absensi_abc123"
CLIENT_SECRET = "sk_live_xxxxxxxxxxxxx"
class SIMAPIClient:
def __init__(self):
self.access_token = None
self.refresh_token = None
self.expires_at = None
def _login(self):
resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"grant_type": "client_credentials",
})
data = resp.json()
self.access_token = data["access_token"]
self.refresh_token = data["refresh_token"]
self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
def _refresh(self):
if not self.refresh_token:
return self._login()
resp = requests.post(f"{SIMAPI_BASE}/oauth/token", json={
"refresh_token": self.refresh_token,
"grant_type": "refresh_token",
})
if resp.status_code != 200:
return self._login()
data = resp.json()
self.access_token = data["access_token"]
self.refresh_token = data["refresh_token"]
self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"] - 60)
def _ensure_token(self):
if not self.access_token or datetime.now() >= self.expires_at:
self._refresh()
return self.access_token
def get_pegawai(self):
headers = {"Authorization": f"Bearer {self._ensure_token()}"}
resp = requests.get(f"{SIMAPI_BASE}/api/v1/pegawai", headers=headers,
params={"page": 1, "limit": 10})
if resp.status_code == 401:
self._refresh()
headers["Authorization"] = f"Bearer {self._ensure_token()}"
resp = requests.get(f"{SIMAPI_BASE}/api/v1/pegawai", headers=headers,
params={"page": 1, "limit": 10})
return resp.json()
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("SIMAPI — Daftar Pegawai")
self.geometry("600x400")
self.api = SIMAPIClient()
self.tree = ttk.Treeview(self, columns=("id", "nama", "jabatan"),
show="headings")
self.tree.heading("id", text="ID")
self.tree.heading("nama", text="Nama")
self.tree.heading("jabatan", text="Jabatan")
self.tree.pack(fill="both", expand=True, padx=10, pady=10)
btn = ttk.Button(self, text="Muat Data", command=self.load_data)
btn.pack(pady=5)
def load_data(self):
threading.Thread(target=self._load, daemon=True).start()
def _load(self):
try:
data = self.api.get_pegawai()
self.tree.delete(*self.tree.get_children())
for p in data["data"]:
self.tree.insert("", "end",
values=(p["id"], p["nama"], p.get("jabatan", "-")))
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
App().mainloop()
IoT / Embedded
SIMAPI bisa dipakai dari ESP32 dan ESP8266 untuk membaca data atau mengirim sensor ke database.
Perbandingan Board
| Board | WiFi | RAM | Bisa HTTPS? | Rekomendasi |
|---|---|---|---|---|
| ESP32 | ✅ Built-in | ~320 KB | ✅ Ya | ⭐ Terbaik |
| ESP8266 | ✅ Built-in | ~80 KB | ⚠️ Berat | ✅ Bisa (dengan hati-hati) |
| Arduino Uno | ❌ Butuh shield | 2 KB | ❌ Tidak realistis | ❌ Tidak disarankan |
| Arduino Mega | ❌ Butuh shield | 8 KB | ⚠️ Sulit | ❌ Tidak disarankan |
| Raspberry Pi | ✅ Built-in | 1–8 GB | ✅ Ya | ⭐ Terbaik (kalau perlu komputasi) |
client_secret di firmware Arduino/ESP! Firmware bisa di-extract dari flash memory dengan esptool.py read_flash. Gunakan backend proxy dengan device key sederhana (contoh di bawah).
Pendekatan yang Direkomendasikan: Backend Proxy
ESP32 cukup bicara HTTP ke backend proxy Anda, yang menyimpan client_secret dengan aman.
┌─────────────┐ HTTP ┌──────────────────┐ HTTPS ┌─────────┐
│ ESP32 │ ────────────▶ │ Backend Proxy │ ────────────▶ │ SIMAPI │
│ (sensor) │ X-Device-Key │ (Flask/Node.js) │ Bearer token │ │
└─────────────┘ └──────────────────┘ └─────────┘
di lapangan di server Anda di server
Firmware ESP32 yang mengirim data sensor ke backend proxy. Ringan (~30 KB RAM), tidak perlu HTTPS handshake.
// esp32_simapi_sensor.ino — Kirim data sensor ke SIMAPI via backend proxy
#include
#include
#include
// === Konfigurasi ===
const char* WIFI_SSID = "WifiKantor";
const char* WIFI_PASSWORD = "password-wifi";
// Backend proxy (BUKAN SIMAPI langsung — secret aman di server)
const char* PROXY_BASE = "http://192.168.1.100:8000"; // backend proxy Anda
const char* DEVICE_KEY = "device-abc123"; // device key sederhana
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Menghubungkan WiFi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi terhubung!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi gagal.");
}
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
delay(5000);
return;
}
// Baca sensor (contoh: suhu)
float suhu = random(200, 350) / 10.0; // 20.0 – 35.0 °C
float kelembaban = random(400, 800) / 10.0;
// Kirim ke backend proxy
sendSensorData(suhu, kelembaban);
delay(30000); // kirim tiap 30 detik
}
void sendSensorData(float suhu, float kelembaban) {
HTTPClient http;
String url = String(PROXY_BASE) + "/api/device/sensor_suhu";
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-Device-Key", DEVICE_KEY);
StaticJsonDocument<256> doc;
doc["device_id"] = "esp32-gudang-01";
doc["suhu"] = suhu;
doc["kelembaban"] = kelembaban;
String body;
serializeJson(doc, body);
Serial.println("POST " + url);
Serial.println("Body: " + body);
int httpCode = http.POST(body);
if (httpCode > 0) {
String response = http.getString();
Serial.printf("HTTP %d: %s\n", httpCode, response.c_str());
} else {
Serial.printf("Error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
# Tambahkan endpoint ini di backend_proxy.py (Flask)
DEVICE_KEYS = {
"device-abc123": "esp32-gudang-01",
"device-def456": "esp8266-suhu-02",
}
@app.route("/api/device/<path:subpath>", methods=["GET", "POST"])
def device_proxy(subpath):
"""
Endpoint khusus IoT device.
Autentikasi pakai X-Device-Key (sederhana, tidak perlu OAuth penuh).
"""
device_key = request.headers.get("X-Device-Key", "").strip()
if device_key not in DEVICE_KEYS:
return jsonify({"success": False, "error": "invalid_device_key"}), 401
token = _ensure_token()
url = f"{SIMAPI_BASE}/api/v1/{subpath}"
resp = requests.request(
method=request.method,
url=url,
headers={"Authorization": f"Bearer {token}"},
params=request.args,
json=request.get_json(silent=True) if request.method != "GET" else None,
timeout=10,
)
if resp.status_code == 401:
_token_cache["refresh_token"] = None
token = _ensure_token()
resp = requests.request(
method=request.method,
url=url,
headers={"Authorization": f"Bearer {token}"},
params=request.args,
json=request.get_json(silent=True) if request.method != "GET" else None,
timeout=10,
)
return (resp.content, resp.status_code, {"Content-Type": "application/json"})
client_secret, tidak perlu HTTPS handshake (ringan), dan mudah ganti device key tanpa update firmware semua device.client_secret ada di firmware dan bisa di-extract dengan esptool.py read_flash. Hanya untuk prototype/hobby. Kalau tetap dipakai, wajib aktifkan flash encryption + secure boot di ESP32.
// esp32_simapi_direct.ino — ESP32 → SIMAPI langsung (OAuth + HTTPS)
// ⚠️ TIDAK DIREKOMENDASIKAN untuk production.
#include
#include
#include
#include
const char* WIFI_SSID = "WifiKantor";
const char* WIFI_PASSWORD = "password-wifi";
const char* SIMAPI_BASE = "https://api.anda.com";
const char* CLIENT_ID = "esp32_sensor_abc123";
const char* CLIENT_SECRET = "sk_live_xxxxxxxxxxxxx"; // ⚠️ JANGAN di production!
String accessToken = "";
String refreshToken = "";
unsigned long tokenExpiresAt = 0;
// Root CA (ganti dengan CA server Anda — bisa pakai CA Let's Encrypt)
const char* ROOT_CA = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\n" \
"... isi dengan CA certificate server Anda ...\n" \
"-----END CERTIFICATE-----\n";
void setup() {
Serial.begin(115200);
delay(1000);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi OK");
login();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) { delay(5000); return; }
// Refresh token kalau hampir expired (60 detik sebelum)
if (millis() > tokenExpiresAt - 60000) {
refreshAccessToken();
}
fetchPegawai();
delay(60000);
}
void login() {
WiFiClientSecure client;
client.setCACert(ROOT_CA); // verifikasi sertifikat server (WAJIB)
HTTPClient http;
String url = String(SIMAPI_BASE) + "/oauth/token";
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
StaticJsonDocument<256> req;
req["client_id"] = CLIENT_ID;
req["client_secret"] = CLIENT_SECRET;
req["grant_type"] = "client_credentials";
String body;
serializeJson(req, body);
int httpCode = http.POST(body);
if (httpCode == 200) {
String response = http.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, response);
accessToken = doc["access_token"].as();
refreshToken = doc["refresh_token"].as();
int expiresIn = doc["expires_in"].as();
tokenExpiresAt = millis() + (expiresIn * 1000UL);
Serial.println("Login OK, expires in " + String(expiresIn) + "s");
} else {
Serial.printf("Login gagal: HTTP %d\n", httpCode);
}
http.end();
}
void refreshAccessToken() {
if (refreshToken.isEmpty()) {
login();
return;
}
WiFiClientSecure client;
client.setCACert(ROOT_CA);
HTTPClient http;
String url = String(SIMAPI_BASE) + "/oauth/token";
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
StaticJsonDocument<256> req;
req["refresh_token"] = refreshToken;
req["grant_type"] = "refresh_token";
String body;
serializeJson(req, body);
int httpCode = http.POST(body);
if (httpCode == 200) {
String response = http.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, response);
accessToken = doc["access_token"].as();
refreshToken = doc["refresh_token"].as();
int expiresIn = doc["expires_in"].as();
tokenExpiresAt = millis() + (expiresIn * 1000UL);
Serial.println("Token refreshed");
} else {
Serial.printf("Refresh gagal: HTTP %d, fallback login\n", httpCode);
login();
}
http.end();
}
void fetchPegawai() {
WiFiClientSecure client;
client.setCACert(ROOT_CA);
HTTPClient http;
String url = String(SIMAPI_BASE) + "/api/v1/pegawai?page=1&limit=5";
http.begin(client, url);
http.addHeader("Authorization", "Bearer " + accessToken);
int httpCode = http.GET();
if (httpCode == 200) {
String response = http.getString();
DynamicJsonDocument doc(4096);
deserializeJson(doc, response);
JsonArray data = doc["data"].as();
Serial.println("Dapat " + String(data.size()) + " pegawai:");
for (JsonObject p : data) {
String nama = p["nama"].as();
String jabatan = p["jabatan"].as();
Serial.println("- " + nama + " (" + jabatan + ")");
}
} else {
Serial.printf("Fetch gagal: HTTP %d\n", httpCode);
}
http.end();
}
- Aktifkan HTTPS dengan
setCACert() - Aktifkan flash encryption di ESP32
- Aktifkan secure boot (mencegah firmware dimodifikasi)
- Set client ESP32 dengan access token pendek (15 menit) & rate limit ketat (10/menit)
- Simpan
refresh_tokendi NVS (bukan di RAM — supaya tidak hilang saat restart)
Error & Rate Limit
Kode Error
| HTTP | Code | Artinya | Solusi |
|---|---|---|---|
| 401 | invalid_client | Client ID / secret salah | Cek kredensial |
| 401 | invalid_token | Access token expired | Refresh token / login ulang |
| 401 | invalid_grant | Refresh token expired / reuse | Login ulang |
| 403 | insufficient_scope | Scope kurang | Hubungi admin |
| 404 | resource_not_found | Resource tidak ada | Cek endpoint |
| 429 | rate_limit_exceeded | Rate limit habis | Retry-After header |
| 500 | internal_error | Server error | Hubungi admin |
Rate Limit
Cek header response:
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 198
X-RateLimit-Reset: 60
X-RateLimit-Window: minute
Kalau kena rate limit:
{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded (minute). Limit: 200. Retry in 45s.",
"retry_after": 45
}
}
FAQ — Tanya Jawab
Bisa, tapi dengan catatan:
- ESP32 — ✅ paling cocok (RAM 320 KB, HTTPS support)
- ESP8266 — ⚠️ bisa, tapi HTTPS sering gagal (RAM 80 KB)
- Arduino Uno/Mega — ❌ tidak realistis (RAM 2–8 KB, tidak cukup untuk HTTPS + JWT)
- Raspberry Pi — ✅ sangat cocok (full Linux, RAM GB)
Cara terbaik untuk ESP32: pakai backend proxy dengan device key sederhana — bukan simpan client_secret di firmware. Contoh kode lengkap ada di section IoT / Embedded.
Firmware ESP32 bisa di-extract dari flash memory dengan perintah:
esptool.py --port /dev/ttyUSB0 read_flash 0x0 0x400000 firmware.bin
strings firmware.bin | grep "sk_live_"
Kalau client_secret ada di firmware, penyerang bisa mengambilnya dan mengakses SIMAPI atas nama device Anda.
Solusi: simpan client_secret di backend proxy (server), ESP32 cukup kirim X-Device-Key (string acak sederhana, bisa di-revoke tanpa update firmware).
Refresh token adalah token khusus untuk memperbarui access token tanpa perlu login ulang (tanpa kirim client_secret).
- Access token — default 1 jam, untuk akses API
- Refresh token — default 30 hari, untuk refresh access token
Setiap kali refresh, server mengembalikan refresh token baru (rotation). Token lama otomatis di-revoke. Kalau token lama dipakai lagi, semua token client di-revoke (reuse detection).
Jangan simpan client_secret di mobile app. APK/IPA bisa dibongkar, sehingga secret bisa dicuri.
Solusi: pakai backend proxy.
- Buat backend kecil (Flask/Node.js) di server Anda
- Backend menyimpan
client_secretdi environment variable - Backend proxy request ke SIMAPI
- Mobile app hanya bicara ke backend proxy Anda
Contoh kode backend proxy ada di tab "Backend Proxy (WAJIB)" di section Contoh Penggunaan.
Bisa, tapi dengan backend proxy. Kode SPA dikirim ke browser, jadi siapapun bisa lihat source code — client_secret akan terlihat.
Pola yang benar:
Browser (React/Vue)
↓ fetch('/api/proxy/...')
Backend Proxy (Flask/Node.js)
↓ Authorization: Bearer ...
SIMAPI
Frontend hanya bicara ke backend proxy Anda, tidak langsung ke SIMAPI.
SIMAPI mendukung semua platform yang bisa kirim HTTP request:
- Backend — cURL, Python, Node.js, PHP, Go, Java, C# (.NET), Ruby, Rust, dll.
- Web frontend — React, Vue, Angular, Svelte (dengan backend proxy)
- Mobile — Android (Kotlin/Java), iOS (Swift), Flutter, React Native, Xamarin (dengan backend proxy)
- Desktop — Electron, .NET WPF/WinForms, JavaFX, Python Tkinter/PyQt
- IoT — ESP32, ESP8266, Raspberry Pi (dengan backend proxy untuk keamanan)
Contoh kode lengkap ada di section Contoh Penggunaan & IoT / Embedded.
Token rotation adalah praktik keamanan standar OAuth2. Setiap kali refresh token dipakai, server membuat token baru dan merevoke yang lama.
- Kalau refresh token bocor, penyerang hanya bisa pakai sekali
- Kalau penyerang pakai token lama → terdeteksi (reuse detection) → semua token client di-revoke otomatis
- Server bisa melacak siapa yang masih aktif
Ya! Setiap client bisa punya durasi token berbeda dari default:
- Access token — default 60 menit, bisa diubah 5–1440 menit
- Refresh token — default 30 hari, bisa diubah 1–365 hari
Contoh:
- Aplikasi backend internal → access 60 menit, refresh 30 hari
- Aplikasi mobile → access 15 menit, refresh 7 hari (lebih aman)
- Dashboard publik → access 5 menit, refresh 1 hari (paling aman)
- ESP32 / IoT → access 15 menit, refresh 1 hari (paling aman untuk device)
SIMAPI v2.0 mendukung 6 jenis database:
- MySQL (siap pakai, port 3306)
- MariaDB (siap pakai, port 3306)
- SQLite (siap pakai, file .db)
- PostgreSQL (perlu install
psycopg2-binary, port 5432) - Oracle (perlu install
oracledb, port 1521) - SQL Server (perlu install
pyodbc+ ODBC Driver, port 1433)
Hubungi admin SIMAPI di organisasi Anda. Admin akan mendaftarkan aplikasi Anda dan memberikan kredensial melalui channel aman (password manager / file terenkripsi). Secret hanya ditampilkan sekali.
Field di SIMAPI punya 3 level proteksi:
- Normal — bisa diakses dengan scope
read:basic - Sensitive — butuh scope
read:sensitive(contoh: gaji, NIK) - Protected — tidak pernah muncul di API (contoh: password_hash)
Kalau butuh akses sensitive, hubungi admin untuk menambah scope.
Segera hubungi admin SIMAPI. Admin akan:
- Revoke semua secret client Anda
- Revoke semua refresh token aktif
- Generate secret baru
- Kirim secret baru melalui channel aman
Update aplikasi Anda dengan secret baru, restart, dan login ulang.
Ya. Setiap client punya rate limit yang diatur admin:
- Limit per menit (default 60)
- Limit per jam (default 1000)
- Kuota harian (default 10000)
Kalau butuh lebih tinggi, hubungi admin.