Sauth FrontEnd
HTTP client dengan auto-token lifecycle dan permission utilities untuk sauth ecosystem.
Versi: 0.3.1 Changelog
Kategori: Pure Utils
TL;DR
Library ini menyediakan dua hal: (1) HTTP client (RequestInstance) yang menangani token lifecycle secara otomatis — mendapatkan, menyimpan, dan me-refresh access token dari sauth ecosystem; (2) permission utilities untuk mengecek izin user berbasis Inertia PageProps atau JWT token langsung.
Exports:
// Entry point utama
import { RequestInstance, ServiceCodes } from '@bpmlib/sauth-frontend'
import type { RequestOptions, SauthTokenClaims, RouteDict, RouteParams } from '@bpmlib/sauth-frontend'
// Sub-path exports
import RequestInstance from '@bpmlib/sauth-frontend/http'
import { getUserPermissions, permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { decodeTokenClaims, getPermissionsFromToken, permissionCheckFromToken, isFirstPartyToken, isThirdPartyToken, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'TIP
NEW v0.3.0: isFirstPartyToken, isThirdPartyToken, isM2MToken tersedia di auth/token.
Installation & Setup
Requirements
- Node.js 18+
- TypeScript 5.0+
Peer Dependencies
| Dependency | Versi | Status | Deskripsi |
|---|---|---|---|
axios | ^1.13.0 | Required | HTTP client |
@inertiajs/core | ^2.0.0 | Optional | Diperlukan untuk auth/inertia utilities |
npm install axios
# Jika menggunakan auth/inertia:
npm install @inertiajs/corePackage Installation
Library dipublish ke private registry. Konfigurasi .npmrc terlebih dahulu:
# .npmrc
@bpmlib:registry=https://js.pkg.ppsdmmigas.id/npm install @bpmlib/sauth-frontendyarn add @bpmlib/sauth-frontendpnpm add @bpmlib/sauth-frontendbun add @bpmlib/sauth-frontendImport
Library menyediakan beberapa entry points — gunakan sub-path import untuk bundle lebih ringan:
// Semua dari entry point utama
import { RequestInstance, getUserPermissions, permissionCheck, ServiceCodes } from '@bpmlib/sauth-frontend'
// Sub-path (lebih ringan jika hanya butuh satu fitur)
import RequestInstance from '@bpmlib/sauth-frontend/http'
import { getUserPermissions, permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { decodeTokenClaims, permissionCheckFromToken, isFirstPartyToken, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'Quick Start
Session Mode (Inertia/Breeze)
Untuk aplikasi Laravel + Inertia.js. Token diperoleh otomatis dari ticketbooth setiap kali dibutuhkan — tidak perlu konfigurasi khusus.
// api/gwa.ts
import RequestInstance from '@bpmlib/sauth-frontend/http'
import routes from './gwa-routes.json'
const gwaApi = new RequestInstance(
'https://gwa.ppsdmmigas.id',
routes,
'gwa'
// mode: 'session' adalah default
)
// Token dikelola otomatis
const { data } = await gwaApi.get('training.index')
const { data: detail } = await gwaApi.get('training.show', 123)Permission Check (Inertia)
import { permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3' // atau @inertiajs/react
const page = usePage()
// Cek satu permission
if (permissionCheck('training.read', page)) {
// tampilkan fitur
}
// OR logic — true jika salah satu terpenuhi
if (permissionCheck(['training.read', 'training.write'], page)) {
// tampilkan fitur
}Key Points:
pageadalah Inertia page object dariusePage()framework adapter masing-masingpermissionCheckreturntruejika user adalah webmaster (fet === 'wm')
Configuration
RequestInstance dikonfigurasi melalui parameter options keempat (tipe RequestOptions):
new RequestInstance(baseURL, routes, service, {
mode: 'pkce',
ticketboothUrl: '/api/sauth/token',
accessTokenPrefix: 'tkac_',
tokenTtl: 720,
refreshTokenTtl: 604800,
refreshUrl: '/oauth/token',
clientId: 'my-client-id',
onAuthFailure: () => router.push('/login'),
})Available Options
| Option | Type | Default | Description |
|---|---|---|---|
mode | 'session' | 'pkce' | 'session' | Strategi token lifecycle Lihat selengkapnya |
ticketboothUrl | string | '/api/sauth/token' | Endpoint ticketbooth (session mode) |
accessTokenPrefix | string | 'tkac_' | Prefix nama cookie access token |
tokenTtl | number | 720 | Fallback TTL dalam detik saat expires_in tidak ada di response |
refreshTokenTtl | number | 604800 | TTL cookie refresh token dalam detik (pkce mode) Lihat selengkapnya |
refreshUrl | string | '/oauth/token' | Endpoint OAuth2 refresh grant (pkce mode) |
clientId | string | - | OAuth2 client_id untuk refresh grant (pkce mode) |
onAuthFailure | () => void | - | Dipanggil saat pkce refresh gagal Lihat selengkapnya |
mode
Menentukan strategi token lifecycle yang digunakan instance.
Values:
'session'— Tidak ada refresh token. Saat access cookie tidak ada atau 401, library POST ke ticketbooth untuk token baru. Cocok untuk aplikasi Inertia/Breeze di mana session PHP menjaga autentikasi.'pkce'— Menggunakan access + refresh token. Token di-refresh via OAuth2 refresh grant saat access cookie tidak ada atau 401. Cocok untuk SPA standalone.
Contoh:
// Session mode — default untuk Inertia apps
new RequestInstance(baseURL, routes, 'gwa', { mode: 'session' })
// PKCE mode — untuk standalone SPA
new RequestInstance(baseURL, routes, 'gwc', {
mode: 'pkce',
clientId: 'gwc-spa',
onAuthFailure: () => window.location.href = '/login',
})onAuthFailure
Callback yang dipanggil ketika refresh token gagal di pkce mode — biasanya karena refresh token expired atau tidak valid.
Signature:
onAuthFailure: () => voidContoh:
import router from './router'
new RequestInstance(baseURL, routes, 'gwc', {
mode: 'pkce',
onAuthFailure: () => router.push({ name: 'login' }),
})Use Case: Gunakan untuk redirect user ke halaman login ketika sesi berakhir. Library sudah menghapus cookies access dan refresh sebelum callback ini dipanggil.
refreshTokenTtl
TTL cookie refresh token dalam detik (pkce mode only). Default 604800 = 7 hari, sesuai default sauth-server.token_ttl.refresh.
Contoh:
new RequestInstance(baseURL, routes, 'svc', {
mode: 'pkce',
refreshTokenTtl: 86400, // 1 hari — sesuaikan dengan SAUTH_REFRESH_TOKEN_TTL server
})Tidak ada aturan 80% untuk nilai ini — TTL penuh digunakan. Server menolak refresh token expired secara eksplisit.
Core Concepts
Token Lifecycle: Session vs PKCE
Library mengelola dua strategi secara otomatis via Axios interceptor:
Session mode (mode: 'session'):
- Request keluar → cek access cookie → ada? attach Bearer → kirim
- Tidak ada? → POST ticketbooth
{ target: service }→ simpan access cookie (80% TTL dariexpires_in) → attach Bearer - Dapat 401? → hapus access cookie → ulangi lewat ticketbooth → retry sekali
PKCE mode (mode: 'pkce'):
- Request keluar → cek access cookie → ada? attach Bearer → kirim
- Tidak ada? → POST refresh grant → simpan access + refresh cookie baru → attach Bearer
- Dapat 401? → POST refresh grant → retry sekali. Gagal? → hapus kedua cookie → panggil
onAuthFailure
WARNING
Interceptor hanya retry sekali — tidak ada loop. 401 kedua langsung reject.
Cookie Naming Convention
| Cookie | Nama | TTL |
|---|---|---|
| Access token | tkac_{service} | 80% dari expires_in ticketbooth |
| Refresh token | tkrf_{service} | refreshTokenTtl option (default 604800s = 7 hari) |
Prefix tkac_ dapat diganti via accessTokenPrefix. Prefix tkrf_ tidak bisa diganti — hardcoded untuk interoperabilitas antar app dalam satu ekosistem.
Contoh: service 'gwa' → cookie tkac_gwa (access) dan tkrf_gwa (refresh).
WARNING
Upgrade dari v0.1.0: Cookie prefix berubah (tkaac_ → tkac_, tkarf_ → tkrf_). Cookie lama tidak lagi dibaca setelah upgrade — user akan melakukan satu request ekstra untuk re-issue token. Tidak ada data loss.
Wildcard Permission Matching & OR Logic
Permission check menggunakan dua mekanisme:
Wildcard (*): Pattern dibangun dari entry fet user, lalu ditest terhadap permission yang dibutuhkan. user.* di fet → /^user\..*$/ → cocok dengan user.read. Tapi user.read di fet tidak cocok dengan user.* sebagai needed — arah wildcard tidak bisa dibalik.
OR logic: permissionCheck(['perm.a', 'perm.b']) → true jika ada satu pasang (fet_entry × needed_entry) yang cocok.
Special case: fet === 'wm' (webmaster) → selalu return true, melewati semua cek.
// User dengan fet: ['training.*', 'exam.read']
permissionCheck('training.write', page) // true — wildcard match
permissionCheck('exam.read', page) // true — exact match
permissionCheck('exam.write', page) // false
permissionCheck(['exam.write', 'exam.read'], page) // true — OR: exam.read cocok
permissionCheck('training.*', page) // false — arah wildcard terbalikLogic yang sama berlaku di permissionCheckFromToken() untuk JWT-based checks.
API Reference
Classes
RequestInstance
HTTP client dengan token lifecycle otomatis. Dapat di-extend untuk behavior custom (contoh: override refreshingToken() untuk non-OAuth2 endpoint).
Constructor
new RequestInstance<TRoutes>(
baseURL: string,
routes: RouteDict,
service: string,
options?: RequestOptions
)Parameters
| Name | Type | Default | Description |
|---|---|---|---|
baseURL | string | - | Base URL resource server |
routes | RouteDict | - | Route dictionary berdasarkan HTTP method |
service | string | - | Service code — digunakan sebagai suffix nama cookie |
options | RequestOptions | {} | Konfigurasi token lifecycle |
Methods
Contains:
- Cookie Helpers
- Token Management
- Request Configuration
- Route Helpers
- HTTP Methods
- Protected (untuk Subclass)
Cookie Helpers
setCookie()
setCookie(name: string, value: string, lifetimeSeconds?: number): voidSet browser cookie. HTTPS: SameSite=None; Secure. HTTP: SameSite=Lax. Tanpa lifetimeSeconds → session cookie (hilang saat tab ditutup).
getCookie()
getCookie(name: string): string | nullBaca cookie berdasarkan nama. Return null jika tidak ditemukan.
eraseCookie()
eraseCookie(name: string): voidHapus cookie berdasarkan nama.
getAccessTk()
getAccessTk(): string | nullBaca access token cookie untuk service ini (tkac_{service}).
getRefreshTk()
getRefreshTk(): string | nullBaca refresh token cookie untuk service ini (tkrf_{service}).
Token Management
storeTokens()
storeTokens(accessToken: string, refreshToken: string, expiresIn: number): voidSimpan access dan refresh token setelah PKCE exchange. Access cookie TTL = 80% dari expiresIn. Refresh cookie TTL = refreshTokenTtl option (default 604800s = 7 hari).
NOTE
Tidak ada aturan 80% untuk refresh token — TTL penuh digunakan. Server menolak refresh token expired secara eksplisit; onAuthFailure menangani kasus itu.
Request Configuration
Method-method ini bisa di-chain sebelum request. Konfigurasi di-reset otomatis setelah setiap HTTP call.
setBody()
setBody(data: Record<string, unknown> | FormData, asFormData?: boolean): thisSet request body. asFormData: true → Content-Type: multipart/form-data, auto-convert JSON ke FormData (mendukung File, FileList, Date, nested objects, dan arrays).
setHeader()
setHeader(key: string, value: string): thisSet satu request header. Content-Type via setBody() tidak bisa di-override.
setHeaders()
setHeaders(objHeader: Record<string, string>): thisSet beberapa request header sekaligus.
setUrlParam()
setUrlParam(key: string, value: string | number | boolean): thisSet satu URL query parameter.
setUrlParams()
setUrlParams(paramsObj: Record<string, string | number | boolean>): thisSet beberapa URL query parameter sekaligus.
setResponseType()
setResponseType(type?: ResponseType): thisSet response type Axios. Default: 'json'. Nilai lain: 'blob', 'arraybuffer', 'document', 'text', 'stream'.
getConfig()
getConfig(): AxiosRequestConfigKembalikan konfigurasi request saat ini sebagai AxiosRequestConfig.
Route Helpers
useUrl()
useUrl<TRoute extends string>(
routeName: string,
method?: HttpMethod,
...params: RouteParams<TRoute>
): stringResolve nama route ke URL penuh, mengganti path parameters. Melempar error jika parameter wajib tidak disertakan.
| Name | Type | Default | Description |
|---|---|---|---|
routeName | string | - | Nama key di route dictionary |
method | HttpMethod | 'get' | HTTP method untuk lookup |
...params | RouteParams | [] | Path params — positional, named object, atau spread |
Returns: string — URL dengan parameter yang sudah di-substitute
// Route: { get: { 'training.show': '/training/{id}' } }
api.useUrl('training.show', 'get', 123) // '/training/123'
api.useUrl('training.show', 'get', { id: 123 }) // '/training/123'setUrl()
setUrl(method: string, routeName: string, ...routeParam: RouteParams<TRoute>): stringAlias useUrl() dengan urutan argumen berbeda (method pertama). Digunakan internal oleh semua HTTP methods.
HTTP Methods
Semua method HTTP me-reset konfigurasi request setelah dipanggil. Generic T adalah tipe response data.
get() / delete()
get<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
delete<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>post() / put() / patch()
post<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
put<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>
patch<T>(routeName: TRoute, ...params: RouteParams): Promise<AxiosResponse<T>>Body dikirim via setBody() sebelum memanggil method ini.
const { data } = await api.setBody({ name: 'Training Baru' }).post('training.store')
const { data: list } = await api.setUrlParam('page', 2).get('training.index')Protected (untuk Subclass)
Method berikut protected — dapat di-override di subclass untuk behavior custom.
issueToken()
protected async issueToken(): Promise<string>POST ke ticketbooth { target: service }, simpan access cookie di 80% TTL, return token string. Digunakan session mode.
refreshingToken()
protected async refreshingToken(): Promise<void>Refresh token sesuai mode aktif. Session: erase access cookie + reissue via ticketbooth. PKCE: OAuth2 refresh grant + store kedua cookie baru. Override untuk logika refresh non-standard.
Functions
auth/inertia
getUserPermissions()
Ambil permissions user dari Inertia PageProps (auth.user.fet).
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
page | Page | - | Inertia page object — pass usePage() dari framework adapter |
ignoreWm | boolean | false | Jika true, return raw fet meski user adalah webmaster |
Returns: 'wm' untuk webmaster | string[] untuk user normal | null jika tidak terautentikasi
import { getUserPermissions } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3'
const perms = getUserPermissions(usePage())
// 'wm' | string[] | nullpermissionCheck()
Cek apakah user memiliki minimal satu dari permissions yang dibutuhkan.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
needed | string | string[] | - | Permission yang dicek — OR logic jika array |
page | Page | - | Inertia page object |
Returns: boolean
auth/token
decodeTokenClaims()
Decode payload JWT tanpa verifikasi signature. Verifikasi signature dilakukan server-side oleh sauth-client.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
token | string | - | Raw JWT string |
Returns: SauthTokenClaims | null — null jika token malformed
getPermissionsFromToken()
Ekstrak klaim fet dari JWT string.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
token | string | - | Raw JWT string |
Returns: 'wm' | string[] | null
permissionCheckFromToken()
Cek permission dari JWT string. Logic identik dengan permissionCheck() — wildcard dan OR logic berlaku sama.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
needed | string | string[] | - | Permission yang dicek |
token | string | - | Raw JWT string |
Returns: boolean
isFirstPartyToken()
TIP
NEW v0.3.0
Cek apakah token diterbitkan untuk first-party client. Menggunakan klaim fp jika ada (sauth-server v0.3+); fallback ke inferensi scope-absence untuk token pre-v0.3.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
claims | SauthTokenClaims | - | Decoded token claims — gunakan decodeTokenClaims() terlebih dahulu |
Returns: boolean
const claims = decodeTokenClaims(token)
if (claims && isFirstPartyToken(claims)) {
// token dari GWA/GWC SPA atau session gateway
}| Kondisi | Result |
|---|---|
fp === '1p' | true |
fp === '3p' | false |
fp absent, scope absent | true (fallback) |
fp absent, scope present | false (fallback) |
isThirdPartyToken()
TIP
NEW v0.3.0
Cek apakah token diterbitkan untuk third-party client (external developer app atau partner). Kebalikan dari isFirstPartyToken().
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
claims | SauthTokenClaims | - | Decoded token claims |
Returns: boolean
isM2MToken()
TIP
NEW v0.3.0
Cek apakah token adalah machine-to-machine token (client_credentials atau API key JWT). Ditentukan dari absennya klaim snm — tidak terpengaruh oleh fp.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
claims | SauthTokenClaims | - | Decoded token claims |
Returns: boolean
const claims = decodeTokenClaims(token)
if (claims) {
if (isM2MToken(claims)) {
console.log('M2M token — sid adalah client/app name:', claims.sid)
} else {
console.log('User token — snm:', claims.snm)
}
}TIP
Gunakan isM2MToken() (bukan isFirstPartyToken()) untuk membedakan token user vs mesin. isFirstPartyToken() mengembalikan true untuk 1p user token dan 1p M2M token.
Constants
ServiceCodes
Konstanta kode service dalam sauth ecosystem.
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'| Key | Value | Deskripsi |
|---|---|---|
ServiceCodes.GWA | 'gwa' | Gateway Admin |
ServiceCodes.GWC | 'gwc' | Gateway Customer |
ServiceCodes.WEBMASTER | 'wm' | Marker webmaster bypass |
ServiceCodes.ROLE_PERSONAL | 'psn' | Role personal customer |
ServiceCodes.ROLE_COMPANY | 'cpy' | Role company customer |
ServiceCodes.ROLE_MITRA | 'mit' | Role mitra customer |
ServiceCodes.CUSTOMER_ROLES | ['psn', 'cpy', 'mit'] | Array semua customer roles |
Types
RequestOptions
interface RequestOptions {
mode?: 'session' | 'pkce'
ticketboothUrl?: string
accessTokenPrefix?: string
tokenTtl?: number
refreshTokenTtl?: number // default: 604800 (7 hari)
refreshUrl?: string
clientId?: string
onAuthFailure?: () => void
}Konfigurasi token lifecycle untuk RequestInstance. Lihat Configuration untuk detail setiap option.
TicketboothResponse
interface TicketboothResponse {
access: string
expires_in: number
}Response dari endpoint ticketbooth (POST /api/sauth/token).
PassportTokenResponse
interface PassportTokenResponse {
access_token: string
refresh_token: string
token_type: string
expires_in: number
}Response dari Laravel Passport OAuth2 token endpoint (pkce refresh grant).
SauthTokenClaims
interface SauthTokenClaims {
iss: string // short code ('gwa'/'gwc') untuk 1p; APP_URL untuk 3p (v0.3+)
aud: string
sid: string
fp?: '1p' | '3p' // explicit party marker — absent pada token pre-v0.3
snm?: string
fet?: string | string[]
act?: { sub: string }
scope?: string
jti?: string // hadir di API key JWT; absent di OAuth token
iat: number
exp?: number // absent di API key JWT; hadir di semua OAuth token
}Claims dalam JWT yang diissue oleh sauth ecosystem.
| Field | Tipe | Deskripsi |
|---|---|---|
iss | string | Issuer — short code (gwa/gwc) untuk 1p token; APP_URL untuk 3p token (v0.3+) |
aud | string | Audience — service code target |
sid | string | Subject ID (user ID, client ID, atau app name) |
fp | '1p' | '3p'? | Party marker eksplisit — absent pada token yang diterbitkan sebelum v0.3 |
snm | string? | Subject name (nama user) — absent di semua M2M token |
fet | string | string[]? | Permissions — 'wm' untuk webmaster, array untuk user normal |
act | { sub: string }? | Actor claims untuk token exchange / M2M |
scope | string? | OAuth2 scope — hadir di 3p token dan API key JWT |
jti | string? | JWT ID — hadir di API key JWT untuk revocation via blacklist |
iat | number | Issued at (Unix timestamp) |
exp | number? | Expiry (Unix timestamp) — absent di API key JWT |
NOTE
Gunakan isFirstPartyToken(), isThirdPartyToken(), dan isM2MToken() untuk mengidentifikasi jenis token — jangan baca fp, scope, atau snm secara langsung. Helper ini menangani fallback inference untuk token pre-v0.3 yang tidak punya fp.
RouteDict
type RouteDict = Partial<Record<HttpMethod, Record<string, string>>>Dictionary route yang dipartisi berdasarkan HTTP method. Biasanya diimport dari file JSON.
{
"get": {
"training.index": "/api/training",
"training.show": "/api/training/{id}"
},
"post": {
"training.store": "/api/training"
}
}RouteParams
type RouteParams<T extends string> = ExtractParams<T> extends never
? []
: [Record<ExtractParams<T>, string | number>] | (string | number)[]Tipe untuk path parameters yang di-extract dari URL template. Mendukung positional array atau named object.
HttpMethod
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete'ResponseType
type ResponseType = 'json' | 'blob' | 'arraybuffer' | 'document' | 'text' | 'stream'Examples
Contains:
- 1. Setup PKCE Mode untuk SPA
- 2. Request dengan Body dan File Upload
- 3. Permission Guard di Vue Component
- 4. Permission Check via JWT (tanpa Inertia)
- 5. Extend RequestInstance untuk Custom Refresh Logic
- 6. Route dengan Path Parameters
- 7. Wildcard & OR Permission Logic
- 8. Identifikasi Jenis Token (v0.3+) ⭐ NEW v0.3.0
1. Setup PKCE Mode untuk SPA
Untuk SPA standalone (contoh: GWC). Token exchange dilakukan di luar library (setelah OAuth2 callback); storeTokens() dipanggil untuk menyimpan hasilnya.
// api/gwc.ts
import RequestInstance from '@bpmlib/sauth-frontend/http'
import { ServiceCodes } from '@bpmlib/sauth-frontend/constants'
import router from '@/router'
import routes from './gwc-routes.json'
const gwcApi = new RequestInstance(
'https://gwc.ppsdmmigas.id',
routes,
ServiceCodes.GWC,
{
mode: 'pkce',
clientId: 'gwc-spa',
onAuthFailure: () => router.push({ name: 'login' }),
}
)
// Setelah PKCE exchange berhasil, simpan token:
gwcApi.storeTokens(accessToken, refreshToken, expiresIn)
export default gwcApi2. Request dengan Body dan File Upload
// JSON body
const { data } = await api
.setBody({ name: 'Training Baru', capacity: 30 })
.post('training.store')
// Multipart — File dan FileList dihandle otomatis
const payload = {
title: 'Laporan Q1',
attachment: fileInput.files[0], // File
screenshots: fileInput.files, // FileList → dikonvert ke attachment[0], attachment[1], ...
meta: { year: 2025, quarter: 1 }, // Nested object
}
const { data: uploaded } = await api
.setBody(payload, true)
.post('document.store')3. Permission Guard di Vue Component
<script setup lang="ts">
import { permissionCheck } from '@bpmlib/sauth-frontend/auth/inertia'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const canCreate = permissionCheck('training.create', page)
const canManage = permissionCheck(['training.update', 'training.delete'], page)
</script>
<template>
<button v-if="canCreate">Tambah Training</button>
<div v-if="canManage">
<button>Edit</button>
<button>Hapus</button>
</div>
</template>4. Permission Check via JWT (tanpa Inertia)
Berguna di context non-Inertia: middleware, worker, atau halaman yang tidak punya PageProps.
import { decodeTokenClaims, permissionCheckFromToken, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'
import api from '@/api/gwc'
const token = api.getAccessTk()
if (token) {
const claims = decodeTokenClaims(token)
if (claims && !isM2MToken(claims)) {
console.log('User:', claims.snm, '| Exp:', claims.exp ? new Date(claims.exp * 1000) : 'no expiry')
}
if (permissionCheckFromToken('report.read', token)) {
// tampilkan report
}
}5. Extend RequestInstance untuk Custom Refresh Logic
protected refreshingToken() bisa di-override untuk endpoint refresh non-OAuth2.
import RequestInstance from '@bpmlib/sauth-frontend/http'
import type { RouteDict } from '@bpmlib/sauth-frontend'
import routes from './exam-routes.json'
class ExamRequestInstance extends RequestInstance<typeof routes> {
private readonly refreshTtl = 9000 // 2.5 jam
constructor() {
super('https://exam.ppsdmmigas.id', routes, 'exam', { mode: 'pkce' })
}
protected override async refreshingToken(): Promise<void> {
const refresh = this.getRefreshTk()
if (!refresh) throw new Error('No refresh token')
const res = await fetch('/api/exam/token/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refresh }),
})
if (!res.ok) {
this.eraseCookie(`tkac_exam`)
this.eraseCookie(`tkrf_exam`)
throw new Error('Refresh failed')
}
const { access_token, refresh_token, expires_in } = await res.json()
this.setCookie('tkac_exam', access_token, Math.floor(expires_in * 0.8))
this.setCookie('tkrf_exam', refresh_token, this.refreshTtl)
}
}
export const examApi = new ExamRequestInstance()6. Route dengan Path Parameters
// Route dict: { get: { 'training.module': '/training/{id}/module/{moduleId?}' } }
// Positional spread
api.useUrl('training.module', 'get', 1, 3) // '/training/1/module/3'
// Named object
api.useUrl('training.module', 'get', { id: 1, moduleId: 3 })
// Optional param hilang jika tidak disertakan
api.useUrl('training.module', 'get', { id: 1 }) // '/training/1/module'
// Langsung via HTTP method
const { data } = await api.get('training.module', 1, 3)7. Wildcard & OR Permission Logic
// User dengan fet: ['training.*', 'exam.read']
permissionCheck('training.create', page) // true — wildcard match
permissionCheck('training.read', page) // true — wildcard match
permissionCheck('exam.read', page) // true — exact match
permissionCheck('exam.create', page) // false — tidak ada wildcard
permissionCheck(['exam.create', 'exam.read'], page) // true — OR: exam.read cocok
// Arah wildcard tidak bisa dibalik
permissionCheck('training.*', page) // false — wildcard di needed tidak berlakuKey Takeaways:
- Wildcard hanya berfungsi dari sisi
fetuser, bukan dari sisineeded - Webmaster (
fet === 'wm') selalu returntruetanpa cek apapun - Logic yang sama berlaku di
permissionCheckFromToken()
8. Identifikasi Jenis Token (v0.3+)
Helper untuk membedakan 1p vs 3p vs M2M token — berguna di context yang menerima berbagai jenis token.
import { decodeTokenClaims, isFirstPartyToken, isThirdPartyToken, isM2MToken } from '@bpmlib/sauth-frontend/auth/token'
const claims = decodeTokenClaims(token)
if (!claims) return
// Cek party
if (isFirstPartyToken(claims)) {
// Token dari GWA/GWC SPA, atau session gateway
// 1p user token: ada snm + fet, tidak ada scope
// 1p M2M token: tidak ada snm, tidak ada scope
}
if (isThirdPartyToken(claims)) {
// Token dari external developer app atau partner
// Selalu punya scope
}
// Cek user vs mesin — terlepas dari party
if (isM2MToken(claims)) {
// client_credentials atau API key JWT — snm absent
console.log('App/service:', claims.sid)
} else {
// User token — snm present
console.log('User:', claims.snm)
}Fallback pre-v0.3: Jika token tidak memiliki fp (diterbitkan sebelum sauth-server v0.3), helper menggunakan inferensi scope-presence — 1p token tidak punya scope, 3p token punya scope. Fallback ini transparan — tidak perlu cek versi token secara manual.
Links
- Repository: Gitea
- Registry: Private NPM