Change login from http basic auth to cookies

This commit is contained in:
Yukino Song
2024-08-30 06:17:02 +08:00
parent c0e65632f6
commit 652661ea50
11 changed files with 289 additions and 109 deletions

View File

@@ -53,6 +53,8 @@ namespace confighttp {
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>; using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>; using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
std::string sessionCookie;
enum class op_e { enum class op_e {
ADD, ///< Add client ADD, ///< Add client
REMOVE ///< Remove client REMOVE ///< Remove client
@@ -80,10 +82,7 @@ namespace confighttp {
send_unauthorized(resp_https_t response, req_https_t request) { send_unauthorized(resp_https_t response, req_https_t request) {
auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv; BOOST_LOG(info) << "Web UI: ["sv << address << "] -- not authorized"sv;
const SimpleWeb::CaseInsensitiveMultimap headers { response->write(SimpleWeb::StatusCode::client_error_unauthorized);
{ "WWW-Authenticate", R"(Basic realm="Sunshine Gamestream Host", charset="UTF-8")" }
};
response->write(SimpleWeb::StatusCode::client_error_unauthorized, headers);
} }
void void
@@ -96,8 +95,26 @@ namespace confighttp {
response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers); response->write(SimpleWeb::StatusCode::redirection_temporary_redirect, headers);
} }
std::string getCookieValue(const std::string& cookieString, const std::string& key) {
std::string keyWithEqual = key + "=";
std::size_t startPos = cookieString.find(keyWithEqual);
if (startPos == std::string::npos) {
return "";
}
startPos += keyWithEqual.length();
std::size_t endPos = cookieString.find(";", startPos);
if (endPos == std::string::npos) {
return cookieString.substr(startPos);
}
return cookieString.substr(startPos, endPos - startPos);
}
bool bool
authenticate(resp_https_t response, req_https_t request) { checkIPOrigin(resp_https_t response, req_https_t request) {
auto address = net::addr_to_normalized_string(request->remote_endpoint().address()); auto address = net::addr_to_normalized_string(request->remote_endpoint().address());
auto ip_type = net::from_address(address); auto ip_type = net::from_address(address);
@@ -107,6 +124,15 @@ namespace confighttp {
return false; return false;
} }
return true;
}
bool
authenticate(resp_https_t response, req_https_t request, bool needsRedirect = false) {
if (!checkIPOrigin(response, request)) {
return false;
}
// If credentials are shown, redirect the user to a /welcome page // If credentials are shown, redirect the user to a /welcome page
if (config::sunshine.username.empty()) { if (config::sunshine.username.empty()) {
send_redirect(response, request, "/welcome"); send_redirect(response, request, "/welcome");
@@ -114,27 +140,24 @@ namespace confighttp {
} }
auto fg = util::fail_guard([&]() { auto fg = util::fail_guard([&]() {
if (needsRedirect) {
send_redirect(response, request, "/login");
} else {
send_unauthorized(response, request); send_unauthorized(response, request);
}
}); });
auto auth = request->header.find("authorization"); if (sessionCookie.empty()) {
if (auth == request->header.end()) {
return false; return false;
} }
auto &rawAuth = auth->second; auto cookies = request->header.find("cookie");
auto authData = SimpleWeb::Crypto::Base64::decode(rawAuth.substr("Basic "sv.length())); if (cookies == request->header.end()) {
int index = authData.find(':');
if (index >= authData.size() - 1) {
return false; return false;
} }
auto username = authData.substr(0, index); auto authCookie = getCookieValue(cookies->second, "auth");
auto password = authData.substr(index + 1); if (authCookie.empty() || authCookie != sessionCookie) {
auto hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
return false; return false;
} }
@@ -156,105 +179,73 @@ namespace confighttp {
<< data.str(); << data.str();
} }
/**
* @todo combine these functions into a single function that accepts the page, i.e "index", "pin", "apps"
*/
void void
getIndexPage(resp_https_t response, req_https_t request) { fetchStaticPage(resp_https_t response, req_https_t request, const std::string& page, bool needsAuthenticate) {
if (!authenticate(response, request)) return; if (needsAuthenticate) {
if (!authenticate(response, request, true)) return;
}
print_req(request); print_req(request);
std::string content = file_handler::read_file(WEB_DIR "index.html"); std::string content = file_handler::read_file((WEB_DIR + page).c_str());
SimpleWeb::CaseInsensitiveMultimap headers; const SimpleWeb::CaseInsensitiveMultimap headers {
headers.emplace("Content-Type", "text/html; charset=utf-8"); { "Content-Type", "text/html; charset=utf-8" },
{ "Access-Control-Allow-Origin", "https://images.igdb.com/"}
};
response->write(content, headers); response->write(content, headers);
};
void
getIndexPage(resp_https_t response, req_https_t request) {
fetchStaticPage(response, request, "index.html", true);
} }
void void
getPinPage(resp_https_t response, req_https_t request) { getPinPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; fetchStaticPage(response, request, "pin.html", true);
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "pin.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(content, headers);
} }
void void
getAppsPage(resp_https_t response, req_https_t request) { getAppsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; fetchStaticPage(response, request, "apps.html", true);
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "apps.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
headers.emplace("Access-Control-Allow-Origin", "https://images.igdb.com/");
response->write(content, headers);
}
void
getClientsPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return;
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "clients.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(content, headers);
} }
void void
getConfigPage(resp_https_t response, req_https_t request) { getConfigPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; fetchStaticPage(response, request, "config.html", true);
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "config.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(content, headers);
} }
void void
getPasswordPage(resp_https_t response, req_https_t request) { getPasswordPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; fetchStaticPage(response, request, "password.html", true);
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "password.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(content, headers);
} }
void void
getWelcomePage(resp_https_t response, req_https_t request) { getWelcomePage(resp_https_t response, req_https_t request) {
print_req(request); if (!checkIPOrigin(response, request)) {
return;
}
if (!config::sunshine.username.empty()) { if (!config::sunshine.username.empty()) {
send_redirect(response, request, "/"); send_redirect(response, request, "/");
return; return;
} }
std::string content = file_handler::read_file(WEB_DIR "welcome.html");
SimpleWeb::CaseInsensitiveMultimap headers; fetchStaticPage(response, request, "welcome.html", false);
headers.emplace("Content-Type", "text/html; charset=utf-8"); }
response->write(content, headers);
void
getLoginPage(resp_https_t response, req_https_t request) {
if (!checkIPOrigin(response, request)) {
return;
}
fetchStaticPage(response, request, "login.html", false);
} }
void void
getTroubleshootingPage(resp_https_t response, req_https_t request) { getTroubleshootingPage(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; fetchStaticPage(response, request, "troubleshooting.html", true);
print_req(request);
std::string content = file_handler::read_file(WEB_DIR "troubleshooting.html");
SimpleWeb::CaseInsensitiveMultimap headers;
headers.emplace("Content-Type", "text/html; charset=utf-8");
response->write(content, headers);
} }
/** /**
@@ -263,11 +254,16 @@ namespace confighttp {
*/ */
void void
getFaviconImage(resp_https_t response, req_https_t request) { getFaviconImage(resp_https_t response, req_https_t request) {
if (!checkIPOrigin(response, request)) {
return;
}
print_req(request); print_req(request);
std::ifstream in(WEB_DIR "images/apollo.ico", std::ios::binary); std::ifstream in(WEB_DIR "images/apollo.ico", std::ios::binary);
SimpleWeb::CaseInsensitiveMultimap headers; const SimpleWeb::CaseInsensitiveMultimap headers {
headers.emplace("Content-Type", "image/x-icon"); { "Content-Type", "image/x-icon" }
};
response->write(SimpleWeb::StatusCode::success_ok, in, headers); response->write(SimpleWeb::StatusCode::success_ok, in, headers);
} }
@@ -277,11 +273,16 @@ namespace confighttp {
*/ */
void void
getSunshineLogoImage(resp_https_t response, req_https_t request) { getSunshineLogoImage(resp_https_t response, req_https_t request) {
if (!checkIPOrigin(response, request)) {
return;
}
print_req(request); print_req(request);
std::ifstream in(WEB_DIR "images/logo-apollo-45.png", std::ios::binary); std::ifstream in(WEB_DIR "images/logo-apollo-45.png", std::ios::binary);
SimpleWeb::CaseInsensitiveMultimap headers; const SimpleWeb::CaseInsensitiveMultimap headers {
headers.emplace("Content-Type", "image/png"); { "Content-Type", "image/png" }
};
response->write(SimpleWeb::StatusCode::success_ok, in, headers); response->write(SimpleWeb::StatusCode::success_ok, in, headers);
} }
@@ -293,6 +294,10 @@ namespace confighttp {
void void
getNodeModules(resp_https_t response, req_https_t request) { getNodeModules(resp_https_t response, req_https_t request) {
if (!checkIPOrigin(response, request)) {
return;
}
print_req(request); print_req(request);
fs::path webDirPath(WEB_DIR); fs::path webDirPath(WEB_DIR);
fs::path nodeModulesPath(webDirPath / "assets"); fs::path nodeModulesPath(webDirPath / "assets");
@@ -332,8 +337,9 @@ namespace confighttp {
print_req(request); print_req(request);
std::string content = file_handler::read_file(config::stream.file_apps.c_str()); std::string content = file_handler::read_file(config::stream.file_apps.c_str());
SimpleWeb::CaseInsensitiveMultimap headers; const SimpleWeb::CaseInsensitiveMultimap headers {
headers.emplace("Content-Type", "application/json"); { "Content-Type", "application/json" }
};
response->write(content, headers); response->write(content, headers);
} }
@@ -344,8 +350,9 @@ namespace confighttp {
print_req(request); print_req(request);
std::string content = file_handler::read_file(config::sunshine.log_file.c_str()); std::string content = file_handler::read_file(config::sunshine.log_file.c_str());
SimpleWeb::CaseInsensitiveMultimap headers; const SimpleWeb::CaseInsensitiveMultimap headers {
headers.emplace("Content-Type", "text/plain"); { "Content-Type", "text/plain" }
};
response->write(SimpleWeb::StatusCode::success_ok, content, headers); response->write(SimpleWeb::StatusCode::success_ok, content, headers);
} }
@@ -691,6 +698,10 @@ namespace confighttp {
else { else {
http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword); http::save_user_creds(config::sunshine.credentials_file, newUsername, newPassword);
http::reload_user_creds(config::sunshine.credentials_file); http::reload_user_creds(config::sunshine.credentials_file);
// Force user to re-login
sessionCookie.clear();
outputTree.put("status", true); outputTree.put("status", true);
} }
} }
@@ -708,6 +719,48 @@ namespace confighttp {
} }
} }
void
login(resp_https_t response, req_https_t request) {
if (!checkIPOrigin(response, request)) {
return;
}
auto fg = util::fail_guard([&]{
response->write(SimpleWeb::StatusCode::client_error_unauthorized);
});
std::stringstream ss;
ss << request->content.rdbuf();
pt::ptree inputTree;
try {
pt::read_json(ss, inputTree);
std::string username = inputTree.get<std::string>("username");
std::string password = inputTree.get<std::string>("password");
std::string hash = util::hex(crypto::hash(password + config::sunshine.salt)).to_string();
if (!boost::iequals(username, config::sunshine.username) || hash != config::sunshine.password) {
return;
}
sessionCookie = crypto::rand_alphabet(64);
const SimpleWeb::CaseInsensitiveMultimap headers {
{ "Set-Cookie", "auth=" + sessionCookie + "; Secure; Max-Age=2592000; Path=/" }
};
response->write(headers);
fg.disable();
} catch (std::exception &e) {
BOOST_LOG(warning) << "Web UI Login failed: ["sv << net::addr_to_normalized_string(request->remote_endpoint().address()) << "]: "sv << e.what();
response->write(SimpleWeb::StatusCode::server_error_internal_server_error);
fg.disable();
return;
}
}
void void
savePin(resp_https_t response, req_https_t request) { savePin(resp_https_t response, req_https_t request) {
if (!authenticate(response, request)) return; if (!authenticate(response, request)) return;
@@ -886,11 +939,12 @@ namespace confighttp {
server.resource["^/$"]["GET"] = getIndexPage; server.resource["^/$"]["GET"] = getIndexPage;
server.resource["^/pin/?$"]["GET"] = getPinPage; server.resource["^/pin/?$"]["GET"] = getPinPage;
server.resource["^/apps/?$"]["GET"] = getAppsPage; server.resource["^/apps/?$"]["GET"] = getAppsPage;
server.resource["^/clients/?$"]["GET"] = getClientsPage;
server.resource["^/config/?$"]["GET"] = getConfigPage; server.resource["^/config/?$"]["GET"] = getConfigPage;
server.resource["^/password/?$"]["GET"] = getPasswordPage; server.resource["^/password/?$"]["GET"] = getPasswordPage;
server.resource["^/welcome/?$"]["GET"] = getWelcomePage; server.resource["^/welcome/?$"]["GET"] = getWelcomePage;
server.resource["^/login/?$"]["GET"] = getLoginPage;
server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage; server.resource["^/troubleshooting/?$"]["GET"] = getTroubleshootingPage;
server.resource["^/api/login"]["POST"] = login;
server.resource["^/api/pin$"]["POST"] = savePin; server.resource["^/api/pin$"]["POST"] = savePin;
server.resource["^/api/otp$"]["GET"] = getOTP; server.resource["^/api/otp$"]["GET"] = getOTP;
server.resource["^/api/apps$"]["GET"] = getApps; server.resource["^/api/apps$"]["GET"] = getApps;

View File

@@ -407,14 +407,18 @@
}; };
}, },
created() { created() {
fetch("/api/apps") fetch("/api/apps", {
credentials: 'include'
})
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then((r) => {
console.log(r); console.log(r);
this.apps = r.apps; this.apps = r.apps;
}); });
fetch("/api/config") fetch("/api/config", {
credentials: 'include'
})
.then(r => r.json()) .then(r => r.json())
.then(r => this.platform = r.platform); .then(r => this.platform = r.platform);
}, },
@@ -473,7 +477,10 @@
"Are you sure to delete " + this.apps[id].name + "?" "Are you sure to delete " + this.apps[id].name + "?"
); );
if (resp) { if (resp) {
fetch("/api/apps/" + id, { method: "DELETE" }).then((r) => { fetch("/api/apps/" + id, {
credentials: 'include',
method: "DELETE"
}).then((r) => {
if (r.status == 200) document.location.reload(); if (r.status == 200) document.location.reload();
}); });
} }
@@ -569,6 +576,7 @@
useCover(cover) { useCover(cover) {
this.coverFinderBusy = true; this.coverFinderBusy = true;
fetch("/api/covers/upload", { fetch("/api/covers/upload", {
credentials: 'include',
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
key: cover.key, key: cover.key,
@@ -584,6 +592,7 @@
save() { save() {
this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, ''); this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, '');
fetch("/api/apps", { fetch("/api/apps", {
credentials: 'include',
method: "POST", method: "POST",
body: JSON.stringify(this.editForm), body: JSON.stringify(this.editForm),
}).then((r) => { }).then((r) => {

View File

@@ -283,7 +283,9 @@
} }
}, },
created() { created() {
fetch("/api/config") fetch("/api/config", {
credentials: 'include'
})
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then((r) => {
this.config = r; this.config = r;
@@ -372,6 +374,7 @@
}); });
return fetch("/api/config", { return fetch("/api/config", {
credentials: 'include',
method: "POST", method: "POST",
body: JSON.stringify(config), body: JSON.stringify(config),
}).then((r) => { }).then((r) => {

View File

@@ -4,7 +4,7 @@ import {createI18n} from "vue-i18n";
import en from './public/assets/locale/en.json' import en from './public/assets/locale/en.json'
export default async function() { export default async function() {
let r = await (await fetch("/api/configLocale")).json(); let r = await (await fetch("/api/configLocale", { credentials: 'include' })).json();
let locale = r.locale ?? "en"; let locale = r.locale ?? "en";
document.querySelector('html').setAttribute('lang', locale); document.querySelector('html').setAttribute('lang', locale);
let messages = { let messages = {
@@ -12,7 +12,7 @@ export default async function() {
}; };
try { try {
if (locale !== 'en') { if (locale !== 'en') {
let r = await (await fetch(`/assets/locale/${locale}.json`)).json(); let r = await (await fetch(`/assets/locale/${locale}.json`, { credentials: 'include' })).json();
messages[locale] = r; messages[locale] = r;
} }
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<%- header %>
</head>
<body id="app" v-cloak>
<main role="main" style="max-width: 1200px; margin: 1em auto">
<div class="d-flex justify-content-center">
<div class="card p-2">
<header>
<h1 class="mb-0">
<img src="/images/logo-apollo-45.png" height="45" alt="">
{{ $t('welcome.greeting') }}
</h1>
</header>
<form @submit.prevent="save" class="mt-4">
<div class="mb-2">
<label for="usernameInput" class="form-label">{{ $t('_common.username') }}</label>
<input type="text" class="form-control" id="usernameInput" autocomplete="username"
v-model="passwordData.username" />
</div>
<div class="mb-4">
<label for="passwordInput" class="form-label">{{ $t('_common.password') }}</label>
<input type="password" class="form-control" id="passwordInput" autocomplete="new-password"
v-model="passwordData.password" required />
</div>
<button type="submit" class="btn btn-primary w-100 mb-2" v-bind:disabled="loading">
{{ $t('welcome.login') }}
</button>
<div class="alert alert-danger" v-if="error"><b>{{ $t('_common.error') }}</b> {{error}}</div>
<div class="alert alert-success" v-if="success">
<b>{{ $t('_common.success') }}</b> {{ $t('welcome.login_success') }}
</div>
</form>
</div>
</div>
</div>
</main>
</body>
<script type="module">
import { createApp } from "vue"
import { initApp } from './init'
let app = createApp({
data() {
return {
error: null,
success: false,
loading: false,
passwordData: {
username: "",
password: ""
},
};
},
methods: {
save() {
this.error = null;
this.loading = true;
fetch("/api/login", {
method: "POST",
body: JSON.stringify(this.passwordData),
}).then((res) => {
this.loading = false;
if (res.status === 200) {
this.success = true;
location.href = './';
} else {
throw new Error(`Server returned ${res.status}`);
}
}).catch((e) => {
this.error = `Login failed: ${e.message}`;
});
},
},
});
initApp(app);
</script>

View File

@@ -95,6 +95,7 @@
}; };
this.error = null; this.error = null;
fetch("/api/password", { fetch("/api/password", {
credentials: 'include',
method: "POST", method: "POST",
body: JSON.stringify(this.passwordData), body: JSON.stringify(this.passwordData),
}).then((r) => { }).then((r) => {

View File

@@ -78,7 +78,11 @@
let name = document.querySelector("#name-input").value; let name = document.querySelector("#name-input").value;
document.querySelector("#status").innerHTML = ""; document.querySelector("#status").innerHTML = "";
let b = JSON.stringify({pin: pin, name: name}); let b = JSON.stringify({pin: pin, name: name});
fetch("/api/pin", {method: "POST", body: b}) fetch("/api/pin", {
credentials: 'include',
method: "POST",
body: b
})
.then((response) => response.json()) .then((response) => response.json())
.then((response) => { .then((response) => {
if (response.status.toString().toLowerCase() === "true") { if (response.status.toString().toLowerCase() === "true") {
@@ -95,7 +99,9 @@
}); });
}, },
requestOTP() { requestOTP() {
fetch(`/api/otp?passphrase=${this.passphrase}${this.deviceName && `&deviceName=${this.deviceName}` || ''}`) fetch(`/api/otp?passphrase=${this.passphrase}${this.deviceName && `&deviceName=${this.deviceName}` || ''}`, {
credentials: 'include'
})
.then(resp => resp.json()) .then(resp => resp.json())
.then(resp => { .then(resp => {
if (resp.status !== 'true') { if (resp.status !== 'true') {

View File

@@ -423,6 +423,7 @@
"create_creds_alert": "The credentials below are needed to access Apollo's Web UI. Keep them safe, since you will never see them again!", "create_creds_alert": "The credentials below are needed to access Apollo's Web UI. Keep them safe, since you will never see them again!",
"greeting": "Welcome to Apollo!", "greeting": "Welcome to Apollo!",
"login": "Login", "login": "Login",
"welcome_success": "This page will reload soon, your browser will ask you for the new credentials" "welcome_success": "This page will reload soon, your browser will ask you for the new credentials",
"login_success": "This page will reload soon."
} }
} }

View File

@@ -424,6 +424,7 @@
"create_creds_alert": "需要下面的账户信息才能访问 Apollo 的 Web UI 。请妥善保存,因为你再也不会见到它们!", "create_creds_alert": "需要下面的账户信息才能访问 Apollo 的 Web UI 。请妥善保存,因为你再也不会见到它们!",
"greeting": "欢迎使用 Apollo", "greeting": "欢迎使用 Apollo",
"login": "登录", "login": "登录",
"welcome_success": "此页面将很快重新加载,您的浏览器将询问您新的账户信息" "welcome_success": "此页面将很快重新加载,您的浏览器将询问您新的账户信息",
"login_success": "此页面将重新加载。"
} }
} }

View File

@@ -163,7 +163,7 @@
logs: 'Loading...', logs: 'Loading...',
logFilter: null, logFilter: null,
logInterval: null, logInterval: null,
serterRestarting: false, serverRestarting: false,
serverQuitting: false, serverQuitting: false,
serverQuit: false, serverQuit: false,
showApplyMessage: false, showApplyMessage: false,
@@ -191,7 +191,7 @@
}, },
methods: { methods: {
refreshLogs() { refreshLogs() {
fetch("/api/logs",) fetch("/api/logs", { credentials: 'include' })
.then((r) => r.text()) .then((r) => r.text())
.then((r) => { .then((r) => {
this.logs = r; this.logs = r;
@@ -199,7 +199,10 @@
}, },
closeApp() { closeApp() {
this.closeAppPressed = true; this.closeAppPressed = true;
fetch("/api/apps/close", { method: "POST" }) fetch("/api/apps/close", {
credentials: 'include',
method: "POST"
})
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then((r) => {
this.closeAppPressed = false; this.closeAppPressed = false;
@@ -211,7 +214,10 @@
}, },
unpairAll() { unpairAll() {
this.unpairAllPressed = true; this.unpairAllPressed = true;
fetch("/api/clients/unpair-all", { method: "POST" }) fetch("/api/clients/unpair-all", {
credentials: 'include',
method: "POST"
})
.then((r) => r.json()) .then((r) => r.json())
.then((r) => { .then((r) => {
this.unpairAllPressed = false; this.unpairAllPressed = false;
@@ -223,13 +229,16 @@
}); });
}, },
unpairSingle(uuid) { unpairSingle(uuid) {
fetch("/api/clients/unpair", { method: "POST", body: JSON.stringify({ uuid }) }).then(() => { fetch("/api/clients/unpair", { credentials: 'include',
method: "POST",
body: JSON.stringify({ uuid })
}).then(() => {
this.showApplyMessage = true; this.showApplyMessage = true;
this.refreshClients(); this.refreshClients();
}); });
}, },
refreshClients() { refreshClients() {
fetch("/api/clients/list") fetch("/api/clients/list", { credentials: 'include' })
.then((response) => response.json()) .then((response) => response.json())
.then((response) => { .then((response) => {
const clientList = document.querySelector("#client-list"); const clientList = document.querySelector("#client-list");
@@ -254,13 +263,26 @@
this.serverRestarting = false; this.serverRestarting = false;
}, 5000); }, 5000);
fetch("/api/restart", { fetch("/api/restart", {
credentials: 'include',
method: "POST", method: "POST",
})
.then((resp) => {
if (resp.status !== 200) {
location.href = './login'
return
}
})
.catch((e) => {
this.serverRestarting = false;
console.error(e);
alert("Restart error!");
}); });
}, },
quit() { quit() {
if (window.confirm("Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.")) { if (window.confirm("Do you really want to quit Apollo? You'll not be able to start Apollo again if you have no other methods to operate your computer.")) {
this.serverQuitting = true; this.serverQuitting = true;
fetch("/api/quit", { fetch("/api/quit", {
credentials: 'include',
method: "POST", method: "POST",
}) })
.then(() => { .then(() => {

View File

@@ -53,6 +53,7 @@ export default defineConfig({
pin: resolve(assetsSrcPath, 'pin.html'), pin: resolve(assetsSrcPath, 'pin.html'),
troubleshooting: resolve(assetsSrcPath, 'troubleshooting.html'), troubleshooting: resolve(assetsSrcPath, 'troubleshooting.html'),
welcome: resolve(assetsSrcPath, 'welcome.html'), welcome: resolve(assetsSrcPath, 'welcome.html'),
login: resolve(assetsSrcPath, 'login.html')
}, },
}, },
}, },