import { io } from "socket.io-client"; import { Socket } from "socket.io-client"; import { defineComponent } from "vue"; import jwtDecode from "jwt-decode"; import { Terminal } from "@xterm/xterm"; let socket : Socket; let terminalMap : Map<string, Terminal> = new Map(); export default defineComponent({ data() { return { socketIO: { token: null, firstConnect: true, connected: false, connectCount: 0, initedSocketIO: false, connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`, showReverseProxyGuide: true, connecting: false, }, info: { }, remember: (localStorage.remember !== "0"), loggedIn: false, allowLoginDialog: false, username: null, stackList: {}, composeTemplate: "", }; }, computed: { usernameFirstChar() { if (typeof this.username == "string" && this.username.length >= 1) { return this.username.charAt(0).toUpperCase(); } else { return "🐻"; } }, /** * Frontend Version * It should be compiled to a static value while building the frontend. * Please see ./frontend/vite.config.ts, it is defined via vite.js * @returns {string} */ frontendVersion() { // eslint-disable-next-line no-undef return FRONTEND_VERSION; }, /** * Are both frontend and backend in the same version? * @returns {boolean} */ isFrontendBackendVersionMatched() { if (!this.info.version) { return true; } return this.info.version === this.frontendVersion; }, }, watch: { remember() { localStorage.remember = (this.remember) ? "1" : "0"; }, // Reload the SPA if the server version is changed. "info.version"(to, from) { if (from && from !== to) { window.location.reload(); } }, }, created() { this.initSocketIO(); }, mounted() { return; }, methods: { /** * Initialize connection to socket server * @param bypass Should the check for if we * are on a status page be bypassed? */ initSocketIO(bypass = false) { // No need to re-init if (this.socketIO.initedSocketIO) { return; } this.socketIO.initedSocketIO = true; let url : string; const env = process.env.NODE_ENV || "production"; if (env === "development" || localStorage.dev === "dev") { url = location.protocol + "//" + location.hostname + ":5001"; } else { url = location.protocol + "//" + location.host; } let connectingMsgTimeout = setTimeout(() => { this.socketIO.connecting = true; }, 1500); socket = io(url, { transports: [ "websocket", "polling" ] }); socket.on("connect", () => { console.log("Connected to the socket server"); clearTimeout(connectingMsgTimeout); this.socketIO.connecting = false; this.socketIO.connectCount++; this.socketIO.connected = true; this.socketIO.showReverseProxyGuide = false; const token = this.storage().token; if (token) { if (token !== "autoLogin") { console.log("Logging in by token"); this.loginByToken(token); } else { // Timeout if it is not actually auto login setTimeout(() => { if (! this.loggedIn) { this.allowLoginDialog = true; this.storage().removeItem("token"); } }, 5000); } } else { this.allowLoginDialog = true; } this.socketIO.firstConnect = false; }); socket.on("disconnect", () => { console.log("disconnect"); this.socketIO.connectionErrorMsg = "Lost connection to the socket server. Reconnecting..."; this.socketIO.connected = false; }); socket.on("connect_error", (err) => { console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("reconnecting...")}`; this.socketIO.showReverseProxyGuide = true; this.socketIO.connected = false; this.socketIO.firstConnect = false; this.socketIO.connecting = false; }); // Custom Events socket.on("info", (info) => { this.info = info; }); socket.on("autoLogin", () => { this.loggedIn = true; this.storage().token = "autoLogin"; this.socketIO.token = "autoLogin"; this.allowLoginDialog = false; this.afterLogin(); }); socket.on("setup", () => { console.log("setup"); this.$router.push("/setup"); }); socket.on("terminalWrite", (terminalName, data) => { const terminal = terminalMap.get(terminalName); if (!terminal) { //console.error("Terminal not found: " + terminalName); return; } terminal.write(data); }); socket.on("stackList", (res) => { if (res.ok) { this.stackList = res.stackList; } }); socket.on("stackStatusList", (res) => { if (res.ok) { for (let stackName in res.stackStatusList) { const stackObj = this.stackList[stackName]; if (stackObj) { stackObj.status = res.stackStatusList[stackName]; } } } }); socket.on("refresh", () => { location.reload(); }); }, /** * The storage currently in use * @returns Current storage */ storage() : Storage { return (this.remember) ? localStorage : sessionStorage; }, getSocket() : Socket { return socket; }, /** * Get payload of JWT cookie * @returns {(object | undefined)} JWT payload */ getJWTPayload() { const jwtToken = this.storage().token; if (jwtToken && jwtToken !== "autoLogin") { return jwtDecode(jwtToken); } return undefined; }, /** * Send request to log user in * @param {string} username Username to log in with * @param {string} password Password to log in with * @param {string} token User token * @param {loginCB} callback Callback to call with result * @returns {void} */ login(username : string, password : string, token : string, callback) { this.getSocket().emit("login", { username, password, token, }, (res) => { if (res.tokenRequired) { callback(res); } if (res.ok) { this.storage().token = res.token; this.socketIO.token = res.token; this.loggedIn = true; this.username = this.getJWTPayload()?.username; this.afterLogin(); // Trigger Chrome Save Password history.pushState({}, ""); } callback(res); }); }, /** * Log in using a token * @param {string} token Token to log in with * @returns {void} */ loginByToken(token : string) { socket.emit("loginByToken", token, (res) => { this.allowLoginDialog = true; if (! res.ok) { this.logout(); } else { this.loggedIn = true; this.username = this.getJWTPayload()?.username; this.afterLogin(); } }); }, /** * Log out of the web application * @returns {void} */ logout() { socket.emit("logout", () => { }); this.storage().removeItem("token"); this.socketIO.token = null; this.loggedIn = false; this.username = null; this.clearData(); }, /** * @returns {void} */ clearData() { }, afterLogin() { }, bindTerminal(terminalName : string, terminal : Terminal) { // Load terminal, get terminal screen socket.emit("terminalJoin", terminalName, (res) => { if (res.ok) { terminal.write(res.buffer); terminalMap.set(terminalName, terminal); } else { this.toastRes(res); } }); }, unbindTerminal(terminalName : string) { terminalMap.delete(terminalName); }, } });