diff --git a/.gitignore b/.gitignore
index c8b2f03..29b49ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
/build
/config
/logs
+node_modules/
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6259932
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,180 @@
+{
+ "name": "viaaas-web",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "viaaas-web",
+ "devDependencies": {
+ "@azure/msal-browser": "^2.27.0",
+ "@types/bootstrap": "^5.1.12",
+ "@types/jquery": "^3.5.14",
+ "@types/uuid": "^8.3.4",
+ "bootstrap": "^5.1.3",
+ "jquery": "^3.6.0",
+ "uuid": "^8.3.2"
+ }
+ },
+ "node_modules/@azure/msal-browser": {
+ "version": "2.27.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.27.0.tgz",
+ "integrity": "sha512-PyATq2WvK+x32waRqqikym8wvn939iO9UhpFqhLwitNrfLa3PHUgJuuI9oLSQOS3/UzjYb8aqN+XzchU3n/ZuQ==",
+ "dev": true,
+ "dependencies": {
+ "@azure/msal-common": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@azure/msal-common": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-7.1.0.tgz",
+ "integrity": "sha512-WyfqE5mY/rggjqvq0Q5DxLnA33KSb0vfsUjxa95rycFknI03L5GPYI4HTU9D+g0PL5TtsQGnV3xzAGq9BFCVJQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.5",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
+ "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@types/bootstrap": {
+ "version": "5.1.12",
+ "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.12.tgz",
+ "integrity": "sha512-pSS5BGEgepwzdbsBGswBWFmgrnYpp7c4UfuYe1FJWwkrcjm/JVwfG4gBkOYtd92Otd3RdJK0ByBWMkBROfLEPw==",
+ "dev": true,
+ "dependencies": {
+ "@popperjs/core": "^2.9.2"
+ }
+ },
+ "node_modules/@types/jquery": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
+ "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
+ "dev": true,
+ "dependencies": {
+ "@types/sizzle": "*"
+ }
+ },
+ "node_modules/@types/sizzle": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+ "dev": true
+ },
+ "node_modules/@types/uuid": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
+ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
+ "dev": true
+ },
+ "node_modules/bootstrap": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
+ "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ },
+ "peerDependencies": {
+ "@popperjs/core": "^2.10.2"
+ }
+ },
+ "node_modules/jquery": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
+ "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
+ "dev": true
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ }
+ },
+ "dependencies": {
+ "@azure/msal-browser": {
+ "version": "2.27.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.27.0.tgz",
+ "integrity": "sha512-PyATq2WvK+x32waRqqikym8wvn939iO9UhpFqhLwitNrfLa3PHUgJuuI9oLSQOS3/UzjYb8aqN+XzchU3n/ZuQ==",
+ "dev": true,
+ "requires": {
+ "@azure/msal-common": "^7.1.0"
+ }
+ },
+ "@azure/msal-common": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-7.1.0.tgz",
+ "integrity": "sha512-WyfqE5mY/rggjqvq0Q5DxLnA33KSb0vfsUjxa95rycFknI03L5GPYI4HTU9D+g0PL5TtsQGnV3xzAGq9BFCVJQ==",
+ "dev": true
+ },
+ "@popperjs/core": {
+ "version": "2.11.5",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz",
+ "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==",
+ "dev": true
+ },
+ "@types/bootstrap": {
+ "version": "5.1.12",
+ "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.1.12.tgz",
+ "integrity": "sha512-pSS5BGEgepwzdbsBGswBWFmgrnYpp7c4UfuYe1FJWwkrcjm/JVwfG4gBkOYtd92Otd3RdJK0ByBWMkBROfLEPw==",
+ "dev": true,
+ "requires": {
+ "@popperjs/core": "^2.9.2"
+ }
+ },
+ "@types/jquery": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.14.tgz",
+ "integrity": "sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==",
+ "dev": true,
+ "requires": {
+ "@types/sizzle": "*"
+ }
+ },
+ "@types/sizzle": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz",
+ "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==",
+ "dev": true
+ },
+ "@types/uuid": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
+ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
+ "dev": true
+ },
+ "bootstrap": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
+ "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
+ "dev": true,
+ "requires": {}
+ },
+ "jquery": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
+ "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
+ "dev": true
+ },
+ "uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..798fbf9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,17 @@
+{
+ "description": "Please use Gradle, NPM is mainly used for auto completing on IDE and update checking.",
+ "name": "viaaas-web",
+ "private": true,
+ "devDependencies": {
+ "@azure/msal-browser": "^2.27.0",
+ "@types/bootstrap": "^5.1.12",
+ "@types/jquery": "^3.5.14",
+ "@types/uuid": "^8.3.4",
+ "bootstrap": "^5.1.3",
+ "jquery": "^3.6.0",
+ "uuid": "^8.3.2"
+ },
+ "scripts": {
+ "start": "./gradlew run"
+ }
+}
diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html
index ff9cb77..5486161 100644
--- a/src/main/resources/web/index.html
+++ b/src/main/resources/web/index.html
@@ -17,7 +17,7 @@
style-src 'self' https://cdnjs.cloudflare.com/;
img-src 'self' data: https://crafthead.net/ https://crafatar.com/;
connect-src 'self' http://localhost:*/ https: wss:;
-script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*.cloudflareinsights.com/ 'unsafe-hashes' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=';
+script-src 'self' https://*.cloudflare.com/ https://alcdn.msauth.net/ https://*.cloudflareinsights.com/;
frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
http-equiv="Content-Security-Policy">
@@ -26,12 +26,13 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
-
+
@@ -41,8 +42,8 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
-
diff --git a/src/main/resources/web/js/page.js b/src/main/resources/web/js/page.js
index e15a3c9..ebefc0f 100644
--- a/src/main/resources/web/js/page.js
+++ b/src/main/resources/web/js/page.js
@@ -1,12 +1,11 @@
-// Minecraft.id
+"use strict";
let urlParams = new URLSearchParams();
window.location.hash.substring(1).split("?")
.map(it => new URLSearchParams(it)
- .forEach((a, b) => urlParams.append(b, a)));
+ .forEach((a, b) => urlParams.append(b, a)));
let mcIdUsername = urlParams.get("username");
let mcauth_code = urlParams.get("mcauth_code");
let mcauth_success = urlParams.get("mcauth_success");
-
$(() => {
if (mcauth_success === "false") {
addToast("Couldn't authenticate with Minecraft.ID", urlParams.get("mcauth_msg"));
@@ -15,8 +14,6 @@ $(() => {
history.replaceState(null, null, "#");
}
});
-
-// Page
let connectionStatus = document.getElementById("connection_status");
let corsStatus = document.getElementById("cors_status");
let listening = document.getElementById("listening");
@@ -24,13 +21,12 @@ let accounts = document.getElementById("accounts-list");
let cors_proxy_txt = document.getElementById("cors-proxy");
let ws_url_txt = document.getElementById("ws-url");
let listenVisible = false;
-// + deltaTime means that the clock is ahead
let deltaTime = 0;
let workers = [];
$(() => {
workers = new Array(navigator.hardwareConcurrency)
.fill(null)
- .map(() => new Worker("js/worker.js"))
+ .map(() => new Worker("js/worker.js"));
workers.forEach(it => it.onmessage = onWorkerMsg);
});
$(() => {
@@ -38,17 +34,13 @@ $(() => {
navigator.serviceWorker.register("sw.js")
.then(() => setTimeout(() => swRefreshFiles(), 1000));
}
-})
-
-window.addEventListener('beforeinstallprompt', e => e.preventDefault());
-
+});
$(() => {
$(".async-css").attr("rel", "stylesheet");
$("form").on("submit", e => e.preventDefault());
-
+ $("a[href='javascript:']").on("click", e => e.preventDefault());
cors_proxy_txt.value = getCorsProxy();
ws_url_txt.value = getWsUrl();
-
$("#form_add_mc").on("submit", () => loginMc($("#mc_email").val(), $("#mc_password").val()));
$("#form_add_ms").on("submit", () => loginMs());
$("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val()));
@@ -57,46 +49,40 @@ $(() => {
$("#form_send_token").on("submit", () => submittedSendToken());
$("#en_notific").on("click", () => Notification.requestPermission().then(renderActions));
$("#listen_continue").on("click", () => clickedListenContinue());
-
+ window.addEventListener('beforeinstallprompt', e => e.preventDefault());
ohNo();
-
refreshAccountList();
- setInterval(refreshCorsStatus, 10 * 60 * 1000); // Heroku auto sleeps in 30 min
+ setInterval(refreshCorsStatus, 10 * 60 * 1000);
refreshCorsStatus();
resetHtml();
});
-
$(() => {
connect();
-})
-
+});
function swRefreshFiles() {
- // https://stackoverflow.com/questions/46830493/is-there-any-way-to-cache-all-files-of-defined-folder-path-in-service-worker
navigator.serviceWorker.ready.then(ready => ready.active.postMessage({
action: "cache",
urls: performance.getEntriesByType("resource").map(it => it.name)
}));
}
-
function setWsStatus(txt) {
connectionStatus.innerText = txt;
}
-
function refreshCorsStatus() {
corsStatus.innerText = "...";
- icanhazip(true).then(ip => {
- return icanhazip(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip !== ip2 ? " (different IP)" : ""));
+ getIpAddress(true).then(ip => {
+ return getIpAddress(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip !== ip2 ? " (different IP)" : ""));
}).catch(e => corsStatus.innerText = "error: " + e);
}
-
function addMcAccountToList(account) {
let line = $(`
-
+
`);
let txt = account.name;
- if (account instanceof MicrosoftAccount) txt += " (" + account.msUser + ")";
+ if (account instanceof MicrosoftAccount)
+ txt += " (" + account.msUser + ")";
line.find(".mc-user").text(txt);
line.find(".mc-remove").on("click", () => account.logout());
let head = line.find(".mc-head");
@@ -104,14 +90,12 @@ function addMcAccountToList(account) {
head.attr("src", "https://crafthead.net/helm/" + account.id);
$(accounts).append(line);
}
-
function addUsernameList(username) {
let line = $("");
line.text(username);
$("#send_token_user").append(line);
$("#backend_user_list").append(line.clone());
}
-
function refreshAccountList() {
accounts.innerHTML = "";
$("#send_token_user .mc_username").remove();
@@ -119,41 +103,39 @@ function refreshAccountList() {
getActiveAccounts()
.sort((a, b) => a.name.localeCompare(b.name))
.forEach(it => {
- addMcAccountToList(it)
- addUsernameList(it.name)
- });
+ addMcAccountToList(it);
+ addUsernameList(it.name);
+ });
}
-
$("#mcIdUsername").text(mcIdUsername);
-
function submittedListen() {
let user = $("#listen_username").val();
- if (!user) return;
+ if (!user)
+ return;
if ($("#listen_online")[0].checked) {
- let callbackUrl = new URL(location);
+ let callbackUrl = new URL(location.href);
callbackUrl.search = "";
callbackUrl.hash = "#username=" + encodeURIComponent(user);
location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user)
+ "?callback=" + encodeURIComponent(callbackUrl.toString());
- } else {
+ }
+ else {
let taskId = Math.random();
- workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime}));
+ workers.forEach(it => it.postMessage({ action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime }));
addToast("Offline username", "Please wait a minute...");
}
}
-
function submittedSendToken() {
- findAccountByMcName($("#send_token_user").val())
- .acquireActiveToken()
- .then(acc => {
- sendSocket(JSON.stringify({
- "action": "save_access_token",
- "mc_access_token": acc.accessToken
- }))
- })
+ let account = findAccountByMcName($("#send_token_user").val());
+ account.acquireActiveToken()
+ .then(() => {
+ sendSocket(JSON.stringify({
+ "action": "save_access_token",
+ "mc_access_token": account.accessToken
+ }));
+ })
.catch(e => addToast("Failed to send access token", e));
}
-
function clickedListenContinue() {
sendSocket(JSON.stringify({
"action": "minecraft_id_login",
@@ -163,13 +145,11 @@ function clickedListenContinue() {
mcauth_code = null;
renderActions();
}
-
function renderActions() {
$("#en_notific").hide();
$("#listen_continue").hide();
$("#listen_open").hide();
$("#send_token_open").hide();
-
if (Notification.permission === "default") {
$("#en_notific").show();
}
@@ -181,31 +161,28 @@ function renderActions() {
$("#send_token_open").show();
}
}
-
function onWorkerMsg(e) {
- if (e.data.action === "completed_pow") onCompletedPoW(e);
+ if (e.data.action === "completed_pow")
+ onCompletedPoW(e);
}
-
function onCompletedPoW(e) {
addToast("Offline username", "Completed proof of work");
- workers.forEach(it => it.postMessage({action: "cancel", id: e.data.id}));
+ workers.forEach(it => it.postMessage({ action: "cancel", id: e.data.id }));
sendSocket(e.data.msg);
}
-
-function addListeningList(user, username, token) {
- let line = $("
");
- line.find(".username").text(username || user);
+function addListeningList(userId, username, token) {
+ let line = $("
");
+ line.find(".username").text(username || userId);
line.find(".btn").on("click", () => {
removeToken(token);
line.remove();
- unlisten(user);
+ unlisten(userId);
});
let head = line.find(".head");
- head.attr("alt", user + "'s head");
- head.attr("src", "https://crafthead.net/helm/" + user);
+ head.attr("alt", userId + "'s head");
+ head.attr("src", "https://crafthead.net/helm/" + userId);
$(listening).append(line);
}
-
function addToast(title, msg, yes = null, no = null) {
let toast = $(`
`);
toast.find(".toast_title_msg").text(title);
-
let tBody = toast.find(".toast-body");
tBody.find(".txt").text(msg);
-
let btns = $(tBody).find(".btns");
let hasButtons = false;
if (yes != null) {
@@ -239,17 +214,14 @@ function addToast(title, msg, yes = null, no = null) {
if (!hasButtons) {
btns.addClass("d-none");
}
-
$("#toasts").prepend(toast);
new bootstrap.Toast(toast[0]).show();
}
-
function resetHtml() {
listening.innerHTML = "";
listenVisible = false;
renderActions();
}
-
function ohNo() {
try {
icanhazepoch().then(sec => {
@@ -258,59 +230,56 @@ function ohNo() {
addToast("Time isn't synchronized", "Please synchronize your computer time to NTP servers");
deltaTime = calcDelta;
console.log("applying delta time " + deltaTime);
- } else {
+ }
+ else {
console.log("time seems synchronized");
}
- })
+ });
try {
new BroadcastChannel("test");
- } catch (e) {
+ }
+ catch (e) {
addToast("Unsupported browser", "This browser doesn't support required APIs");
}
new Date().getDay() === 3 && console.log("it's snapshot day 🐸 my dudes");
new Date().getDate() === 1 && new Date().getMonth() === 3 && addToast("LICENSE EXPIRED", "Your ViaVersion has expired, please renew it at https://viaversion.com/ for only $99");
- } catch (e) {
+ }
+ catch (e) {
console.log(e);
}
}
-
-// Util
function checkFetchSuccess(msg) {
- return r => {
- if (!r.ok) throw r.status + " " + msg;
+ return (r) => {
+ if (!r.ok)
+ throw r.status + " " + msg;
return r;
};
}
-
-function icanhazip(cors) {
+async function getIpAddress(cors) {
return fetch((cors ? getCorsProxy() : "") + "https://ipv4.icanhazip.com")
.then(checkFetchSuccess("code"))
.then(r => r.text())
.then(it => it.trim());
}
-
function icanhazepoch() {
return fetch("https://icanhazepoch.com")
.then(checkFetchSuccess("code"))
.then(r => r.text())
- .then(it => parseInt(it.trim()))
+ .then(it => parseInt(it.trim()));
}
-
-// Notification
-let notificationCallbacks = {};
+let notificationCallbacks = new Map();
$(() => {
new BroadcastChannel("viaaas-notification").addEventListener("message", handleSWMsg);
-})
-
+});
function handleSWMsg(event) {
console.log("sw msg: " + event);
let data = event.data;
- let callback = notificationCallbacks[data.tag];
- delete notificationCallbacks[data.tag];
- if (callback == null) return;
+ let callback = notificationCallbacks.get(data.tag);
+ notificationCallbacks.delete(data.tag);
+ if (callback == null)
+ return;
callback(data.action);
}
-
function authNotification(msg, yes, no) {
if (!navigator.serviceWorker || Notification.permission !== "granted") {
addToast("Allow auth impersonation?", msg, yes, no);
@@ -318,66 +287,57 @@ function authNotification(msg, yes, no) {
}
let tag = uuid.v4();
navigator.serviceWorker.ready.then(r => {
- r.showNotification("Click to allow auth impersionation", {
+ r.showNotification("Click to allow auth impersonation", {
body: msg,
tag: tag,
vibrate: [200, 10, 100, 200, 100, 10, 100, 10, 200],
actions: [
- {action: "reject", title: "Reject"},
- {action: "confirm", title: "Confirm"}
+ { action: "reject", title: "Reject" },
+ { action: "confirm", title: "Confirm" }
]
});
- notificationCallbacks[tag] = action => {
+ notificationCallbacks.set(tag, action => {
if (action === "reject") {
no();
- } else if (!action || action === "confirm") {
+ }
+ else if (!action || action === "confirm") {
yes();
}
- };
+ });
setTimeout(() => {
- delete notificationCallbacks[tag]
+ notificationCallbacks.delete(tag);
}, 30 * 1000);
});
}
-
-// Cors proxy
function defaultCors() {
return "https://crp123-cors.herokuapp.com/";
}
-
function getCorsProxy() {
return localStorage.getItem("viaaas_cors_proxy") || defaultCors();
}
-
function setCorsProxy(url) {
localStorage.setItem("viaaas_cors_proxy", url);
refreshCorsStatus();
}
-
-// Account manager
let activeAccounts = [];
-
function loadAccounts() {
- (JSON.parse(localStorage.getItem("viaaas_mc_accounts")) || []).forEach(it => {
+ (JSON.parse(localStorage.getItem("viaaas_mc_accounts")) || []).forEach((it) => {
if (it.clientToken) {
- addActiveAccount(new MojangAccount(it.id, it.name, it.accessToken, it.clientToken))
- } else if (it.msUser && myMSALObj.getAccountByUsername(it.msUser)) {
- addActiveAccount(new MicrosoftAccount(it.id, it.name, it.accessToken, it.msUser))
+ addActiveAccount(new MojangAccount(it.id, it.name, it.accessToken, it.clientToken));
}
- })
+ else if (it.msUser && myMSALObj.getAccountByUsername(it.msUser)) {
+ addActiveAccount(new MicrosoftAccount(it.id, it.name, it.accessToken, it.msUser));
+ }
+ });
}
-
$(() => loadAccounts());
-
function saveRefreshAccounts() {
- localStorage.setItem("viaaas_mc_accounts", JSON.stringify(getActiveAccounts()))
- refreshAccountList()
+ localStorage.setItem("viaaas_mc_accounts", JSON.stringify(getActiveAccounts()));
+ refreshAccountList();
}
-
function getActiveAccounts() {
return activeAccounts;
}
-
class McAccount {
constructor(id, username, accessToken) {
this.id = id;
@@ -385,232 +345,216 @@ class McAccount {
this.accessToken = accessToken;
this.loggedOut = false;
}
-
- logout() {
+ async logout() {
activeAccounts = activeAccounts.filter(it => it !== this);
saveRefreshAccounts();
this.loggedOut = true;
}
-
- checkActive() {
+ async checkActive() {
return fetch(getCorsProxy() + "https://authserver.mojang.com/validate", {
method: "post",
body: JSON.stringify({
accessToken: this.accessToken,
clientToken: this.clientToken || undefined
}),
- headers: {"content-type": "application/json"}
+ headers: { "content-type": "application/json" }
}).then(data => data.ok);
}
-
- joinGame(hash) {
- return this.acquireActiveToken()
+ async joinGame(hash) {
+ await this.acquireActiveToken()
.then(() => fetch(getCorsProxy() + "https://sessionserver.mojang.com/session/minecraft/join", {
- method: "post",
- body: JSON.stringify({
- accessToken: this.accessToken,
- selectedProfile: this.id,
- serverId: hash
- }),
- headers: {"content-type": "application/json"}
- })).then(checkFetchSuccess("Failed to join session"));
+ method: "post",
+ body: JSON.stringify({
+ accessToken: this.accessToken,
+ selectedProfile: this.id,
+ serverId: hash
+ }),
+ headers: { "content-type": "application/json" }
+ }))
+ .then(checkFetchSuccess("Failed to join session"));
}
-
- refresh() {
+ async refresh() {
}
-
- acquireActiveToken() {
- return this.checkActive().then(success => {
+ async acquireActiveToken() {
+ return this.checkActive()
+ .then(success => {
if (!success) {
- return this.refresh().then(() => this);
+ return this.refresh().then(() => {
+ });
}
- return this;
- }).catch(e => addToast("Failed to refresh token!", e));
+ return Promise.resolve();
+ })
+ .catch(e => addToast("Failed to refresh token!", e));
}
}
-
class MojangAccount extends McAccount {
constructor(id, username, accessToken, clientToken) {
super(id, username, accessToken);
this.clientToken = clientToken;
}
-
- logout() {
- super.logout();
- fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", {
+ async logout() {
+ await super.logout();
+ await fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", {
method: "post",
body: JSON.stringify({
accessToken: this.accessToken,
clientToken: this.clientToken
}),
- headers: {"content-type": "application/json"}
+ headers: { "content-type": "application/json" }
}).then(checkFetchSuccess("not success logout"));
}
-
- refresh() {
- super.refresh();
-
+ async refresh() {
console.log("refreshing " + this.id);
- return fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", {
+ let jsonResp = await fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", {
method: "post",
body: JSON.stringify({
accessToken: this.accessToken,
clientToken: this.clientToken
}),
- headers: {"content-type": "application/json"},
+ headers: { "content-type": "application/json" },
})
- .then(r => {
- if (r.status === 403) {
- this.logout();
- throw "403, token expired?";
+ .then(async (r) => {
+ if (r.status === 403) {
+ try {
+ await this.logout();
}
- return r;
- })
+ catch (e) {
+ console.error(e);
+ }
+ throw "403, token expired?";
+ }
+ return r;
+ })
.then(checkFetchSuccess("code"))
- .then(r => r.json())
- .then(json => {
- console.log("refreshed " + json.selectedProfile.id);
- this.accessToken = json.accessToken;
- this.clientToken = json.clientToken;
- this.name = json.selectedProfile.name;
- this.id = json.selectedProfile.id;
- saveRefreshAccounts();
- });
+ .then(r => r.json());
+ console.log("refreshed " + jsonResp.selectedProfile.id);
+ this.accessToken = jsonResp.accessToken;
+ this.clientToken = jsonResp.clientToken;
+ this.name = jsonResp.selectedProfile.name;
+ this.id = jsonResp.selectedProfile.id;
+ saveRefreshAccounts();
}
}
-
class MicrosoftAccount extends McAccount {
constructor(id, username, accessToken, msUser) {
super(id, username, accessToken);
this.msUser = msUser;
}
-
- logout() {
- super.logout();
-
+ async logout() {
+ await super.logout();
let msAccount = myMSALObj.getAccountByUsername(this.msUser);
- if (!msAccount) return;
-
- const logoutRequest = {account: msAccount};
- myMSALObj.logoutPopup(logoutRequest);
+ if (!msAccount)
+ return;
+ const logoutRequest = { account: msAccount };
+ await myMSALObj.logoutPopup(logoutRequest);
}
-
- refresh() {
- super.refresh();
- return getTokenPopup(this.msUser, getLoginRequest())
- .then(response => fetch("https://user.auth.xboxlive.com/user/authenticate", {
- method: "post",
- body: JSON.stringify({
- Properties: {
- AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com",
- RpsTicket: "d=" + response.accessToken
- }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"
- }),
- headers: {"content-type": "application/json"}
- })
- .then(checkFetchSuccess("xbox response not success"))
- .then(r => r.json()))
- .then(json => fetch("https://xsts.auth.xboxlive.com/xsts/authorize", {
- method: "post",
- body: JSON.stringify({
- Properties: {SandboxId: "RETAIL", UserTokens: [json.Token]},
- RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"
- }),
- headers: {"content-type": "application/json"}
- })
- .then(data => {
- if (data.status !== 401) return data;
- return data.json().then(errorData => {
- let error = errorData.XErr;
- switch (error) {
- case 2148916233:
- throw "Xbox account not found";
- case 2148916235:
- throw "Xbox Live not available in this country";
- case 2148916238:
- throw "Account is underage, add it to a family";
- }
- throw "xsts error code " + error;
- });
- })
- .then(checkFetchSuccess("xsts response not success"))
- .then(r => r.json()))
- .then(json => fetch(getCorsProxy() + "https://api.minecraftservices.com/authentication/login_with_xbox", {
- method: "post",
- body: JSON.stringify({identityToken: "XBL3.0 x=" + json.DisplayClaims.xui[0].uhs + ";" + json.Token}),
- headers: {"content-type": "application/json"}
- })
- .then(checkFetchSuccess("mc response not success"))
- .then(r => r.json()))
- .then(json => fetch(getCorsProxy() + "https://api.minecraftservices.com/minecraft/profile", {
- method: "get",
- headers: {"content-type": "application/json", "authorization": "Bearer " + json.access_token}
- })
- .then(profile => {
- if (profile.status === 404) throw "Minecraft profile not found";
- if (!profile.ok) throw "profile response not success " + profile.status;
- return profile.json();
- })
- .then(jsonProfile => {
- this.accessToken = json.access_token;
- this.name = jsonProfile.name;
- this.id = jsonProfile.id;
- saveRefreshAccounts();
- })
- );
+ async refresh() {
+ let msTokenResp = await getTokenPopup(this.msUser, getLoginRequest());
+ let xboxJson = await fetch("https://user.auth.xboxlive.com/user/authenticate", {
+ method: "post",
+ body: JSON.stringify({
+ Properties: {
+ AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com",
+ RpsTicket: "d=" + msTokenResp.accessToken
+ }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"
+ }),
+ headers: { "content-type": "application/json" }
+ })
+ .then(checkFetchSuccess("xbox response not success"))
+ .then(r => r.json());
+ let xstsJson = await fetch("https://xsts.auth.xboxlive.com/xsts/authorize", {
+ method: "post",
+ body: JSON.stringify({
+ Properties: { SandboxId: "RETAIL", UserTokens: [xboxJson.Token] },
+ RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"
+ }),
+ headers: { "content-type": "application/json" }
+ })
+ .then(resp => {
+ if (resp.status !== 401)
+ return resp;
+ return resp.json().then(errorData => {
+ let error = errorData.XErr;
+ switch (error) {
+ case 2148916233:
+ throw "Xbox account not found";
+ case 2148916235:
+ throw "Xbox Live not available in this country";
+ case 2148916238:
+ throw "Account is underage, add it to a family";
+ }
+ throw "xsts error code " + error;
+ });
+ })
+ .then(checkFetchSuccess("xsts response not success"))
+ .then(r => r.json());
+ let mcJson = await fetch(getCorsProxy() + "https://api.minecraftservices.com/authentication/login_with_xbox", {
+ method: "post",
+ body: JSON.stringify({ identityToken: "XBL3.0 x=" + xstsJson.DisplayClaims.xui[0].uhs + ";" + xstsJson.Token }),
+ headers: { "content-type": "application/json" }
+ })
+ .then(checkFetchSuccess("mc response not success"))
+ .then(r => r.json());
+ let jsonProfile = await fetch(getCorsProxy() + "https://api.minecraftservices.com/minecraft/profile", {
+ method: "get",
+ headers: { "content-type": "application/json", "authorization": "Bearer " + mcJson.access_token }
+ })
+ .then(profile => {
+ if (profile.status === 404)
+ throw "Minecraft profile not found";
+ if (!profile.ok)
+ throw "profile response not success " + profile.status;
+ return profile.json();
+ });
+ this.accessToken = mcJson.access_token;
+ this.name = jsonProfile.name;
+ this.id = jsonProfile.id;
+ saveRefreshAccounts();
}
-
- checkActive() {
+ async checkActive() {
return fetch(getCorsProxy() + "https://api.minecraftservices.com/entitlements/mcstore", {
method: "get",
- headers: {"authorization": "Bearer " + this.accessToken}
+ headers: { "authorization": "Bearer " + this.accessToken }
}).then(data => data.ok);
}
}
-
function findAccountByMcName(name) {
return activeAccounts.find(it => it.name.toLowerCase() === name.toLowerCase());
}
-
function findAccountByMs(username) {
return getActiveAccounts().find(it => it.msUser === username);
}
-
function addActiveAccount(acc) {
- activeAccounts.push(acc)
- saveRefreshAccounts()
+ activeAccounts.push(acc);
+ saveRefreshAccounts();
}
-
function loginMc(user, pass) {
const clientToken = uuid.v4();
fetch(getCorsProxy() + "https://authserver.mojang.com/authenticate", {
method: "post",
body: JSON.stringify({
- agent: {name: "Minecraft", version: 1},
+ agent: { name: "Minecraft", version: 1 },
username: user,
password: pass,
clientToken: clientToken,
}),
- headers: {"content-type": "application/json"}
+ headers: { "content-type": "application/json" }
}).then(checkFetchSuccess("code"))
.then(r => r.json())
.then(data => {
- let acc = new MojangAccount(data.selectedProfile.id, data.selectedProfile.name, data.accessToken, data.clientToken);
- addActiveAccount(acc);
- return acc;
- }).catch(e => addToast("Failed to login", e));
+ let acc = new MojangAccount(data.selectedProfile.id, data.selectedProfile.name, data.accessToken, data.clientToken);
+ addActiveAccount(acc);
+ return acc;
+ }).catch(e => addToast("Failed to login", e));
$("#form_add_mc input").val("");
}
-
function getLoginRequest() {
- return {scopes: ["XboxLive.signin"]};
+ return { scopes: ["XboxLive.signin"] };
}
-
let redirectUrl = "https://viaversion.github.io/VIAaaS/src/main/resources/web/";
if (location.hostname === "localhost" || whitelistedOrigin.includes(location.origin)) {
redirectUrl = location.origin + location.pathname;
}
-
const msalConfig = {
auth: {
clientId: azureClientId,
@@ -622,65 +566,56 @@ const msalConfig = {
storeAuthStateInCookie: false,
}
};
-
const myMSALObj = new msal.PublicClientApplication(msalConfig);
-
function loginMs() {
let req = getLoginRequest();
- req.prompt = "select_account";
+ req["prompt"] = "select_account";
myMSALObj.loginRedirect(req);
}
-
$(() => myMSALObj.handleRedirectPromise().then((resp) => {
if (resp) {
- let found = findAccountByMs(resp.account.username)
+ let found = findAccountByMs(resp.account.username);
if (!found) {
let accNew = new MicrosoftAccount("", "", "", resp.account.username);
accNew.refresh()
.then(() => addActiveAccount(accNew))
.catch(e => addToast("Failed to get token", e));
- } else {
+ }
+ else {
found.refresh()
.catch(e => addToast("Failed to refresh token", e));
}
}
}));
-
function getTokenPopup(username, request) {
request.account = myMSALObj.getAccountByUsername(username);
request.loginHint = username;
- return myMSALObj.acquireTokenSilent(request).catch(error => {
+ return myMSALObj.acquireTokenSilent(request)
+ .catch((e) => {
console.warn("silent token acquisition fails.");
if (error instanceof msal.InteractionRequiredAuthError) {
- // fallback to interaction when silent call fails
- return myMSALObj.acquireTokenPopup(request).catch(error => console.error(error));
- } else {
- console.warn(error);
+ return myMSALObj.acquireTokenPopup(request).catch((error) => console.error(error));
+ }
+ else {
+ console.warn(e);
}
});
}
-
-// Websocket
let wsUrl = getWsUrl();
let socket = null;
-
function defaultWs() {
- let url = new URL("ws", new URL(location));
+ let url = new URL("ws", location.href);
url.protocol = "wss";
return window.location.host.endsWith("github.io") || !window.location.protocol.startsWith("http")
? "wss://localhost:25543/ws" : url.toString();
}
-
function getWsUrl() {
return localStorage.getItem("viaaas_ws_url") || defaultWs();
}
-
function setWsUrl(url) {
localStorage.setItem("viaaas_ws_url", url);
location.reload();
}
-
-// Tokens
function saveToken(token) {
let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {};
let tokens = getTokens();
@@ -688,7 +623,6 @@ function saveToken(token) {
hTokens[wsUrl] = tokens;
localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens));
}
-
function removeToken(token) {
let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {};
let tokens = getTokens();
@@ -696,42 +630,35 @@ function removeToken(token) {
hTokens[wsUrl] = tokens;
localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens));
}
-
function getTokens() {
return (JSON.parse(localStorage.getItem("viaaas_tokens")) || {})[wsUrl] || [];
}
-
-// Websocket
function listen(token) {
- socket.send(JSON.stringify({"action": "listen_login_requests", "token": token}));
+ socket.send(JSON.stringify({ "action": "listen_login_requests", "token": token }));
}
-
function unlisten(id) {
- socket.send(JSON.stringify({"action": "unlisten_login_requests", "uuid": id}));
+ socket.send(JSON.stringify({ "action": "unlisten_login_requests", "uuid": id }));
}
-
function confirmJoin(hash) {
- socket.send(JSON.stringify({action: "session_hash_response", session_hash: hash}));
+ socket.send(JSON.stringify({ action: "session_hash_response", session_hash: hash }));
}
-
function handleJoinRequest(parsed) {
authNotification("Allow auth impersonation from VIAaaS instance?\nAccount: "
+ parsed.user + "\nServer Message: \n"
- + parsed.message.split(/[\r\n]+/).map(it => "> " + it).join('\n'), () => {
+ + parsed.message.split(/[\r\n]+/).map((it) => "> " + it).join('\n'), () => {
let account = findAccountByMcName(parsed.user);
if (account) {
account.joinGame(parsed.session_hash)
- .then(checkFetchSuccess("code"))
.finally(() => confirmJoin(parsed.session_hash))
.catch((e) => addToast("Couldn't contact session server", "Error: " + e));
- } else {
+ }
+ else {
confirmJoin(parsed.session_hash);
addToast("Couldn't find account", "Couldn't find " + parsed.user + ", check Accounts tab");
}
}, () => confirmJoin(parsed.session_hash));
}
-
-function onSocketMsg(event) {
+function onWsMsg(event) {
let parsed = JSON.parse(event.data);
switch (parsed.action) {
case "ad_login_methods":
@@ -741,7 +668,8 @@ function onSocketMsg(event) {
case "login_result":
if (!parsed.success) {
addToast("Couldn't verify Minecraft account", "VIAaaS returned failed response");
- } else {
+ }
+ else {
listen(parsed.token);
saveToken(parsed.token);
}
@@ -749,7 +677,8 @@ function onSocketMsg(event) {
case "listen_login_requests_result":
if (parsed.success) {
addListeningList(parsed.user, parsed.username, parsed.token);
- } else {
+ }
+ else {
removeToken(parsed.token);
}
break;
@@ -761,7 +690,6 @@ function onSocketMsg(event) {
break;
}
}
-
function handleParametersRequest(parsed) {
let url = new URL("https://" + $("#connect_address").val());
socket.send(JSON.stringify({
@@ -769,48 +697,41 @@ function handleParametersRequest(parsed) {
callback: parsed["callback"],
version: $("#connect_version").val(),
host: url.hostname,
- port: parseInt(url.port || 25565),
+ port: parseInt(url.port) || 25565,
frontOnline: $("#connect_online").val(),
backName: $("#connect_user").val() || undefined
}));
}
-
function listenStoredTokens() {
getTokens().forEach(listen);
}
-
-function onConnect() {
+function onWsConnect() {
setWsStatus("connected");
resetHtml();
listenStoredTokens();
}
-
function onWsError(e) {
console.log(e);
setWsStatus("socket error");
resetHtml();
}
-
-function onDisconnect(evt) {
+function onWsClose(evt) {
setWsStatus("disconnected with close code " + evt.code + " and reason: " + evt.reason);
resetHtml();
setTimeout(connect, 5000);
}
-
function connect() {
setWsStatus("connecting...");
socket = new WebSocket(wsUrl);
-
socket.onerror = onWsError;
- socket.onopen = onConnect;
- socket.onclose = onDisconnect
- socket.onmessage = onSocketMsg;
+ socket.onopen = onWsConnect;
+ socket.onclose = onWsClose;
+ socket.onmessage = onWsMsg;
}
-
function sendSocket(msg) {
if (!socket) {
console.error("couldn't send msg, socket isn't set");
- return
+ return;
}
socket.send(msg);
}
diff --git a/src/main/resources/web/js/page.ts b/src/main/resources/web/js/page.ts
new file mode 100644
index 0000000..8ae34e1
--- /dev/null
+++ b/src/main/resources/web/js/page.ts
@@ -0,0 +1,836 @@
+///
+"use strict"
+
+// Minecraft.id
+let urlParams = new URLSearchParams();
+window.location.hash.substring(1).split("?")
+ .map(it => new URLSearchParams(it)
+ .forEach((a, b) => urlParams.append(b, a)));
+let mcIdUsername = urlParams.get("username");
+let mcauth_code = urlParams.get("mcauth_code");
+let mcauth_success = urlParams.get("mcauth_success");
+
+$(() => {
+ if (mcauth_success === "false") {
+ addToast("Couldn't authenticate with Minecraft.ID", urlParams.get("mcauth_msg"));
+ }
+ if (mcauth_code != null) {
+ history.replaceState(null, null, "#");
+ }
+});
+
+// Page
+let connectionStatus = document.getElementById("connection_status");
+let corsStatus = document.getElementById("cors_status");
+let listening = document.getElementById("listening");
+let accounts = document.getElementById("accounts-list");
+let cors_proxy_txt = document.getElementById("cors-proxy") as HTMLInputElement;
+let ws_url_txt = document.getElementById("ws-url") as HTMLInputElement;
+let listenVisible = false;
+// + deltaTime means that the clock is ahead
+let deltaTime = 0;
+let workers: Array = [];
+$(() => {
+ workers = new Array(navigator.hardwareConcurrency)
+ .fill(null)
+ .map(() => new Worker("js/worker.js"))
+ workers.forEach(it => it.onmessage = onWorkerMsg);
+});
+$(() => {
+ if (navigator.serviceWorker) {
+ navigator.serviceWorker.register("sw.js")
+ .then(() => setTimeout(() => swRefreshFiles(), 1000));
+ }
+})
+
+$(() => {
+ $(".async-css").attr("rel", "stylesheet");
+ $("form").on("submit", e => e.preventDefault());
+ $("a[href='javascript:']").on("click", e => e.preventDefault());
+
+ cors_proxy_txt.value = getCorsProxy();
+ ws_url_txt.value = getWsUrl();
+
+ $("#form_add_mc").on("submit", () => loginMc($("#mc_email").val() as string, $("#mc_password").val() as string));
+ $("#form_add_ms").on("submit", () => loginMs());
+ $("#form_ws_url").on("submit", () => setWsUrl($("#ws-url").val() as string));
+ $("#form_cors_proxy").on("submit", () => setCorsProxy($("#cors-proxy").val() as string));
+ $("#form_listen").on("submit", () => submittedListen());
+ $("#form_send_token").on("submit", () => submittedSendToken());
+ $("#en_notific").on("click", () => Notification.requestPermission().then(renderActions));
+ $("#listen_continue").on("click", () => clickedListenContinue());
+ window.addEventListener('beforeinstallprompt', e => e.preventDefault());
+
+ ohNo();
+
+ refreshAccountList();
+ setInterval(refreshCorsStatus, 10 * 60 * 1000); // Heroku auto sleeps in 30 min
+ refreshCorsStatus();
+ resetHtml();
+});
+
+$(() => {
+ connect();
+})
+
+function swRefreshFiles() {
+ // https://stackoverflow.com/questions/46830493/is-there-any-way-to-cache-all-files-of-defined-folder-path-in-service-worker
+ navigator.serviceWorker.ready.then(ready => ready.active.postMessage({
+ action: "cache",
+ urls: performance.getEntriesByType("resource").map(it => it.name)
+ }));
+}
+
+function setWsStatus(txt: string) {
+ connectionStatus.innerText = txt;
+}
+
+function refreshCorsStatus() {
+ corsStatus.innerText = "...";
+ getIpAddress(true).then(ip => {
+ return getIpAddress(false).then(ip2 => corsStatus.innerText = "OK " + ip + (ip !== ip2 ? " (different IP)" : ""));
+ }).catch(e => corsStatus.innerText = "error: " + e);
+}
+
+function addMcAccountToList(account: McAccount) {
+ let line = $(`
+
+
+
+ `);
+ let txt = account.name;
+ if (account instanceof MicrosoftAccount) txt += " (" + account.msUser + ")";
+ line.find(".mc-user").text(txt);
+ line.find(".mc-remove").on("click", () => account.logout());
+ let head = line.find(".mc-head");
+ head.attr("alt", account.name + "'s head");
+ head.attr("src", "https://crafthead.net/helm/" + account.id);
+ $(accounts).append(line);
+}
+
+function addUsernameList(username: string) {
+ let line = $("");
+ line.text(username);
+ $("#send_token_user").append(line);
+ $("#backend_user_list").append(line.clone());
+}
+
+function refreshAccountList() {
+ accounts.innerHTML = "";
+ $("#send_token_user .mc_username").remove();
+ $("#backend_user_list .mc_username").remove();
+ getActiveAccounts()
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .forEach(it => {
+ addMcAccountToList(it)
+ addUsernameList(it.name)
+ });
+}
+
+$("#mcIdUsername").text(mcIdUsername);
+
+function submittedListen() {
+ let user = $("#listen_username").val() as string;
+ if (!user) return;
+ if (($("#listen_online")[0] as HTMLInputElement).checked) {
+ let callbackUrl = new URL(location.href);
+ callbackUrl.search = "";
+ callbackUrl.hash = "#username=" + encodeURIComponent(user);
+ location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user)
+ + "?callback=" + encodeURIComponent(callbackUrl.toString());
+ } else {
+ let taskId = Math.random();
+ workers.forEach(it => it.postMessage({action: "listen_pow", user: user, id: taskId, deltaTime: deltaTime}));
+ addToast("Offline username", "Please wait a minute...");
+ }
+}
+
+function submittedSendToken() {
+ let account = findAccountByMcName($("#send_token_user").val() as string)
+ account.acquireActiveToken()
+ .then(() => {
+ sendSocket(JSON.stringify({
+ "action": "save_access_token",
+ "mc_access_token": account.accessToken
+ }))
+ })
+ .catch(e => addToast("Failed to send access token", e));
+}
+
+function clickedListenContinue() {
+ sendSocket(JSON.stringify({
+ "action": "minecraft_id_login",
+ "username": mcIdUsername,
+ "code": mcauth_code
+ }));
+ mcauth_code = null;
+ renderActions();
+}
+
+function renderActions() {
+ $("#en_notific").hide();
+ $("#listen_continue").hide();
+ $("#listen_open").hide();
+ $("#send_token_open").hide();
+
+ if (Notification.permission === "default") {
+ $("#en_notific").show();
+ }
+ if (listenVisible) {
+ if (mcIdUsername != null && mcauth_code != null) {
+ $("#listen_continue").show();
+ }
+ $("#listen_open").show();
+ $("#send_token_open").show();
+ }
+}
+
+function onWorkerMsg(e: MessageEvent) {
+ if (e.data.action === "completed_pow") onCompletedPoW(e);
+}
+
+function onCompletedPoW(e: MessageEvent) {
+ addToast("Offline username", "Completed proof of work");
+ workers.forEach(it => it.postMessage({action: "cancel", id: e.data.id}));
+ sendSocket(e.data.msg);
+}
+
+function addListeningList(userId: string, username: string, token: string) {
+ let line = $("
");
+ line.find(".username").text(username || userId);
+ line.find(".btn").on("click", () => {
+ removeToken(token);
+ line.remove();
+ unlisten(userId);
+ });
+ let head = line.find(".head");
+ head.attr("alt", userId + "'s head");
+ head.attr("src", "https://crafthead.net/helm/" + userId);
+ $(listening).append(line);
+}
+
+function addToast(title: string, msg: string, yes: () => void = null, no: () => void = null) {
+ let toast = $(``);
+ toast.find(".toast_title_msg").text(title);
+
+ let tBody = toast.find(".toast-body");
+ tBody.find(".txt").text(msg);
+
+ let btns = $(tBody).find(".btns");
+ let hasButtons = false;
+ if (yes != null) {
+ hasButtons = true;
+ let btn = $("");
+ btn.on("click", yes);
+ btns.append(btn);
+ }
+ if (no != null) {
+ hasButtons = true;
+ let btn = $("");
+ btn.on("click", no);
+ btns.append(btn);
+ }
+ if (!hasButtons) {
+ btns.addClass("d-none");
+ }
+
+ $("#toasts").prepend(toast);
+ // @ts-ignore
+ new bootstrap.Toast(toast[0]).show();
+}
+
+function resetHtml() {
+ listening.innerHTML = "";
+ listenVisible = false;
+ renderActions();
+}
+
+function ohNo() {
+ try {
+ icanhazepoch().then(sec => {
+ const calcDelta = Date.now() - sec * 1000;
+ if (Math.abs(calcDelta) > 10000) {
+ addToast("Time isn't synchronized", "Please synchronize your computer time to NTP servers");
+ deltaTime = calcDelta;
+ console.log("applying delta time " + deltaTime);
+ } else {
+ console.log("time seems synchronized");
+ }
+ })
+ try {
+ new BroadcastChannel("test");
+ } catch (e) {
+ addToast("Unsupported browser", "This browser doesn't support required APIs");
+ }
+ new Date().getDay() === 3 && console.log("it's snapshot day 🐸 my dudes");
+ new Date().getDate() === 1 && new Date().getMonth() === 3 && addToast("LICENSE EXPIRED", "Your ViaVersion has expired, please renew it at https://viaversion.com/ for only $99");
+ } catch (e) {
+ console.log(e);
+ }
+}
+
+// Util
+function checkFetchSuccess(msg: String): (a: Response) => Response {
+ return (r: Response): Response => {
+ if (!r.ok) throw r.status + " " + msg;
+ return r;
+ };
+}
+
+async function getIpAddress(cors: boolean): Promise {
+ return fetch((cors ? getCorsProxy() : "") + "https://ipv4.icanhazip.com")
+ .then(checkFetchSuccess("code"))
+ .then(r => r.text())
+ .then(it => it.trim());
+}
+
+function icanhazepoch() {
+ return fetch("https://icanhazepoch.com")
+ .then(checkFetchSuccess("code"))
+ .then(r => r.text())
+ .then(it => parseInt(it.trim()))
+}
+
+// Notification
+let notificationCallbacks: Map void> = new Map();
+$(() => {
+ new BroadcastChannel("viaaas-notification").addEventListener("message", handleSWMsg);
+})
+
+function handleSWMsg(event: MessageEvent) {
+ console.log("sw msg: " + event);
+ let data = event.data;
+ let callback = notificationCallbacks.get(data.tag as string);
+ notificationCallbacks.delete(data.tag as string);
+ if (callback == null) return;
+ callback(data.action);
+}
+
+function authNotification(msg: string, yes: () => void, no: () => void) {
+ if (!navigator.serviceWorker || Notification.permission !== "granted") {
+ addToast("Allow auth impersonation?", msg, yes, no);
+ return;
+ }
+ // @ts-ignore
+ let tag = uuid.v4();
+ navigator.serviceWorker.ready.then(r => {
+ r.showNotification("Click to allow auth impersonation", {
+ body: msg,
+ tag: tag,
+ vibrate: [200, 10, 100, 200, 100, 10, 100, 10, 200],
+ actions: [
+ {action: "reject", title: "Reject"},
+ {action: "confirm", title: "Confirm"}
+ ]
+ });
+ notificationCallbacks.set(tag, action => {
+ if (action === "reject") {
+ no();
+ } else if (!action || action === "confirm") {
+ yes();
+ }
+ });
+ setTimeout(() => {
+ notificationCallbacks.delete(tag);
+ }, 30 * 1000);
+ });
+}
+
+// Cors proxy
+function defaultCors() {
+ return "https://crp123-cors.herokuapp.com/";
+}
+
+function getCorsProxy() {
+ return localStorage.getItem("viaaas_cors_proxy") || defaultCors();
+}
+
+function setCorsProxy(url: string) {
+ localStorage.setItem("viaaas_cors_proxy", url);
+ refreshCorsStatus();
+}
+
+// Account manager
+let activeAccounts: Array = [];
+
+function loadAccounts() {
+ (JSON.parse(localStorage.getItem("viaaas_mc_accounts")) || []).forEach((it: any) => {
+ if (it.clientToken) {
+ addActiveAccount(new MojangAccount(it.id, it.name, it.accessToken, it.clientToken))
+ } else if (it.msUser && myMSALObj.getAccountByUsername(it.msUser)) {
+ addActiveAccount(new MicrosoftAccount(it.id, it.name, it.accessToken, it.msUser))
+ }
+ })
+}
+
+$(() => loadAccounts());
+
+function saveRefreshAccounts() {
+ localStorage.setItem("viaaas_mc_accounts", JSON.stringify(getActiveAccounts()))
+ refreshAccountList()
+}
+
+function getActiveAccounts() {
+ return activeAccounts;
+}
+
+class McAccount {
+ public id: string;
+ public name: string;
+ public accessToken: string;
+ public loggedOut: boolean;
+
+ constructor(id: string, username: string, accessToken: string) {
+ this.id = id;
+ this.name = username;
+ this.accessToken = accessToken;
+ this.loggedOut = false;
+ }
+
+ async logout(): Promise {
+ activeAccounts = activeAccounts.filter(it => it !== this);
+ saveRefreshAccounts();
+ this.loggedOut = true;
+ }
+
+ async checkActive(): Promise {
+ return fetch(getCorsProxy() + "https://authserver.mojang.com/validate", {
+ method: "post",
+ body: JSON.stringify({
+ accessToken: this.accessToken,
+ clientToken: (this as any).clientToken || undefined
+ }),
+ headers: {"content-type": "application/json"}
+ }).then(data => data.ok);
+ }
+
+ async joinGame(hash: string): Promise {
+ await this.acquireActiveToken()
+ .then(() => fetch(getCorsProxy() + "https://sessionserver.mojang.com/session/minecraft/join", {
+ method: "post",
+ body: JSON.stringify({
+ accessToken: this.accessToken,
+ selectedProfile: this.id,
+ serverId: hash
+ }),
+ headers: {"content-type": "application/json"}
+ }))
+ .then(checkFetchSuccess("Failed to join session"));
+ }
+
+ async refresh(): Promise {
+ }
+
+ async acquireActiveToken(): Promise {
+ return this.checkActive()
+ .then(success => {
+ if (!success) {
+ return this.refresh().then(() => {
+ });
+ }
+ return Promise.resolve();
+ })
+ .catch(e => addToast("Failed to refresh token!", e));
+ }
+}
+
+class MojangAccount extends McAccount {
+ public clientToken: string;
+
+ constructor(id: string, username: string, accessToken: string, clientToken: string) {
+ super(id, username, accessToken);
+ this.clientToken = clientToken;
+ }
+
+ override async logout() {
+ await super.logout();
+ await fetch(getCorsProxy() + "https://authserver.mojang.com/invalidate", {
+ method: "post",
+ body: JSON.stringify({
+ accessToken: this.accessToken,
+ clientToken: this.clientToken
+ }),
+ headers: {"content-type": "application/json"}
+ }).then(checkFetchSuccess("not success logout"));
+ }
+
+ override async refresh() {
+ console.log("refreshing " + this.id);
+ let jsonResp = await fetch(getCorsProxy() + "https://authserver.mojang.com/refresh", {
+ method: "post",
+ body: JSON.stringify({
+ accessToken: this.accessToken,
+ clientToken: this.clientToken
+ }),
+ headers: {"content-type": "application/json"},
+ })
+ .then(async r => {
+ if (r.status === 403) {
+ try {
+ await this.logout();
+ } catch (e) {
+ console.error(e);
+ }
+ throw "403, token expired?";
+ }
+ return r;
+ })
+ .then(checkFetchSuccess("code"))
+ .then(r => r.json());
+
+ console.log("refreshed " + jsonResp.selectedProfile.id);
+ this.accessToken = jsonResp.accessToken;
+ this.clientToken = jsonResp.clientToken;
+ this.name = jsonResp.selectedProfile.name;
+ this.id = jsonResp.selectedProfile.id;
+ saveRefreshAccounts();
+ }
+}
+
+class MicrosoftAccount extends McAccount {
+ public msUser: string;
+
+ constructor(id: string, username: string, accessToken: string, msUser: string) {
+ super(id, username, accessToken);
+ this.msUser = msUser;
+ }
+
+ override async logout() {
+ await super.logout();
+
+ let msAccount = myMSALObj.getAccountByUsername(this.msUser);
+ if (!msAccount) return;
+
+ const logoutRequest = {account: msAccount};
+ await myMSALObj.logoutPopup(logoutRequest);
+ }
+
+ override async refresh(): Promise {
+ let msTokenResp = await getTokenPopup(this.msUser, getLoginRequest());
+ // noinspection HttpUrlsUsage
+ let xboxJson = await fetch("https://user.auth.xboxlive.com/user/authenticate", {
+ method: "post",
+ body: JSON.stringify({
+ Properties: {
+ AuthMethod: "RPS", SiteName: "user.auth.xboxlive.com",
+ RpsTicket: "d=" + msTokenResp.accessToken
+ }, RelyingParty: "http://auth.xboxlive.com", TokenType: "JWT"
+ }),
+ headers: {"content-type": "application/json"}
+ })
+ .then(checkFetchSuccess("xbox response not success"))
+ .then(r => r.json());
+ let xstsJson = await fetch("https://xsts.auth.xboxlive.com/xsts/authorize", {
+ method: "post",
+ body: JSON.stringify({
+ Properties: {SandboxId: "RETAIL", UserTokens: [xboxJson.Token]},
+ RelyingParty: "rp://api.minecraftservices.com/", TokenType: "JWT"
+ }),
+ headers: {"content-type": "application/json"}
+ })
+ .then(resp => {
+ if (resp.status !== 401) return resp;
+ return resp.json().then(errorData => {
+ let error = errorData.XErr;
+ switch (error) {
+ case 2148916233:
+ throw "Xbox account not found";
+ case 2148916235:
+ throw "Xbox Live not available in this country";
+ case 2148916238:
+ throw "Account is underage, add it to a family";
+ }
+ throw "xsts error code " + error;
+ });
+ })
+ .then(checkFetchSuccess("xsts response not success"))
+ .then(r => r.json());
+ let mcJson = await fetch(getCorsProxy() + "https://api.minecraftservices.com/authentication/login_with_xbox", {
+ method: "post",
+ body: JSON.stringify({identityToken: "XBL3.0 x=" + xstsJson.DisplayClaims.xui[0].uhs + ";" + xstsJson.Token}),
+ headers: {"content-type": "application/json"}
+ })
+ .then(checkFetchSuccess("mc response not success"))
+ .then(r => r.json());
+ let jsonProfile = await fetch(getCorsProxy() + "https://api.minecraftservices.com/minecraft/profile", {
+ method: "get",
+ headers: {"content-type": "application/json", "authorization": "Bearer " + mcJson.access_token}
+ })
+ .then(profile => {
+ if (profile.status === 404) throw "Minecraft profile not found";
+ if (!profile.ok) throw "profile response not success " + profile.status;
+ return profile.json();
+ });
+
+ this.accessToken = mcJson.access_token;
+ this.name = jsonProfile.name;
+ this.id = jsonProfile.id;
+ saveRefreshAccounts();
+ }
+
+ override async checkActive() {
+ return fetch(getCorsProxy() + "https://api.minecraftservices.com/entitlements/mcstore", {
+ method: "get",
+ headers: {"authorization": "Bearer " + this.accessToken}
+ }).then(data => data.ok);
+ }
+}
+
+function findAccountByMcName(name: string) {
+ return activeAccounts.find(it => it.name.toLowerCase() === name.toLowerCase());
+}
+
+function findAccountByMs(username: string) {
+ return getActiveAccounts().find(it => (it as any).msUser === username);
+}
+
+function addActiveAccount(acc: McAccount) {
+ activeAccounts.push(acc)
+ saveRefreshAccounts()
+}
+
+function loginMc(user: string, pass: string) {
+ // @ts-ignore
+ const clientToken = uuid.v4();
+ fetch(getCorsProxy() + "https://authserver.mojang.com/authenticate", {
+ method: "post",
+ body: JSON.stringify({
+ agent: {name: "Minecraft", version: 1},
+ username: user,
+ password: pass,
+ clientToken: clientToken,
+ }),
+ headers: {"content-type": "application/json"}
+ }).then(checkFetchSuccess("code"))
+ .then(r => r.json())
+ .then(data => {
+ let acc = new MojangAccount(data.selectedProfile.id, data.selectedProfile.name, data.accessToken, data.clientToken);
+ addActiveAccount(acc);
+ return acc;
+ }).catch(e => addToast("Failed to login", e));
+ $("#form_add_mc input").val("");
+}
+
+function getLoginRequest() {
+ return {scopes: ["XboxLive.signin"]};
+}
+
+let redirectUrl = "https://viaversion.github.io/VIAaaS/src/main/resources/web/";
+if (location.hostname === "localhost" || whitelistedOrigin.includes(location.origin)) {
+ redirectUrl = location.origin + location.pathname;
+}
+
+const msalConfig = {
+ auth: {
+ clientId: azureClientId,
+ authority: "https://login.microsoftonline.com/consumers/",
+ redirectUri: redirectUrl,
+ },
+ cache: {
+ cacheLocation: "localStorage",
+ storeAuthStateInCookie: false,
+ }
+};
+
+// @ts-ignore
+const myMSALObj = new msal.PublicClientApplication(msalConfig);
+
+function loginMs() {
+ let req = getLoginRequest();
+ (req as any)["prompt"] = "select_account";
+ myMSALObj.loginRedirect(req);
+}
+
+$(() => myMSALObj.handleRedirectPromise().then((resp: any) => {
+ if (resp) {
+ let found = findAccountByMs(resp.account.username)
+ if (!found) {
+ let accNew = new MicrosoftAccount("", "", "", resp.account.username);
+ accNew.refresh()
+ .then(() => addActiveAccount(accNew))
+ .catch(e => addToast("Failed to get token", e));
+ } else {
+ found.refresh()
+ .catch(e => addToast("Failed to refresh token", e));
+ }
+ }
+}));
+
+function getTokenPopup(username: string, request: any) {
+ request.account = myMSALObj.getAccountByUsername(username);
+ request.loginHint = username;
+ return myMSALObj.acquireTokenSilent(request)
+ .catch((e: any) => {
+ console.warn("silent token acquisition fails.");
+ // @ts-ignore
+ if (error instanceof msal.InteractionRequiredAuthError) {
+ // fallback to interaction when silent call fails
+ return myMSALObj.acquireTokenPopup(request).catch((error: any) => console.error(error));
+ } else {
+ console.warn(e);
+ }
+ });
+}
+
+// Websocket
+let wsUrl = getWsUrl();
+let socket: WebSocket | null = null;
+
+function defaultWs() {
+ let url = new URL("ws", location.href);
+ url.protocol = "wss";
+ return window.location.host.endsWith("github.io") || !window.location.protocol.startsWith("http")
+ ? "wss://localhost:25543/ws" : url.toString();
+}
+
+function getWsUrl() {
+ return localStorage.getItem("viaaas_ws_url") || defaultWs();
+}
+
+function setWsUrl(url: string) {
+ localStorage.setItem("viaaas_ws_url", url);
+ location.reload();
+}
+
+// Tokens
+function saveToken(token: string) {
+ let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {};
+ let tokens = getTokens();
+ tokens.push(token);
+ hTokens[wsUrl] = tokens;
+ localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens));
+}
+
+function removeToken(token: string) {
+ let hTokens = JSON.parse(localStorage.getItem("viaaas_tokens")) || {};
+ let tokens = getTokens();
+ tokens = tokens.filter(it => it !== token);
+ hTokens[wsUrl] = tokens;
+ localStorage.setItem("viaaas_tokens", JSON.stringify(hTokens));
+}
+
+function getTokens(): Array {
+ return (JSON.parse(localStorage.getItem("viaaas_tokens")) || {})[wsUrl] || [];
+}
+
+// Websocket
+function listen(token: String) {
+ socket.send(JSON.stringify({"action": "listen_login_requests", "token": token}));
+}
+
+function unlisten(id: String) {
+ socket.send(JSON.stringify({"action": "unlisten_login_requests", "uuid": id}));
+}
+
+function confirmJoin(hash: String) {
+ socket.send(JSON.stringify({action: "session_hash_response", session_hash: hash}));
+}
+
+function handleJoinRequest(parsed: any) {
+ authNotification("Allow auth impersonation from VIAaaS instance?\nAccount: "
+ + parsed.user + "\nServer Message: \n"
+ + parsed.message.split(/[\r\n]+/).map((it: string) => "> " + it).join('\n'), () => {
+ let account = findAccountByMcName(parsed.user);
+ if (account) {
+ account.joinGame(parsed.session_hash)
+ .finally(() => confirmJoin(parsed.session_hash))
+ .catch((e) => addToast("Couldn't contact session server", "Error: " + e));
+ } else {
+ confirmJoin(parsed.session_hash);
+ addToast("Couldn't find account", "Couldn't find " + parsed.user + ", check Accounts tab");
+ }
+ }, () => confirmJoin(parsed.session_hash));
+}
+
+function onWsMsg(event: MessageEvent) {
+ let parsed = JSON.parse(event.data);
+ switch (parsed.action) {
+ case "ad_login_methods":
+ listenVisible = true;
+ renderActions();
+ break;
+ case "login_result":
+ if (!parsed.success) {
+ addToast("Couldn't verify Minecraft account", "VIAaaS returned failed response");
+ } else {
+ listen(parsed.token);
+ saveToken(parsed.token);
+ }
+ break;
+ case "listen_login_requests_result":
+ if (parsed.success) {
+ addListeningList(parsed.user, parsed.username, parsed.token);
+ } else {
+ removeToken(parsed.token);
+ }
+ break;
+ case "session_hash_request":
+ handleJoinRequest(parsed);
+ break;
+ case "parameters_request":
+ handleParametersRequest(parsed);
+ break;
+ }
+}
+
+function handleParametersRequest(parsed: any) {
+ let url = new URL("https://" + $("#connect_address").val());
+ socket.send(JSON.stringify({
+ action: "parameters_response",
+ callback: parsed["callback"],
+ version: $("#connect_version").val(),
+ host: url.hostname,
+ port: parseInt(url.port) || 25565,
+ frontOnline: $("#connect_online").val(),
+ backName: $("#connect_user").val() || undefined
+ }));
+}
+
+function listenStoredTokens() {
+ getTokens().forEach(listen);
+}
+
+function onWsConnect() {
+ setWsStatus("connected");
+ resetHtml();
+ listenStoredTokens();
+}
+
+function onWsError(e: any) {
+ console.log(e);
+ setWsStatus("socket error");
+ resetHtml();
+}
+
+function onWsClose(evt: CloseEvent) {
+ setWsStatus("disconnected with close code " + evt.code + " and reason: " + evt.reason);
+ resetHtml();
+ setTimeout(connect, 5000);
+}
+
+function connect() {
+ setWsStatus("connecting...");
+ socket = new WebSocket(wsUrl);
+
+ socket.onerror = onWsError;
+ socket.onopen = onWsConnect;
+ socket.onclose = onWsClose;
+ socket.onmessage = onWsMsg;
+}
+
+function sendSocket(msg: string) {
+ if (!socket) {
+ console.error("couldn't send msg, socket isn't set");
+ return
+ }
+ socket.send(msg);
+}
diff --git a/src/main/resources/web/js/worker.js b/src/main/resources/web/js/worker.js
index 0f34163..e8642c4 100644
--- a/src/main/resources/web/js/worker.js
+++ b/src/main/resources/web/js/worker.js
@@ -1,11 +1,12 @@
+"use strict";
importScripts("https://cdnjs.cloudflare.com/ajax/libs/js-sha512/0.8.0/sha512.min.js");
let pending = [];
-onmessage = function (e) {
+self.addEventListener("message", e => {
if (e.data.action === "listen_pow") startPoW(e);
if (e.data.action === "cancel") removePending(e.data.id);
-}
+});
function removePending(id) {
pending = pending.filter(it => it !== id);
@@ -16,12 +17,16 @@ function startPoW(e) {
listenPoW(e);
}
+function isPending(id) {
+ return pending.includes(id);
+}
+
function listenPoW(e) {
let user = e.data.user;
let msg = null;
let endTime = Date.now() + 1000;
do {
- if (!pending.includes(e.data.id)) return; // cancelled
+ if (!isPending(e.data.id)) return; // cancelled
msg = JSON.stringify({
action: "offline_login",
@@ -31,10 +36,13 @@ function listenPoW(e) {
});
if (Date.now() >= endTime) {
- setTimeout(() => listenPoW(e), 0);
+ setTimeout(() => listenPoW(e));
return;
}
} while (!sha512(msg).startsWith("00000"));
- postMessage({id: e.data.id, action: "completed_pow", msg: msg});
+ setTimeout(() => {
+ if (!isPending(e.data.id)) return;
+ postMessage({id: e.data.id, action: "completed_pow", msg: msg});
+ })
}
\ No newline at end of file
diff --git a/src/main/resources/web/tsconfig.json b/src/main/resources/web/tsconfig.json
new file mode 100644
index 0000000..d866106
--- /dev/null
+++ b/src/main/resources/web/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ES2018",
+ "module": "ES2015",
+ "lib": [
+ "ES2018",
+ "DOM"
+ ],
+ "moduleResolution": "Node",
+ "noImplicitAny": true,
+ "noImplicitOverride": true,
+ "noImplicitThis": true,
+ "noImplicitReturns": true,
+ "removeComments": true
+ },
+ "include": [
+ "js/**.ts"
+ ]
+}
\ No newline at end of file