Download src/services/omadaClient.js from Mbonea/WifiBiz: direct link, hf CLI and curl.
- Browser
- Download file 8.38 kB
-
https://huggingface.co/spaces/Mbonea/WifiBiz/resolve/main/src/services/omadaClient.js
- Command line
-
hf download hf://spaces/Mbonea/WifiBiz/src/services/omadaClient.js
-
curl -L -o omadaClient.js https://huggingface.co/spaces/Mbonea/WifiBiz/resolve/main/src/services/omadaClient.js
8.38 kB
| const axios = require('axios'); | |
| const crypto = require('crypto'); | |
| const fs = require('fs'); | |
| const https = require('https'); | |
| function trimSlash(value) { | |
| return String(value || '').replace(/\/+$/, ''); | |
| } | |
| function encodeQuery(query = {}) { | |
| const params = new URLSearchParams(); | |
| for (const [key, value] of Object.entries(query || {})) { | |
| if (value !== undefined && value !== null && value !== '') { | |
| params.set(key, String(value)); | |
| } | |
| } | |
| const text = params.toString(); | |
| return text ? `?${text}` : ''; | |
| } | |
| function formatOmadaError(error, context = '') { | |
| if (error.response) { | |
| const data = error.response.data; | |
| const body = typeof data === 'string' ? data : JSON.stringify(data); | |
| return `${context}${error.response.status} ${error.response.statusText}: ${body}`; | |
| } | |
| if (error.omada) { | |
| const detail = error.result ? ` ${JSON.stringify(error.result)}` : ''; | |
| return `${context}Omada error ${error.errorCode}: ${error.message}${detail}`; | |
| } | |
| return `${context}${error.message}`; | |
| } | |
| class OmadaClient { | |
| constructor(config) { | |
| this.config = config || {}; | |
| this.controllerUrl = trimSlash(config.controllerUrl); | |
| this.omadacId = config.omadacId; | |
| this.clientId = config.clientId; | |
| this.clientSecret = config.clientSecret; | |
| this.accessToken = config.accessToken || null; | |
| this.authHeaderStyle = config.authHeaderStyle || 'access-token'; | |
| this.controllerId = config.controllerId || config.omadacId || null; | |
| this.csrfToken = null; | |
| this.sessionCookies = ''; | |
| this.http = axios.create({ | |
| baseURL: this.controllerUrl, | |
| timeout: Number(config.timeoutMs || 30000), | |
| httpsAgent: new https.Agent({ | |
| rejectUnauthorized: config.rejectUnauthorized === true, | |
| }), | |
| validateStatus: () => true, | |
| }); | |
| } | |
| requireOpenApiConfig() { | |
| const missing = []; | |
| if (!this.controllerUrl) missing.push('OMADA_URL'); | |
| if (!this.omadacId) missing.push('OMADA_OPENAPI_OMADAC_ID'); | |
| if (!this.clientId) missing.push('OMADA_OPENAPI_CLIENT_ID'); | |
| if (!this.clientSecret) missing.push('OMADA_OPENAPI_CLIENT_SECRET'); | |
| if (missing.length) { | |
| throw new Error(`Missing required Omada OpenAPI config: ${missing.join(', ')}`); | |
| } | |
| } | |
| async authorize() { | |
| this.requireOpenApiConfig(); | |
| const body = { | |
| omadacId: this.omadacId, | |
| client_id: this.clientId, | |
| client_secret: this.clientSecret, | |
| }; | |
| const res = await this.http.post('/openapi/authorize/token?grant_type=client_credentials', body); | |
| const data = this.unwrap(res, 'authorize'); | |
| const token = data.result?.accessToken || data.result?.token || data.accessToken || data.token; | |
| if (!token) { | |
| throw new Error(`Authorize succeeded but no access token was found: ${JSON.stringify(data)}`); | |
| } | |
| this.accessToken = token; | |
| return data; | |
| } | |
| authHeaders(style = this.authHeaderStyle) { | |
| if (!this.accessToken) return {}; | |
| if (style === 'bearer-access-token') { | |
| return { Authorization: `Bearer AccessToken=${this.accessToken}` }; | |
| } | |
| if (style === 'bearer') { | |
| return { Authorization: `Bearer ${this.accessToken}` }; | |
| } | |
| return { Authorization: `AccessToken=${this.accessToken}` }; | |
| } | |
| openApiPath(pathPart) { | |
| const suffix = String(pathPart || '').startsWith('/') ? pathPart : `/${pathPart}`; | |
| return `/openapi/v1/${this.omadacId}${suffix}`; | |
| } | |
| async request(method, url, { body = null, query = null, headers = {}, retry = true } = {}) { | |
| if (!this.accessToken) { | |
| await this.authorize(); | |
| } | |
| const fullUrl = `${url}${encodeQuery(query)}`; | |
| const styles = [this.authHeaderStyle, 'bearer-access-token', 'bearer'] | |
| .filter((style, index, arr) => arr.indexOf(style) === index); | |
| let lastError = null; | |
| for (const style of styles) { | |
| const res = await this.http.request({ | |
| method, | |
| url: fullUrl, | |
| headers: { | |
| ...this.authHeaders(style), | |
| ...headers, | |
| }, | |
| data: body ?? undefined, | |
| }); | |
| try { | |
| const data = this.unwrap(res, `${method} ${fullUrl}`); | |
| this.authHeaderStyle = style; | |
| return data; | |
| } catch (err) { | |
| lastError = err; | |
| if (retry && this.isExpiredToken(err)) { | |
| this.accessToken = null; | |
| await this.authorize(); | |
| return this.request(method, url, { body, query, headers, retry: false }); | |
| } | |
| if (!this.isAuthHeaderCandidate(err)) { | |
| throw err; | |
| } | |
| } | |
| } | |
| throw lastError; | |
| } | |
| async openApi(method, pathPart, options = {}) { | |
| return this.request(method, this.openApiPath(pathPart), options); | |
| } | |
| async controllerLogin() { | |
| if (!this.controllerId) { | |
| const info = await this.http.get('/api/info'); | |
| const data = this.unwrap(info, 'GET /api/info'); | |
| this.controllerId = data.result?.omadacId; | |
| if (!this.controllerId) { | |
| throw new Error(`Controller ID not found in /api/info: ${JSON.stringify(data)}`); | |
| } | |
| } | |
| const username = this.config.controllerUsername || process.env.OMADA_USER; | |
| const password = this.config.controllerPassword || process.env.OMADA_PASS; | |
| if (!username || !password) { | |
| throw new Error('Missing OMADA_USER/OMADA_PASS for Omada controller fallback'); | |
| } | |
| const res = await this.http.post(`/${this.controllerId}/api/v2/login`, { username, password }); | |
| const data = this.unwrap(res, 'controller login'); | |
| this.csrfToken = data.result?.token; | |
| this.sessionCookies = (res.headers['set-cookie'] || []).map((item) => item.split(';')[0]).join('; '); | |
| return data; | |
| } | |
| async controllerRequest(method, pathPart, body = null, retried = false) { | |
| if (!this.controllerId || !this.csrfToken) { | |
| await this.controllerLogin(); | |
| } | |
| const path = String(pathPart || '').startsWith('/') ? pathPart : `/${pathPart}`; | |
| const res = await this.http.request({ | |
| method, | |
| url: `/${this.controllerId}/api/v2${path}`, | |
| headers: { | |
| 'Csrf-Token': this.csrfToken, | |
| Cookie: this.sessionCookies, | |
| 'Content-Type': 'application/json', | |
| }, | |
| data: body ?? undefined, | |
| }); | |
| try { | |
| return this.unwrap(res, `${method} controller ${path}`); | |
| } catch (err) { | |
| if (!retried && this.isExpiredToken(err)) { | |
| await this.controllerLogin(); | |
| return this.controllerRequest(method, pathPart, body, true); | |
| } | |
| throw err; | |
| } | |
| } | |
| async controllerUploadDataField(pathPart, data, retried = false) { | |
| if (!this.controllerId || !this.csrfToken) { | |
| await this.controllerLogin(); | |
| } | |
| const path = String(pathPart || '').startsWith('/') ? pathPart : `/${pathPart}`; | |
| const form = new FormData(); | |
| form.append('data', JSON.stringify(data)); | |
| const res = await this.http.request({ | |
| method: 'POST', | |
| url: `/${this.controllerId}/api/v2${path}`, | |
| headers: { | |
| 'Csrf-Token': this.csrfToken, | |
| Cookie: this.sessionCookies, | |
| 'X-Requested-With': 'XMLHttpRequest', | |
| refresh: 'manual', | |
| }, | |
| data: form, | |
| }); | |
| try { | |
| return this.unwrap(res, `POST controller ${path}`); | |
| } catch (err) { | |
| if (!retried && this.isExpiredToken(err)) { | |
| await this.controllerLogin(); | |
| return this.controllerUploadDataField(pathPart, data, true); | |
| } | |
| throw err; | |
| } | |
| } | |
| fileMd5(filePath) { | |
| return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex'); | |
| } | |
| unwrap(res, label) { | |
| const data = res.data; | |
| if (res.status < 200 || res.status >= 300) { | |
| const err = new Error(`${label} failed with HTTP ${res.status}: ${typeof data === 'string' ? data : JSON.stringify(data)}`); | |
| err.response = res; | |
| throw err; | |
| } | |
| if (data && typeof data === 'object' && 'errorCode' in data && data.errorCode !== 0) { | |
| const err = new Error(data.msg || data.message || `${label} failed`); | |
| err.omada = true; | |
| err.errorCode = data.errorCode; | |
| err.result = data.result; | |
| throw err; | |
| } | |
| return data; | |
| } | |
| isExpiredToken(err) { | |
| return err.response?.status === 401 || [-1000, -44106, -44112].includes(err.errorCode); | |
| } | |
| isAuthHeaderCandidate(err) { | |
| return err.response?.status === 401 || [-44106, -44112, -44113].includes(err.errorCode); | |
| } | |
| } | |
| module.exports = { | |
| OmadaClient, | |
| formatOmadaError, | |
| }; | |