replace minecraft.id with kick code

also simplify text on the web page
closes #39
This commit is contained in:
creeper123123321 2024-06-12 15:26:50 -03:00
parent ee58150767
commit e6676c93d9
11 changed files with 88 additions and 127 deletions

View File

@ -13,6 +13,7 @@ import com.viaversion.aas.handler.forward
import com.viaversion.aas.handler.setCompression import com.viaversion.aas.handler.setCompression
import com.viaversion.aas.util.IntendedState import com.viaversion.aas.util.IntendedState
import com.viaversion.aas.util.StacklessException import com.viaversion.aas.util.StacklessException
import com.viaversion.aas.web.TempLoginInfo
import com.viaversion.viaversion.api.protocol.packet.State import com.viaversion.viaversion.api.protocol.packet.State
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
import com.viaversion.viaversion.api.type.Type import com.viaversion.viaversion.api.type.Type
@ -281,7 +282,8 @@ class LoginState : ConnectionState {
frontOnline = info.frontOnline frontOnline = info.frontOnline
info.backName?.also { backName = info.backName } info.backName?.also { backName = info.backName }
} }
if (VIAaaSConfig.forceOnlineMode) frontOnline = true val isLink = backAddress!!.host == "link"
if (VIAaaSConfig.forceOnlineMode || isLink) frontOnline = true
if (frontOnline != null) { if (frontOnline != null) {
when (frontOnline) { when (frontOnline) {
false -> callbackPlayerId.complete(generateOfflinePlayerUuid(frontName)) false -> callbackPlayerId.complete(generateOfflinePlayerUuid(frontName))
@ -290,6 +292,7 @@ class LoginState : ConnectionState {
} }
val id = callbackPlayerId.await() val id = callbackPlayerId.await()
mcLogger.info("Login: {} {} {}", handler.endRemoteAddress, frontName, id) mcLogger.info("Login: {} {} {}", handler.endRemoteAddress, frontName, id)
if (isLink) return@launch handleTempCode(handler, frontName, id)
} }
connectBack( connectBack(
handler, handler,
@ -308,6 +311,12 @@ class LoginState : ConnectionState {
} }
} }
private fun handleTempCode(handler: MinecraftHandler, name: String, id: UUID) {
val info = TempLoginInfo(secureRandom.nextInt().toUInt().toString(36), name, id)
AspirinServer.viaWebServer.tempCodes.put(name.lowercase(), info)
handler.disconnect("Your temp code is: ${info.tempCode}")
}
override fun disconnect(handler: MinecraftHandler, msg: String) { override fun disconnect(handler: MinecraftHandler, msg: String) {
super.disconnect(handler, msg) super.disconnect(handler, msg)

View File

@ -76,7 +76,9 @@ class StatusState : ConnectionState {
handler.data.frontChannel.setAutoRead(false) handler.data.frontChannel.setAutoRead(false)
handler.coroutineScope.launch(Dispatchers.IO) { handler.coroutineScope.launch(Dispatchers.IO) {
try { try {
if (address != null) { if (address?.host == "link") {
handler.disconnect("Join to link your account")
} else if (address != null) {
connectBack(handler, address!!, IntendedState.STATUS) connectBack(handler, address!!, IntendedState.STATUS)
} else { } else {
handler.disconnect("VIAaaS") handler.disconnect("VIAaaS")

View File

@ -0,0 +1,5 @@
package com.viaversion.aas.web
import java.util.UUID
data class TempLoginInfo(val tempCode: String, val username: String, val id: UUID)

View File

@ -9,11 +9,7 @@ import com.viaversion.aas.util.StacklessException
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion import com.viaversion.viaversion.api.protocol.version.ProtocolVersion
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import kotlinx.coroutines.future.await
import java.net.URLEncoder
import java.time.Duration import java.time.Duration
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -31,7 +27,7 @@ class WebLogin : WebState {
when (obj["action"].asString) { when (obj["action"].asString) {
"offline_login" -> handleOfflineLogin(webClient, msg, obj) "offline_login" -> handleOfflineLogin(webClient, msg, obj)
"minecraft_id_login" -> handleMcIdLogin(webClient, obj) "temp_code_login" -> handleTempCodeLogin(webClient, obj)
"listen_login_requests" -> handleListenLogins(webClient, obj) "listen_login_requests" -> handleListenLogins(webClient, obj)
"unlisten_login_requests" -> handleUnlisten(webClient, obj) "unlisten_login_requests" -> handleUnlisten(webClient, obj)
"session_hash_response" -> handleSessionResponse(webClient, obj) "session_hash_response" -> handleSessionResponse(webClient, obj)
@ -83,25 +79,22 @@ class WebLogin : WebState {
webLogger.info("Token gen: {}: offline {} {}", webClient.id, username, uuid) webLogger.info("Token gen: {}: offline {} {}", webClient.id, username, uuid)
} }
private suspend fun handleMcIdLogin(webClient: WebClient, obj: JsonObject) { private suspend fun handleTempCodeLogin(webClient: WebClient, obj: JsonObject) {
val username = obj["username"].asString val username = obj["username"].asString
val code = obj["code"].asString val code = obj["code"].asString
val check = AspirinServer.httpClient.submitForm( val cacheKey = username.lowercase()
"https://api.minecraft.id/gateway/verify/${URLEncoder.encode(username, Charsets.UTF_8)}", val check = webClient.server.tempCodes.getIfPresent(cacheKey)
formParameters = parametersOf("code", code),
).body<JsonObject>()
if (check["valid"].asBoolean) { if (check != null && check.tempCode == code) {
val mcIdUser = check["username"].asString webClient.server.tempCodes.invalidate(cacheKey)
val uuid = check["uuid"]?.asString?.let { parseUndashedId(it.replace("-", "")) } val mcIdUser = check.username
?: webClient.server.usernameToIdCache[mcIdUser].await() val uuid = check.id
?: throw StacklessException("Failed to get UUID from minecraft.id")
val token = webClient.server.generateToken(uuid, mcIdUser) val token = webClient.server.generateToken(uuid, mcIdUser)
webClient.ws.sendSerialized(loginSuccessJson(mcIdUser, uuid, token)) webClient.ws.sendSerialized(loginSuccessJson(mcIdUser, uuid, token))
webLogger.info("Token gen: {}: {} {}", webClient.id, mcIdUser, uuid) webLogger.info("Token gen: {}: temp code {} {}", webClient.id, mcIdUser, uuid)
} else { } else {
webClient.ws.sendSerialized(loginNotSuccess()) webClient.ws.sendSerialized(loginNotSuccess())
webLogger.info("Token gen fail: {}: {}", webClient.id, username) webLogger.info("Token gen fail: {}: {}", webClient.id, username)

View File

@ -75,6 +75,9 @@ class WebServer {
val minecraftAccessTokens = CacheBuilder.newBuilder() val minecraftAccessTokens = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) .expireAfterWrite(10, TimeUnit.MINUTES)
.build<UUID, String>() .build<UUID, String>()
val tempCodes = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build<String, TempLoginInfo>() // lowercase username
fun generateToken(account: UUID, username: String): String { fun generateToken(account: UUID, username: String): String {
return JWT.create() return JWT.create()

View File

@ -97,9 +97,6 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<ul class="list-group" id="listening"></ul> <ul class="list-group" id="listening"></ul>
</div> </div>
<div id="actions"> <div id="actions">
<button type="button" class="btn btn-primary" id="listen_continue">Listen to <span
id="mcIdUsername"></span>
</button>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" <button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-bs-target="#listenModal" data-bs-target="#listenModal"
id="listen_open">Listen to logins id="listen_open">Listen to logins
@ -348,11 +345,7 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button> <button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>The instance will send impersonation requests to this page. Please note <p>The instance will send auth requests and address parameters to this page.</p>
that they're verified by UUID, so offline and online accounts
are considered different.</p>
<p>VIAaaS will also request the address parameters when they're not specified.
They're verified by username. Online-mode listeners have priority.</p>
<div class="mb-3"> <div class="mb-3">
<label for="listen_username" class="form-label">Username</label> <label for="listen_username" class="form-label">Username</label>
<input type="text" class="form-control" id="listen_username"> <input type="text" class="form-control" id="listen_username">
@ -361,6 +354,11 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<input type="checkbox" class="form-check-input" id="listen_online" checked> <input type="checkbox" class="form-check-input" id="listen_online" checked>
<label for="listen_online" class="form-check-label">Online Mode</label> <label for="listen_online" class="form-check-label">Online Mode</label>
</div> </div>
<div class="mb-3">
<label for="listen_code" class="form-label">Temp Code</label>
<p>Join <span id="link_address" class="font-monospace"></span> to get the temporary code</p>
<input type="text" class="form-control" id="listen_code">
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal">Listen</button> <button type="submit" class="btn btn-primary" data-bs-dismiss="modal">Listen</button>
@ -379,12 +377,9 @@ frame-src 'self' https://login.microsoftonline.com/ https://login.live.com/"
<button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button> <button aria-label="Close" class="btn-close" data-bs-dismiss="modal" type="button"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>The instance will cache your Minecraft access token for some minutes, allowing you to connect <p>The instance will cache your Minecraft access token, usually for some minutes, allowing you to connect
without keeping this page open.</p> without keeping this page open.</p>
<p>Note that the access token are valid for ~1 day and a compromised instance may be used to access <p>You'll need to connect as online-mode in the front-end.</p>
your account.</p>
<p>You'll need to connect as online-mode in the front-end. It's not possible to use the cached
token when changing the backend username</p>
<div class="input-group mb-3"> <div class="input-group mb-3">
<label class="input-group-text" for="send_token_user">Account</label> <label class="input-group-text" for="send_token_user">Account</label>
<select class="form-select" id="send_token_user"> <select class="form-select" id="send_token_user">

View File

@ -12,4 +12,3 @@ const whitelistedOrigin = [
var defaultCorsProxy = "https://cors.re.yt.nom.br/"; var defaultCorsProxy = "https://cors.re.yt.nom.br/";
// Default instance suffix, in format "viaaas.example.com[:25565]", null to use the page hostname; // Default instance suffix, in format "viaaas.example.com[:25565]", null to use the page hostname;
var defaultInstanceSuffix = null; var defaultInstanceSuffix = null;
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHlwZXNjcmlwdC9qcy9jb25maWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLG9HQUFvRztBQUNwRyxzRkFBc0Y7QUFDdEYsdURBQXVEO0FBRXZELGtCQUFrQjtBQUNsQixNQUFNLGFBQWEsR0FBVyxzQ0FBc0MsQ0FBQztBQUNyRSxrRkFBa0Y7QUFDbEYsTUFBTSxpQkFBaUIsR0FBYTtJQUNoQywwQkFBMEI7Q0FDN0IsQ0FBQztBQUNGLDRCQUE0QjtBQUM1QixJQUFJLGdCQUFnQixHQUFrQiw0QkFBNEIsQ0FBQztBQUNuRSxrR0FBa0c7QUFDbEcsSUFBSSxxQkFBcUIsR0FBa0IsSUFBSSxDQUFDIn0=

File diff suppressed because one or more lines are too long

View File

@ -41,4 +41,3 @@ function listenPoW(e) {
postMessage({ id: e.data.id, action: "completed_pow", msg: msg }); postMessage({ id: e.data.id, action: "completed_pow", msg: msg });
}); });
} }
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoid29ya2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHlwZXNjcmlwdC9qcy93b3JrZXIuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsWUFBWSxDQUFDO0FBQ2IsYUFBYSxDQUFDLHNFQUFzRSxDQUFDLENBQUM7QUFFdEYsSUFBSSxPQUFPLEdBQUcsRUFBRSxDQUFDO0FBRWpCLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDakMsSUFBSSxDQUFDLENBQUMsSUFBSSxDQUFDLE1BQU0sS0FBSyxZQUFZO1FBQUUsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ2hELElBQUksQ0FBQyxDQUFDLElBQUksQ0FBQyxNQUFNLEtBQUssUUFBUTtRQUFFLGFBQWEsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQzdELENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxhQUFhLENBQUMsRUFBRTtJQUNyQixPQUFPLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztBQUM5QyxDQUFDO0FBRUQsU0FBUyxRQUFRLENBQUMsQ0FBQztJQUNmLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUN4QixTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDakIsQ0FBQztBQUVELFNBQVMsU0FBUyxDQUFDLEVBQUU7SUFDakIsT0FBTyxPQUFPLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQ2hDLENBQUM7QUFFRCxTQUFTLFNBQVMsQ0FBQyxDQUFDO0lBQ2hCLElBQUksSUFBSSxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDO0lBQ3ZCLElBQUksR0FBRyxHQUFHLElBQUksQ0FBQztJQUNmLElBQUksT0FBTyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUM7SUFDaEMsR0FBRztRQUNDLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFBRSxPQUFPLENBQUMsWUFBWTtRQUUvQyxHQUFHLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQztZQUNqQixNQUFNLEVBQUUsZUFBZTtZQUN2QixRQUFRLEVBQUUsSUFBSTtZQUNkLElBQUksRUFBRSxJQUFJLENBQUMsR0FBRyxFQUFFLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxTQUFTO1lBQ25DLElBQUksRUFBRSxJQUFJLENBQUMsTUFBTSxFQUFFO1NBQ3RCLENBQUMsQ0FBQztRQUVILElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxJQUFJLE9BQU8sRUFBRTtZQUN2QixVQUFVLENBQUMsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7WUFDL0IsT0FBTztTQUNWO0tBQ0osUUFBUSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxVQUFVLENBQUMsT0FBTyxDQUFDLEVBQUU7SUFFM0MsVUFBVSxDQUFDLEdBQUcsRUFBRTtRQUNaLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFBRSxPQUFPO1FBQ2xDLFdBQVcsQ0FBQyxFQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLEVBQUUsRUFBRSxNQUFNLEVBQUUsZUFBZSxFQUFFLEdBQUcsRUFBRSxHQUFHLEVBQUMsQ0FBQyxDQUFDO0lBQ3BFLENBQUMsQ0FBQyxDQUFBO0FBQ04sQ0FBQyJ9

View File

@ -1,24 +1,6 @@
/// <reference path='config.ts' /> /// <reference path='config.ts' />
// Note that some APIs only work on HTTPS // Note that some APIs only work on HTTPS
// 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 mcIdCode = urlParams.get("mcauth_code");
let mcIdSuccess = urlParams.get("mcauth_success");
$(() => {
if (mcIdSuccess === "false") {
addToast("Couldn't authenticate with Minecraft.ID", urlParams.get("mcauth_msg") ?? "");
}
if (mcIdCode != null) {
history.replaceState(null, "", "#");
}
});
// Page // Page
let connectionStatus = document.getElementById("connection_status")!!; let connectionStatus = document.getElementById("connection_status")!!;
let corsStatus = document.getElementById("cors_status")!!; let corsStatus = document.getElementById("cors_status")!!;
@ -68,9 +50,10 @@ $(() => {
$("#form_listen").on("submit", () => submittedListen()); $("#form_listen").on("submit", () => submittedListen());
$("#form_send_token").on("submit", () => submittedSendToken()); $("#form_send_token").on("submit", () => submittedSendToken());
$("#en_notifications").on("click", () => Notification.requestPermission().then(renderActions)); $("#en_notifications").on("click", () => Notification.requestPermission().then(renderActions));
$("#listen_continue").on("click", () => clickedListenContinue());
$("#address_info_form").on("input", () => generateAddress()); $("#address_info_form").on("input", () => generateAddress());
$("#generated_address").on("click", () => copyGeneratedAddress()); $("#generated_address").on("click", () => copyGeneratedAddress());
$("#listen_online").on("change", () => updateTempCodeVisibility());
$("#link_address").text("link." + instance_suffix_input.value);
$(window).on("beforeinstallprompt", e => e.preventDefault()); $(window).on("beforeinstallprompt", e => e.preventDefault());
ohNo(); ohNo();
@ -161,17 +144,16 @@ function generateAddress() {
} }
} }
$("#mcIdUsername").text(mcIdUsername ?? "");
function submittedListen() { function submittedListen() {
let user = $("#listen_username").val() as string; let user = $("#listen_username").val() as string;
if (!user) return; if (!user) return;
if (($("#listen_online")[0] as HTMLInputElement).checked) { if (($("#listen_online")[0] as HTMLInputElement).checked) {
let callbackUrl = new URL(location.href); sendSocket(JSON.stringify({
callbackUrl.search = ""; action: "temp_code_login",
callbackUrl.hash = "#username=" + encodeURIComponent(user); username: user,
location.href = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) code: $("#listen_code").val()
+ "?callback=" + encodeURIComponent(callbackUrl.toString()); }));
// todo
} else { } else {
let taskId = Math.random(); 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}));
@ -192,19 +174,8 @@ function submittedSendToken() {
.catch(e => addToast("Failed to send access token", e)); .catch(e => addToast("Failed to send access token", e));
} }
function clickedListenContinue() {
sendSocket(JSON.stringify({
"action": "minecraft_id_login",
"username": mcIdUsername,
"code": mcIdCode
}));
mcIdCode = null;
renderActions();
}
function renderActions() { function renderActions() {
$("#en_notifications").hide(); $("#en_notifications").hide();
$("#listen_continue").hide();
$("#listen_open").hide(); $("#listen_open").hide();
$("#send_token_open").hide(); $("#send_token_open").hide();
@ -212,14 +183,20 @@ function renderActions() {
$("#en_notifications").show(); $("#en_notifications").show();
} }
if (listenVisible) { if (listenVisible) {
if (mcIdUsername != null && mcIdCode != null) {
$("#listen_continue").show();
}
$("#listen_open").show(); $("#listen_open").show();
$("#send_token_open").show(); $("#send_token_open").show();
} }
} }
function updateTempCodeVisibility() {
let tmpCode = $("#listen_code");
if (($("#listen_online")[0] as HTMLInputElement).checked) {
tmpCode.prop("disabled", false);
} else {
tmpCode.prop("disabled", true);
}
}
function onWorkerMsg(e: MessageEvent) { function onWorkerMsg(e: MessageEvent) {
if (e.data.action === "completed_pow") onCompletedPoW(e); if (e.data.action === "completed_pow") onCompletedPoW(e);
} }
@ -356,7 +333,7 @@ function handleSWMsg(event: MessageEvent) {
function authNotification(msg: string, yes: () => void, no: () => void) { function authNotification(msg: string, yes: () => void, no: () => void) {
if (!navigator.serviceWorker || Notification.permission !== "granted") { if (!navigator.serviceWorker || Notification.permission !== "granted") {
addToast("Allow auth impersonation?", msg, yes, no); addToast("Allow auth?", msg, yes, no);
return; return;
} }
// @ts-ignore // @ts-ignore
@ -371,7 +348,7 @@ function authNotification(msg: string, yes: () => void, no: () => void) {
{action: "confirm", title: "Confirm"} {action: "confirm", title: "Confirm"}
] ]
} }
r.showNotification("Click to allow auth impersonation", options).then(() => { r.showNotification("Click to allow auth", options).then(() => {
}); });
notificationCallbacks.set(tag, action => { notificationCallbacks.set(tag, action => {
if (action === "reject") { if (action === "reject") {
@ -704,7 +681,7 @@ function confirmJoin(hash: string) {
} }
function handleJoinRequest(parsed: any) { function handleJoinRequest(parsed: any) {
authNotification("Allow auth impersonation from VIAaaS instance?\nAccount: " authNotification("Allow auth from VIAaaS instance?\nAccount: "
+ parsed.user + "\nServer Message: \n" + parsed.user + "\nServer Message: \n"
+ parsed.message.split(/[\r\n]+/).map((it: string) => "> " + it).join('\n'), () => { + parsed.message.split(/[\r\n]+/).map((it: string) => "> " + it).join('\n'), () => {
let account = findAccountByMcName(parsed.user); let account = findAccountByMcName(parsed.user);

View File

@ -15,7 +15,6 @@
"alwaysStrict": true, "alwaysStrict": true,
"strict": true, "strict": true,
"allowJs": true, "allowJs": true,
"inlineSourceMap": true,
"outDir": "src/main/resources/web/js" "outDir": "src/main/resources/web/js"
}, },
"include": [ "include": [