fix(confighttp): do not return 200 on errors (#3385)
Co-authored-by: Lukas Senionis <22381748+FrogTheFrog@users.noreply.github.com>
This commit is contained in:
parent
80ecf19d40
commit
40ac718691
4 changed files with 437 additions and 193 deletions
|
|
@ -62,4 +62,8 @@ INPUT = ../README.md \
|
|||
HTML_EXTRA_STYLESHEET += doc-styles.css
|
||||
|
||||
# extra js
|
||||
HTML_EXTRA_FILES += api.js
|
||||
HTML_EXTRA_FILES += configuration.js
|
||||
|
||||
# custom aliases
|
||||
ALIASES += api_examples{3|}="@htmlonly<script>(function() { let examples = generateExamples('\1', '\2', \3); document.write(createTabs(examples)); })();</script>@endhtmlonly"
|
||||
|
|
|
|||
130
docs/api.js
Normal file
130
docs/api.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
function generateExamples(endpoint, method, body = null) {
|
||||
let curlBodyString = '';
|
||||
let psBodyString = '';
|
||||
|
||||
if (body) {
|
||||
const curlJsonString = JSON.stringify(body).replace(/"/g, '\\"');
|
||||
curlBodyString = ` -d "${curlJsonString}"`;
|
||||
psBodyString = `-Body (ConvertTo-Json ${JSON.stringify(body)})`;
|
||||
}
|
||||
|
||||
return {
|
||||
cURL: `curl -u user:pass -X ${method.trim()} -k https://localhost:47990${endpoint.trim()}${curlBodyString}`,
|
||||
Python: `import json
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
requests.${method.trim().toLowerCase()}(
|
||||
auth=HTTPBasicAuth('user', 'pass'),
|
||||
url='https://localhost:47990${endpoint.trim()}',
|
||||
verify=False,${body ? `\n json=${JSON.stringify(body)},` : ''}
|
||||
).json()`,
|
||||
JavaScript: `fetch('https://localhost:47990${endpoint.trim()}', {
|
||||
method: '${method.trim()}',
|
||||
headers: {
|
||||
'Authorization': 'Basic ' + btoa('user:pass'),
|
||||
'Content-Type': 'application/json',
|
||||
}${body ? `,\n body: JSON.stringify(${JSON.stringify(body)}),` : ''}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log(data));`,
|
||||
PowerShell: `Invoke-RestMethod \`
|
||||
-SkipCertificateCheck \`
|
||||
-Uri 'https://localhost:47990${endpoint.trim()}' \`
|
||||
-Method ${method.trim()} \`
|
||||
-Headers @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('user:pass'))}
|
||||
${psBodyString}`
|
||||
};
|
||||
}
|
||||
|
||||
function hashString(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function createTabs(examples) {
|
||||
const languages = Object.keys(examples);
|
||||
let tabs = '<div class="tabs-overview-container"><div class="tabs-overview">';
|
||||
let content = '<div class="tab-content">';
|
||||
|
||||
languages.forEach((lang, index) => {
|
||||
const hash = hashString(examples[lang]);
|
||||
tabs += `<button class="tab-button ${index === 0 ? 'active' : ''}" onclick="openTab(event, '${lang}')"><b class="tab-title" title=" ${lang} "> ${lang} </b></button>`;
|
||||
content += `<div id="${lang}" class="tabcontent" style="display: ${index === 0 ? 'block' : 'none'};">
|
||||
<div class="doxygen-awesome-fragment-wrapper">
|
||||
<div class="fragment">
|
||||
${examples[lang].split('\n').map(line => `<div class="line">${line}</div>`).join('')}
|
||||
</div>
|
||||
<doxygen-awesome-fragment-copy-button id="copy-button-${lang}-${hash}" title="Copy to clipboard">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
||||
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path>
|
||||
</svg>
|
||||
</doxygen-awesome-fragment-copy-button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
tabs += '</div></div>';
|
||||
content += '</div>';
|
||||
|
||||
setTimeout(() => {
|
||||
languages.forEach((lang, index) => {
|
||||
const hash = hashString(examples[lang]);
|
||||
const copyButton = document.getElementById(`copy-button-${lang}-${hash}`);
|
||||
copyButton.addEventListener('click', copyContent);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return tabs + content;
|
||||
}
|
||||
|
||||
function copyContent() {
|
||||
const content = this.previousElementSibling.cloneNode(true);
|
||||
if (content instanceof Element) {
|
||||
// filter out line number from file listings
|
||||
content.querySelectorAll(".lineno, .ttc").forEach((node) => {
|
||||
node.remove();
|
||||
});
|
||||
let textContent = Array.from(content.querySelectorAll('.line'))
|
||||
.map(line => line.innerText)
|
||||
.join('\n')
|
||||
.trim(); // Join lines with newline characters and trim leading/trailing whitespace
|
||||
navigator.clipboard.writeText(textContent);
|
||||
this.classList.add("success");
|
||||
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>`;
|
||||
window.setTimeout(() => {
|
||||
this.classList.remove("success");
|
||||
this.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
|
||||
}, 980);
|
||||
} else {
|
||||
console.error('Failed to copy: content is not a DOM element');
|
||||
}
|
||||
}
|
||||
|
||||
function openTab(evt, lang) {
|
||||
const tabcontent = document.getElementsByClassName("tabcontent");
|
||||
for (const content of tabcontent) {
|
||||
content.style.display = "none";
|
||||
}
|
||||
|
||||
const tablinks = document.getElementsByClassName("tab-button");
|
||||
for (const link of tablinks) {
|
||||
link.className = link.className.replace(" active", "");
|
||||
}
|
||||
|
||||
const selectedTabs = document.querySelectorAll(`#${lang}`);
|
||||
for (const tab of selectedTabs) {
|
||||
tab.style.display = "block";
|
||||
}
|
||||
|
||||
const selectedButtons = document.querySelectorAll(`.tab-button[onclick*="${lang}"]`);
|
||||
for (const button of selectedButtons) {
|
||||
button.className += " active";
|
||||
}
|
||||
}
|
||||
11
docs/api.md
11
docs/api.md
|
|
@ -5,6 +5,10 @@ Sunshine has a RESTful API which can be used to interact with the service.
|
|||
Unless otherwise specified, authentication is required for all API calls. You can authenticate using
|
||||
basic authentication with the admin username and password.
|
||||
|
||||
@htmlonly
|
||||
<script src="api.js"></script>
|
||||
@endhtmlonly
|
||||
|
||||
## GET /api/apps
|
||||
@copydoc confighttp::getApps()
|
||||
|
||||
|
|
@ -14,7 +18,7 @@ basic authentication with the admin username and password.
|
|||
## POST /api/apps
|
||||
@copydoc confighttp::saveApp()
|
||||
|
||||
## DELETE /api/apps{index}
|
||||
## DELETE /api/apps/{index}
|
||||
@copydoc confighttp::deleteApp()
|
||||
|
||||
## POST /api/covers/upload
|
||||
|
|
@ -32,6 +36,9 @@ basic authentication with the admin username and password.
|
|||
## POST /api/restart
|
||||
@copydoc confighttp::restart()
|
||||
|
||||
## POST /api/reset-display-device-persistence
|
||||
@copydoc confighttp::resetDisplayDevicePersistence()
|
||||
|
||||
## POST /api/password
|
||||
@copydoc confighttp::savePassword()
|
||||
|
||||
|
|
@ -47,7 +54,7 @@ basic authentication with the admin username and password.
|
|||
## GET /api/clients/list
|
||||
@copydoc confighttp::listClients()
|
||||
|
||||
## GET /api/apps/close
|
||||
## POST /api/apps/close
|
||||
@copydoc confighttp::closeApp()
|
||||
|
||||
<div class="section_buttons">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue