105 lines
3.7 KiB
JavaScript
105 lines
3.7 KiB
JavaScript
|
|
// Dragonflight API client. Wraps UXP's fetch() with the Bearer header and
|
||
|
|
// handles the cross-origin-redirect quirk (UXP strips Authorization across
|
||
|
|
// hosts as a security fix — we follow such redirects manually).
|
||
|
|
//
|
||
|
|
// Persists serverUrl + apiToken in localStorage so the panel reconnects on
|
||
|
|
// reopen.
|
||
|
|
|
||
|
|
(function () {
|
||
|
|
const API = {};
|
||
|
|
const LS_URL = 'df.uxp.serverUrl';
|
||
|
|
const LS_TOKEN = 'df.uxp.apiToken';
|
||
|
|
|
||
|
|
API.state = {
|
||
|
|
serverUrl: localStorage.getItem(LS_URL) || '',
|
||
|
|
apiToken: localStorage.getItem(LS_TOKEN) || '',
|
||
|
|
connected: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
API.save = function () {
|
||
|
|
localStorage.setItem(LS_URL, API.state.serverUrl);
|
||
|
|
localStorage.setItem(LS_TOKEN, API.state.apiToken);
|
||
|
|
};
|
||
|
|
|
||
|
|
API.clear = function () {
|
||
|
|
API.state.serverUrl = '';
|
||
|
|
API.state.apiToken = '';
|
||
|
|
API.state.connected = false;
|
||
|
|
localStorage.removeItem(LS_URL);
|
||
|
|
localStorage.removeItem(LS_TOKEN);
|
||
|
|
};
|
||
|
|
|
||
|
|
function trimUrl(u) { return String(u || '').replace(/\/+$/, ''); }
|
||
|
|
|
||
|
|
// Core request. `url` may be a path (joined to serverUrl) or absolute.
|
||
|
|
// Auth header is added only for requests to our own serverUrl.
|
||
|
|
API.request = async function (urlOrPath, opts) {
|
||
|
|
opts = opts || {};
|
||
|
|
const isAbs = /^https?:\/\//i.test(urlOrPath);
|
||
|
|
const base = trimUrl(API.state.serverUrl);
|
||
|
|
const url = isAbs ? urlOrPath : (base + urlOrPath);
|
||
|
|
const headers = Object.assign({}, opts.headers || {});
|
||
|
|
if (!isAbs || url.indexOf(base) === 0) {
|
||
|
|
if (API.state.apiToken) headers['Authorization'] = 'Bearer ' + API.state.apiToken;
|
||
|
|
}
|
||
|
|
// UXP fetch supports standard options. Don't pass `credentials` — UXP
|
||
|
|
// doesn't ship a cookie jar and warns about unsupported values.
|
||
|
|
return fetch(url, Object.assign({}, opts, { headers }));
|
||
|
|
};
|
||
|
|
|
||
|
|
API.json = async function (path, opts) {
|
||
|
|
const r = await API.request(path, opts);
|
||
|
|
if (!r.ok) {
|
||
|
|
const text = await r.text().catch(() => '');
|
||
|
|
throw new Error('HTTP ' + r.status + (text ? ' — ' + text.slice(0, 200) : ''));
|
||
|
|
}
|
||
|
|
return r.json();
|
||
|
|
};
|
||
|
|
|
||
|
|
// GET /api/v1/auth/me — used as the connect probe. Returns 200 on a valid
|
||
|
|
// bearer, 401 otherwise.
|
||
|
|
API.connect = async function (serverUrl, apiToken) {
|
||
|
|
API.state.serverUrl = trimUrl(serverUrl);
|
||
|
|
API.state.apiToken = String(apiToken || '').trim();
|
||
|
|
if (!API.state.serverUrl || !API.state.apiToken) {
|
||
|
|
throw new Error('Server URL and API token are required');
|
||
|
|
}
|
||
|
|
const me = await API.json('/api/v1/auth/me');
|
||
|
|
API.state.connected = true;
|
||
|
|
API.save();
|
||
|
|
return me;
|
||
|
|
};
|
||
|
|
|
||
|
|
API.disconnect = function () {
|
||
|
|
API.clear();
|
||
|
|
};
|
||
|
|
|
||
|
|
// Asset list. The web UI calls /api/v1/assets?... — we mirror that.
|
||
|
|
API.listAssets = async function (query) {
|
||
|
|
const params = new URLSearchParams();
|
||
|
|
if (query) params.set('q', query);
|
||
|
|
params.set('limit', '60');
|
||
|
|
return API.json('/api/v1/assets?' + params.toString());
|
||
|
|
};
|
||
|
|
|
||
|
|
// Resolve a proxy stream URL → returns an absolute URL we can fetch.
|
||
|
|
// /stream returns { url: '/api/v1/assets/<id>/video', type: 'mp4', source }.
|
||
|
|
API.getProxyUrl = async function (assetId) {
|
||
|
|
const data = await API.json('/api/v1/assets/' + assetId + '/stream');
|
||
|
|
if (!data || !data.url) throw new Error('Asset has no proxy');
|
||
|
|
const u = data.url;
|
||
|
|
const abs = /^https?:\/\//i.test(u) ? u : trimUrl(API.state.serverUrl) + u;
|
||
|
|
return { url: abs, source: data.source || 'proxy' };
|
||
|
|
};
|
||
|
|
|
||
|
|
// Resolve a hi-res URL → returns a presigned S3 URL (off-host).
|
||
|
|
// /hires returns { url, filename, ext, file_size, type: 'hires' }.
|
||
|
|
API.getHiresInfo = async function (assetId) {
|
||
|
|
const data = await API.json('/api/v1/assets/' + assetId + '/hires');
|
||
|
|
if (!data || !data.url) throw new Error('Asset has no hi-res source');
|
||
|
|
return data;
|
||
|
|
};
|
||
|
|
|
||
|
|
window.API = API;
|
||
|
|
})();
|