diff --git a/AGENTS.md b/AGENTS.md index 6519bf4..337c9f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,8 @@ APIlite is a lightweight PHP library. A class extends `TPsoft\APIlite\APIlite`, public methods become API actions, requests are processed automatically, and responses are returned as JSON. The project also generates frontend clients: -- JavaScript client: `format=javascript` (`.js`) -- TypeScript client: `format=typescript` (`.ts`, real typed output) +- JavaScript client: `format=javascript` (`.js`, includes `bearerSet()` helper backed by `localStorage`) +- TypeScript client: `format=typescript` (`.ts`, real typed output, includes `bearerSet()` helper backed by `localStorage`) The HTML help page is also interactive: - HTML help: `format=html` includes endpoint docs, a built-in request tester, and optional Bearer token support stored in browser `localStorage` @@ -22,6 +22,7 @@ The HTML help page is also interactive: - Maintain both generated clients: - JavaScript stays plain JS. - TypeScript stays typed (interfaces + typed method signatures). + - Bearer token behavior stays aligned between JavaScript and TypeScript clients (`bearerSet()`, `apilite_bearer_token`, automatic `Authorization: Bearer ...` header). - Keep the HTML help page usable both as documentation and as a lightweight in-browser tester. - If you change output field names in JSON help, update README and templates consistently. - If you change request metadata or tester behavior, update `README.md` and keep `src/help.tpl.php` aligned with the actual runtime request model. @@ -42,3 +43,4 @@ The HTML help page is also interactive: ## Documentation rule When output format names/flags/URLs change, update `README.md` in the same change set. +When Bearer token helper/storage behavior changes, update `README.md` and keep JS/TS client docs aligned with the actual generated templates. diff --git a/README.md b/README.md index 47d117d..976745f 100644 --- a/README.md +++ b/README.md @@ -238,11 +238,15 @@ You can also download generated frontend clients: * JavaScript: `?format=javascript` * TypeScript (typed for Vue/TS projects): `?format=typescript` +Both generated clients support a shared Bearer token helper. Call `bearerSet(token)` once, the token is stored in `localStorage` under `apilite_bearer_token`, and subsequent requests automatically send `Authorization: Bearer ...`. + JavaScript usage example: ```js import backend from './backend.js'; +backend.bearerSet('your-access-token'); + backend.add(1, 2).then((response) => { console.log(response.data); }); @@ -253,6 +257,8 @@ TypeScript usage example (Vue + TS): ```ts import backend from './backend'; +backend.bearerSet('your-access-token'); + backend.add(1, 2).then((response) => { console.log(response.data); // typed value based on PHP return type }); diff --git a/src/javascript.tpl.php b/src/javascript.tpl.php index 33a7591..8e1a186 100644 --- a/src/javascript.tpl.php +++ b/src/javascript.tpl.php @@ -7,12 +7,63 @@ class apiName; ?> { endpoint = endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; + bearerStorageKey = 'apilite_bearer_token'; + + normalizeBearerToken(token) { + if (typeof token !== 'string') return null; + token = token.trim(); + return token === '' ? null : token; + } + + getStorage() { + try { + if (typeof window !== 'undefined' && window.localStorage) return window.localStorage; + } catch (error) { + return null; + } + return null; + } + + getBearerToken() { + var storage = this.getStorage(); + if (storage == null) return null; + return this.normalizeBearerToken(storage.getItem(this.bearerStorageKey)); + } + + getRequestHeaders(headers = {}) { + var requestHeaders = Object.assign({}, headers); + if (typeof requestHeaders.Authorization === 'undefined') { + var token = this.getBearerToken(); + if (token != null) requestHeaders.Authorization = 'Bearer ' + token; + } + return requestHeaders; + } + + applyHeaders(xhttp, headers) { + Object.keys(headers).forEach(key => { + var value = headers[key]; + if (typeof value === 'undefined' || value === null) return; + xhttp.setRequestHeader(key, value); + }); + } + + bearerSet(token) { + var storage = this.getStorage(); + if (storage == null) return; + token = this.normalizeBearerToken(token); + if (token == null) { + storage.removeItem(this.bearerStorageKey); + return; + } + storage.setItem(this.bearerStorageKey, token); + } /* ---------------------------------------------------- * General API call */ call(method, data, callback) { var xhttp = new XMLHttpRequest(); + var headers = this.getRequestHeaders(); xhttp.withCredentials = true; xhttp.onreadystatechange = function() { if (this.readyState === 4) { @@ -31,6 +82,7 @@ class apiName; ?> { form_data.append(key, val); }); xhttp.open('POST', this.endpoint + '?action=' + method); + this.applyHeaders(xhttp, headers); xhttp.send(form_data); } @@ -66,4 +118,4 @@ class apiName; ?> { }; -export default new apiName; ?>(); +export default new apiName; ?>(); \ No newline at end of file diff --git a/src/typescript.tpl.php b/src/typescript.tpl.php index 22a38fd..638840e 100644 --- a/src/typescript.tpl.php +++ b/src/typescript.tpl.php @@ -152,8 +152,72 @@ export interface APIliteHelpResponse { msg: string; } +type APIliteRequestHeaders = Partial>; + class apiName; ?> { endpoint: string = endpoint, 0, 4) == 'http' ? '"%s"' : '%s', $this->endpoint); ?>; + private readonly bearerStorageKey: string = 'apilite_bearer_token'; + + private normalizeBearerToken(token: string | null | undefined): string | null { + if (typeof token !== 'string') { + return null; + } + const normalizedToken = token.trim(); + return normalizedToken === '' ? null : normalizedToken; + } + + private getStorage(): Storage | null { + try { + if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') { + return window.localStorage; + } + } catch { + return null; + } + return null; + } + + private getBearerToken(): string | null { + const storage = this.getStorage(); + if (storage === null) { + return null; + } + return this.normalizeBearerToken(storage.getItem(this.bearerStorageKey)); + } + + private getRequestHeaders(headers: APIliteRequestHeaders = {}): APIliteRequestHeaders { + const requestHeaders: APIliteRequestHeaders = { ...headers }; + if (typeof requestHeaders.Authorization === 'undefined') { + const token = this.getBearerToken(); + if (token !== null) { + requestHeaders.Authorization = `Bearer ${token}`; + } + } + return requestHeaders; + } + + private applyHeaders(xhttp: XMLHttpRequest, headers: APIliteRequestHeaders): void { + Object.keys(headers).forEach((key) => { + const value = headers[key]; + if (typeof value === 'undefined' || value === null) { + return; + } + xhttp.setRequestHeader(key, value); + }); + } + + bearerSet(token: string | null): void { + const storage = this.getStorage(); + if (storage === null) { + return; + } + const normalizedToken = this.normalizeBearerToken(token); + if (normalizedToken === null) { + storage.removeItem(this.bearerStorageKey); + return; + } + storage.setItem(this.bearerStorageKey, normalizedToken); + } private call( method: string, @@ -161,6 +225,7 @@ class apiName; ?> { callback: (response: APIliteHelpResponse | APIliteActionResponse | APIliteErrorResponse) => void ): void { const xhttp = new XMLHttpRequest(); + const headers = this.getRequestHeaders(); xhttp.withCredentials = true; xhttp.onreadystatechange = function() { if (this.readyState === 4) { @@ -191,6 +256,7 @@ class apiName; ?> { }); xhttp.open('POST', this.endpoint + '?action=' + method); + this.applyHeaders(xhttp, headers); xhttp.send(formData); } @@ -231,4 +297,4 @@ class apiName; ?> { } -export default new apiName; ?>(); +export default new apiName; ?>(); \ No newline at end of file