wrap all internal api calls with auth check and use token when possible

Signed-off-by: Vladimir Mandic <mandic00@live.com>
pull/4401/head^2
Vladimir Mandic 2025-11-11 18:13:47 -05:00
parent f383a2babc
commit 8fb037d4d4
19 changed files with 55 additions and 24 deletions

View File

@ -50,6 +50,7 @@
},
"globals": {
"panzoom": "readonly",
"authFetch": "readonly",
"log": "readonly",
"debug": "readonly",
"error": "readonly",

View File

@ -25,6 +25,7 @@ TBD
- update global lint rules
- chrono: switch to official pipeline
- pipeline: add optional preprocess and postprocess hooks
- auth: wrap all internal api calls with auth check and use token when possible
- **Fixes**
- hires: strength save/load in metadata, thanks @awsr
- imgi2img: fix initial scale tab, thanks @awsr

View File

@ -4,6 +4,11 @@
- <https://github.com/users/vladmandic/projects>
## Kanvas
- server-side mask handling vs ui mask handling
- outpaint visible image edge seam
## Internal
- UI: New inpaint/outpaint interface

20
javascript/authWrap.js Normal file
View File

@ -0,0 +1,20 @@
let user = null;
let token = null;
async function authFetch(url, options = {}) {
if (!token) {
const res = await fetch(`${window.subpath}/token`);
if (res.ok) {
const data = await res.json();
user = data.user;
token = data.token;
}
}
if (user && token) {
if (!options.headers) options.headers = {};
const encoded = btoa(`${user}:${token}`);
options.headers.Authorization = `Basic ${encoded}`;
}
const res = await fetch(url, options);
return res;
}

View File

@ -73,7 +73,7 @@ async function modelCardClick(id) {
log('modelCardClick id', id);
const el = gradioApp().getElementById('model-details') || gradioApp().getElementById('civitai_models_output') || gradioApp().getElementById('models_outcome');
if (!el) return;
const res = await fetch(`${window.api}/civitai?model_id=${encodeURI(id)}`);
const res = await authFetch(`${window.api}/civitai?model_id=${encodeURI(id)}`);
if (!res || res.status !== 200) {
error(`modelCardClick: id=${id} status=${res ? res.status : 'unknown'}`);
return;

View File

@ -132,7 +132,7 @@ const getStatus = async () => {
log('progressInternal:', data);
if (el) el.innerText += '\nProgress internal:\n' + JSON.stringify(data, null, 2); // eslint-disable-line prefer-template
}
res = await fetch('./sdapi/v1/progress?skip_current_image=true', { method: 'GET', headers });
res = await authFetch('./sdapi/v1/progress?skip_current_image=true', { method: 'GET', headers });
if (res?.ok) {
data = await res.json();
log('progressAPI:', data);

View File

@ -368,7 +368,7 @@ function selectHistory(id) {
const headers = new Headers();
headers.set('Content-Type', 'application/json');
const init = { method: 'POST', body: { name: id }, headers };
fetch(`${window.api}/history`, { method: 'POST', body: JSON.stringify({ name: id }), headers });
authFetch(`${window.api}/history`, { method: 'POST', body: JSON.stringify({ name: id }), headers });
}
let enDirty = false;

View File

@ -185,7 +185,7 @@ async function delayFetchThumb(fn) {
while (outstanding > 16) await new Promise((resolve) => setTimeout(resolve, 50)); // eslint-disable-line no-promise-executor-return
outstanding++;
const ts = Date.now().toString();
const res = await fetch(`${window.api}/browser/thumb?file=${encodeURI(fn)}&ts=${ts}`, { priority: 'low' });
const res = await authFetch(`${window.api}/browser/thumb?file=${encodeURI(fn)}&ts=${ts}`, { priority: 'low' });
if (!res.ok) {
error(`fetchThumb: ${res.statusText}`);
outstanding--;
@ -552,7 +552,7 @@ async function fetchFilesHT(evt) {
updateStatusWithSort(`Folder: ${evt.target.name} | in-progress`);
let numFiles = 0;
const res = await fetch(`${window.api}/browser/files?folder=${encodeURI(evt.target.name)}`);
const res = await authFetch(`${window.api}/browser/files?folder=${encodeURI(evt.target.name)}`);
if (!res || res.status !== 200) {
updateStatusWithSort(`Folder: ${evt.target.name} | failed: ${res?.statusText}`);
return;
@ -639,7 +639,7 @@ async function pruneImages() {
async function galleryVisible() {
// if (el.folders.children.length > 0) return;
const res = await fetch(`${window.api}/browser/folders`);
const res = await authFetch(`${window.api}/browser/folders`);
if (!res || res.status !== 200) return;
el.folders.innerHTML = '';
url = res.url.split('/sdapi')[0].replace('http', 'ws'); // update global url as ws need fqdn

View File

@ -30,7 +30,7 @@ async function updateGPU() {
const gpuEl = document.getElementById('gpu');
const gpuTable = document.getElementById('gpu-table');
try {
const res = await fetch(`${window.api}/gpu`);
const res = await authFetch(`${window.api}/gpu`);
if (!res.ok) {
clearInterval(gpuInterval);
gpuEl.style.display = 'none';

View File

@ -3,7 +3,7 @@ const ioTypes = ['load', 'save'];
function refreshHistory() {
log('refreshHistory');
fetch(`${window.api}/history`, { priority: 'low' }).then((res) => {
authFetch(`${window.api}/history`, { priority: 'low' }).then((res) => {
const timeline = document.getElementById('history_timeline');
const table = document.getElementById('history_table');
timeline.innerHTML = '';

View File

@ -52,7 +52,7 @@ async function createSplash() {
}
const imgEl = `<div id="spash-img" class="splash-img" alt="logo" style="background-image: url(file=html/logo-bg-${dark ? 'dark' : 'light'}.jpg), url(file=html/logo-bg-${num}.jpg); background-blend-mode: ${dark ? 'multiply' : 'lighten'}"></div>`;
document.getElementById('splash').insertAdjacentHTML('afterbegin', imgEl);
fetch(`${window.api}/motd`)
authFetch(`${window.api}/motd`)
.then((res) => res.text())
.then((text) => {
const motdEl = document.getElementById('motd');

View File

@ -74,7 +74,7 @@ async function logMonitor() {
if (!logMonitorEl) return;
const atBottom = logMonitorEl.scrollHeight <= (logMonitorEl.scrollTop + logMonitorEl.clientHeight);
try {
const res = await fetch(`${window.api}/log?clear=True`);
const res = await authFetch(`${window.api}/log?clear=True`);
if (res?.ok) {
logMonitorStatus = true;
const lines = await res.json();
@ -123,7 +123,7 @@ async function initLogMonitor() {
</table>
`;
el.style.display = 'none';
fetch(`${window.api}/start?agent=${encodeURI(navigator.userAgent)}`);
authFetch(`${window.api}/start?agent=${encodeURI(navigator.userAgent)}`);
logMonitor();
log('initLogMonitor');
}

View File

@ -54,11 +54,11 @@ const xhrInternal = (xhrObj, data, handler = undefined, errorHandler = undefined
try {
const json = JSON.parse(xhrObj.responseText);
if (handler) handler(json);
} catch (e) {
error(`xhr.onreadystatechange: ${e}`);
} catch {
// error(`xhr.onreadystatechange: ${e}`);
}
} else {
err(`xhr.onreadystatechange: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`);
// err(`xhr.onreadystatechange: state=${xhrObj.readyState} status=${xhrObj.status} response=${xhrObj.responseText}`);
}
}
};

View File

@ -23,7 +23,7 @@ async function updateIndicator(online, data, msg) {
async function monitorConnection() {
try {
const res = await fetch(`${window.api}/version`);
const res = await authFetch(`${window.api}/version`);
const data = await res.json();
const url = res.url.split('/sdapi')[0].replace('http', 'ws'); // update global url as ws need fqdn
const ws = new WebSocket(`${url}/queue/join`);

View File

@ -164,7 +164,7 @@ async function initModels() {
const el = gradioApp().getElementById('main_info');
const en = gradioApp().getElementById('txt2img_extra_networks');
if (!el || !en) return;
const req = await fetch(`${window.api}/sd-models`);
const req = await authFetch(`${window.api}/sd-models`);
const res = req.ok ? await req.json() : [];
log('initModels', res.length);
const ready = () => `

View File

@ -463,7 +463,7 @@ function monitorServerStatus() {
function restartReload() {
document.body.style = 'background: #222222; font-size: 1rem; font-family:monospace; margin-top:20%; color:lightgray; text-align:center';
document.body.innerHTML = '<h1>Server shutdown in progress...</h1>';
fetch(`${window.api}/progress?skip_current_image=true`)
authFetch(`${window.api}/progress?skip_current_image=true`)
.then((res) => setTimeout(restartReload, 1000))
.catch((e) => setTimeout(monitorServerStatus, 500));
return [];

View File

@ -117,8 +117,8 @@ class Api:
from modules.civitai import api_civitai
api_civitai.register_api()
def add_api_route(self, path: str, fn, **kwargs):
if self.credentials:
def add_api_route(self, path: str, fn, auth: bool = True, **kwargs):
if auth and self.credentials:
deps = list(kwargs.get('dependencies', []))
deps.append(Depends(self.auth))
kwargs['dependencies'] = deps
@ -132,6 +132,9 @@ class Api:
if credentials.username in self.credentials:
if compare_digest(credentials.password, self.credentials[credentials.username]):
return True
if hasattr(self.app, 'tokens') and (self.app.tokens is not None):
if credentials.password in self.app.tokens.keys():
return True
shared.log.error(f'API authentication: user="{credentials.username}" password="{credentials.password}"')
raise HTTPException(status_code=401, detail="Unauthorized", headers={"WWW-Authenticate": "Basic"})

View File

@ -81,7 +81,8 @@ def setup_middleware(app: FastAPI, cmd_opts):
if err['code'] == 404 and 'file=html/' in req.url.path: # dont spam with locales
return JSONResponse(status_code=err['code'], content=jsonable_encoder(err))
log.error(f"API error: {req.method}: {req.url} {err}")
if not any([req.url.path.endswith(x) for x in ignore_endpoints]): # noqa C419 # pylint: disable=use-a-generator
log.error(f"API error: {req.method}: {req.url} {err}")
if not isinstance(e, HTTPException) and err['error'] != 'TypeError': # do not print backtrace on known httpexceptions
errors.display(e, 'HTTP API', [anyio, fastapi, uvicorn, starlette])

View File

@ -115,10 +115,10 @@ def init_api():
return JSONResponse(obj)
shared.api.add_api_route("/sdapi/v1/network", get_network, methods=["GET"])
shared.api.add_api_route("/sdapi/v1/network/thumb", fetch_file, methods=["GET"])
shared.api.add_api_route("/sdapi/v1/network/metadata", get_metadata, methods=["GET"])
shared.api.add_api_route("/sdapi/v1/network/info", get_info, methods=["GET"])
shared.api.add_api_route("/sdapi/v1/network/desc", get_desc, methods=["GET"])
shared.api.add_api_route("/sdapi/v1/network/thumb", fetch_file, methods=["GET"], auth=False)
shared.api.add_api_route("/sdapi/v1/network/metadata", get_metadata, methods=["GET"], auth=False)
shared.api.add_api_route("/sdapi/v1/network/info", get_info, methods=["GET"], auth=False)
shared.api.add_api_route("/sdapi/v1/network/desc", get_desc, methods=["GET"], auth=False)
class DateTimeEncoder(json.JSONEncoder):