feat(webui): handle auth via websocket (#2226)
Now when logging in and out of the game, the webui is notified so it can refresh the nonce, removing the need for constant login requests to revalidate it. Closes #2223 Reviewed-on: #2226 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									93ef9a5348
								
							
						
					
					
						commit
						2fa6dcc7ed
					
				@ -4,16 +4,16 @@ import { config } from "@/src/services/configService";
 | 
				
			|||||||
import { buildConfig } from "@/src/services/buildConfigService";
 | 
					import { buildConfig } from "@/src/services/buildConfigService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Account } from "@/src/models/loginModel";
 | 
					import { Account } from "@/src/models/loginModel";
 | 
				
			||||||
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
 | 
					import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
 | 
				
			||||||
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
 | 
					import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { version_compare } from "@/src/helpers/inventoryHelpers";
 | 
					import { version_compare } from "@/src/helpers/inventoryHelpers";
 | 
				
			||||||
 | 
					import { sendWsBroadcastTo } from "@/src/services/webService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const loginController: RequestHandler = async (request, response) => {
 | 
					export const loginController: RequestHandler = async (request, response) => {
 | 
				
			||||||
    const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
 | 
					    const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const account = await Account.findOne({ email: loginRequest.email });
 | 
					    const account = await Account.findOne({ email: loginRequest.email });
 | 
				
			||||||
    const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const buildLabel: string =
 | 
					    const buildLabel: string =
 | 
				
			||||||
        typeof request.query.buildLabel == "string"
 | 
					        typeof request.query.buildLabel == "string"
 | 
				
			||||||
@ -42,26 +42,14 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
				
			|||||||
            loginRequest.ClientType == "webui-register")
 | 
					            loginRequest.ClientType == "webui-register")
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
 | 
					            const name = await getUsernameFromEmail(loginRequest.email);
 | 
				
			||||||
            let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
 | 
					 | 
				
			||||||
            if (await isNameTaken(name)) {
 | 
					 | 
				
			||||||
                let suffix = 0;
 | 
					 | 
				
			||||||
                do {
 | 
					 | 
				
			||||||
                    ++suffix;
 | 
					 | 
				
			||||||
                    name = nameFromEmail + suffix;
 | 
					 | 
				
			||||||
                } while (await isNameTaken(name));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            const newAccount = await createAccount({
 | 
					            const newAccount = await createAccount({
 | 
				
			||||||
                email: loginRequest.email,
 | 
					                email: loginRequest.email,
 | 
				
			||||||
                password: loginRequest.password,
 | 
					                password: loginRequest.password,
 | 
				
			||||||
                DisplayName: name,
 | 
					                DisplayName: name,
 | 
				
			||||||
                CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
 | 
					                CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
 | 
				
			||||||
                ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
 | 
					                ClientType: loginRequest.ClientType,
 | 
				
			||||||
                CrossPlatformAllowed: true,
 | 
					                Nonce: createNonce(),
 | 
				
			||||||
                ForceLogoutVersion: 0,
 | 
					 | 
				
			||||||
                ConsentNeeded: false,
 | 
					 | 
				
			||||||
                TrackedSettings: [],
 | 
					 | 
				
			||||||
                Nonce: nonce,
 | 
					 | 
				
			||||||
                BuildLabel: buildLabel,
 | 
					                BuildLabel: buildLabel,
 | 
				
			||||||
                LastLogin: new Date()
 | 
					                LastLogin: new Date()
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -80,22 +68,11 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (loginRequest.ClientType == "webui-register") {
 | 
					 | 
				
			||||||
        response.status(400).json({ error: "account already exists" });
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!isCorrectPassword(loginRequest.password, account.password)) {
 | 
					    if (!isCorrectPassword(loginRequest.password, account.password)) {
 | 
				
			||||||
        response.status(400).json({ error: "incorrect login data" });
 | 
					        response.status(400).json({ error: "incorrect login data" });
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (loginRequest.ClientType == "webui") {
 | 
					 | 
				
			||||||
        if (!account.Nonce) {
 | 
					 | 
				
			||||||
            account.ClientType = "webui";
 | 
					 | 
				
			||||||
            account.Nonce = nonce;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
    if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
 | 
					    if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
 | 
				
			||||||
        // U17 seems to handle "nonce still set" like a login failure.
 | 
					        // U17 seems to handle "nonce still set" like a login failure.
 | 
				
			||||||
        if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
 | 
					        if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
 | 
				
			||||||
@ -105,13 +82,15 @@ export const loginController: RequestHandler = async (request, response) => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    account.ClientType = loginRequest.ClientType;
 | 
					    account.ClientType = loginRequest.ClientType;
 | 
				
			||||||
        account.Nonce = nonce;
 | 
					    account.Nonce = createNonce();
 | 
				
			||||||
    account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
 | 
					    account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
 | 
				
			||||||
    account.BuildLabel = buildLabel;
 | 
					    account.BuildLabel = buildLabel;
 | 
				
			||||||
    account.LastLogin = new Date();
 | 
					    account.LastLogin = new Date();
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    await account.save();
 | 
					    await account.save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Tell WebUI its nonce has been invalidated
 | 
				
			||||||
 | 
					    sendWsBroadcastTo(account._id.toString(), { logged_out: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
 | 
					    response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { Account } from "@/src/models/loginModel";
 | 
					import { Account } from "@/src/models/loginModel";
 | 
				
			||||||
 | 
					import { sendWsBroadcastTo } from "@/src/services/webService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const logoutController: RequestHandler = async (req, res) => {
 | 
					export const logoutController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    if (!req.query.accountId) {
 | 
					    if (!req.query.accountId) {
 | 
				
			||||||
@ -10,7 +11,7 @@ export const logoutController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        throw new Error("Request is missing nonce parameter");
 | 
					        throw new Error("Request is missing nonce parameter");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await Account.updateOne(
 | 
					    const stat = await Account.updateOne(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _id: req.query.accountId,
 | 
					            _id: req.query.accountId,
 | 
				
			||||||
            Nonce: nonce
 | 
					            Nonce: nonce
 | 
				
			||||||
@ -19,6 +20,10 @@ export const logoutController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            Nonce: 0
 | 
					            Nonce: 0
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    if (stat.modifiedCount) {
 | 
				
			||||||
 | 
					        // Tell WebUI its nonce has been invalidated
 | 
				
			||||||
 | 
					        sendWsBroadcastTo(req.query.accountId as string, { logged_out: true });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.writeHead(200, {
 | 
					    res.writeHead(200, {
 | 
				
			||||||
        "Content-Type": "text/html",
 | 
					        "Content-Type": "text/html",
 | 
				
			||||||
 | 
				
			|||||||
@ -11,13 +11,13 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
 | 
				
			|||||||
        email: { type: String, required: true, unique: true },
 | 
					        email: { type: String, required: true, unique: true },
 | 
				
			||||||
        password: { type: String, required: true },
 | 
					        password: { type: String, required: true },
 | 
				
			||||||
        DisplayName: { type: String, required: true, unique: true },
 | 
					        DisplayName: { type: String, required: true, unique: true },
 | 
				
			||||||
        CountryCode: { type: String, required: true },
 | 
					        CountryCode: { type: String, default: "" },
 | 
				
			||||||
        ClientType: { type: String },
 | 
					        ClientType: { type: String },
 | 
				
			||||||
        CrossPlatformAllowed: { type: Boolean, required: true },
 | 
					        CrossPlatformAllowed: { type: Boolean, default: true },
 | 
				
			||||||
        ForceLogoutVersion: { type: Number, required: true },
 | 
					        ForceLogoutVersion: { type: Number, default: 0 },
 | 
				
			||||||
        AmazonAuthToken: { type: String },
 | 
					        AmazonAuthToken: { type: String },
 | 
				
			||||||
        AmazonRefreshToken: { type: String },
 | 
					        AmazonRefreshToken: { type: String },
 | 
				
			||||||
        ConsentNeeded: { type: Boolean, required: true },
 | 
					        ConsentNeeded: { type: Boolean, default: false },
 | 
				
			||||||
        TrackedSettings: { type: [String], default: [] },
 | 
					        TrackedSettings: { type: [String], default: [] },
 | 
				
			||||||
        Nonce: { type: Number, default: 0 },
 | 
					        Nonce: { type: Number, default: 0 },
 | 
				
			||||||
        BuildLabel: String,
 | 
					        BuildLabel: String,
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,23 @@ export const isNameTaken = async (name: string): Promise<boolean> => {
 | 
				
			|||||||
    return !!(await Account.findOne({ DisplayName: name }));
 | 
					    return !!(await Account.findOne({ DisplayName: name }));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const createNonce = (): number => {
 | 
				
			||||||
 | 
					    return Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getUsernameFromEmail = async (email: string): Promise<string> => {
 | 
				
			||||||
 | 
					    const nameFromEmail = email.substring(0, email.indexOf("@"));
 | 
				
			||||||
 | 
					    let name = nameFromEmail || email.substring(1) || "SpaceNinja";
 | 
				
			||||||
 | 
					    if (await isNameTaken(name)) {
 | 
				
			||||||
 | 
					        let suffix = 0;
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            ++suffix;
 | 
				
			||||||
 | 
					            name = nameFromEmail + suffix;
 | 
				
			||||||
 | 
					        } while (await isNameTaken(name));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return nameFromEmail;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {
 | 
					export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {
 | 
				
			||||||
    const account = new Account(accountData);
 | 
					    const account = new Account(accountData);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,10 @@ import { logger } from "../utils/logger";
 | 
				
			|||||||
import { app } from "../app";
 | 
					import { app } from "../app";
 | 
				
			||||||
import { AddressInfo } from "node:net";
 | 
					import { AddressInfo } from "node:net";
 | 
				
			||||||
import ws from "ws";
 | 
					import ws from "ws";
 | 
				
			||||||
 | 
					import { Account } from "../models/loginModel";
 | 
				
			||||||
 | 
					import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService";
 | 
				
			||||||
 | 
					import { IDatabaseAccountJson } from "../types/loginTypes";
 | 
				
			||||||
 | 
					import { HydratedDocument } from "mongoose";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let httpServer: http.Server | undefined;
 | 
					let httpServer: http.Server | undefined;
 | 
				
			||||||
let httpsServer: https.Server | undefined;
 | 
					let httpsServer: https.Server | undefined;
 | 
				
			||||||
@ -25,7 +29,7 @@ export const startWebServer = (): void => {
 | 
				
			|||||||
    httpServer = http.createServer(app);
 | 
					    httpServer = http.createServer(app);
 | 
				
			||||||
    httpServer.listen(httpPort, () => {
 | 
					    httpServer.listen(httpPort, () => {
 | 
				
			||||||
        wsServer = new ws.Server({ server: httpServer });
 | 
					        wsServer = new ws.Server({ server: httpServer });
 | 
				
			||||||
        //wsServer.on("connection", wsOnConnect);
 | 
					        wsServer.on("connection", wsOnConnect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.info("HTTP server started on port " + httpPort);
 | 
					        logger.info("HTTP server started on port " + httpPort);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,7 +37,7 @@ export const startWebServer = (): void => {
 | 
				
			|||||||
        httpsServer = https.createServer(tlsOptions, app);
 | 
					        httpsServer = https.createServer(tlsOptions, app);
 | 
				
			||||||
        httpsServer.listen(httpsPort, () => {
 | 
					        httpsServer.listen(httpsPort, () => {
 | 
				
			||||||
            wssServer = new ws.Server({ server: httpsServer });
 | 
					            wssServer = new ws.Server({ server: httpsServer });
 | 
				
			||||||
            //wssServer.on("connection", wsOnConnect);
 | 
					            wssServer.on("connection", wsOnConnect);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logger.info("HTTPS server started on port " + httpsPort);
 | 
					            logger.info("HTTPS server started on port " + httpsPort);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -92,11 +96,91 @@ export const stopWebServer = async (): Promise<void> => {
 | 
				
			|||||||
    await Promise.all(promises);
 | 
					    await Promise.all(promises);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*const wsOnConnect = (ws: ws, _req: http.IncomingMessage): void => {
 | 
					interface IWsCustomData extends ws {
 | 
				
			||||||
    ws.on("message", console.log);
 | 
					    accountId?: string;
 | 
				
			||||||
};*/
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sendWsBroadcast = <T>(data: T): void => {
 | 
					interface IWsMsgFromClient {
 | 
				
			||||||
 | 
					    auth?: {
 | 
				
			||||||
 | 
					        email: string;
 | 
				
			||||||
 | 
					        password: string;
 | 
				
			||||||
 | 
					        isRegister: boolean;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    logout?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IWsMsgToClient {
 | 
				
			||||||
 | 
					    ports?: {
 | 
				
			||||||
 | 
					        http: number | undefined;
 | 
				
			||||||
 | 
					        https: number | undefined;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    config_reloaded?: boolean;
 | 
				
			||||||
 | 
					    auth_succ?: {
 | 
				
			||||||
 | 
					        id: string;
 | 
				
			||||||
 | 
					        DisplayName: string;
 | 
				
			||||||
 | 
					        Nonce: number;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    auth_fail?: {
 | 
				
			||||||
 | 
					        isRegister: boolean;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    logged_out?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const wsOnConnect = (ws: ws, _req: http.IncomingMessage): void => {
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
				
			||||||
 | 
					    ws.on("message", async msg => {
 | 
				
			||||||
 | 
					        const data = JSON.parse(String(msg)) as IWsMsgFromClient;
 | 
				
			||||||
 | 
					        if (data.auth) {
 | 
				
			||||||
 | 
					            let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
 | 
				
			||||||
 | 
					            if (account) {
 | 
				
			||||||
 | 
					                if (isCorrectPassword(data.auth.password, account.password)) {
 | 
				
			||||||
 | 
					                    if (!account.Nonce) {
 | 
				
			||||||
 | 
					                        account.ClientType = "webui";
 | 
				
			||||||
 | 
					                        account.Nonce = createNonce();
 | 
				
			||||||
 | 
					                        await (account as HydratedDocument<IDatabaseAccountJson>).save();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    account = null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else if (data.auth.isRegister) {
 | 
				
			||||||
 | 
					                const name = await getUsernameFromEmail(data.auth.email);
 | 
				
			||||||
 | 
					                account = await createAccount({
 | 
				
			||||||
 | 
					                    email: data.auth.email,
 | 
				
			||||||
 | 
					                    password: data.auth.password,
 | 
				
			||||||
 | 
					                    ClientType: "webui",
 | 
				
			||||||
 | 
					                    LastLogin: new Date(),
 | 
				
			||||||
 | 
					                    DisplayName: name,
 | 
				
			||||||
 | 
					                    Nonce: createNonce()
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (account) {
 | 
				
			||||||
 | 
					                (ws as IWsCustomData).accountId = account.id;
 | 
				
			||||||
 | 
					                ws.send(
 | 
				
			||||||
 | 
					                    JSON.stringify({
 | 
				
			||||||
 | 
					                        auth_succ: {
 | 
				
			||||||
 | 
					                            id: account.id,
 | 
				
			||||||
 | 
					                            DisplayName: account.DisplayName,
 | 
				
			||||||
 | 
					                            Nonce: account.Nonce
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } satisfies IWsMsgToClient)
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                ws.send(
 | 
				
			||||||
 | 
					                    JSON.stringify({
 | 
				
			||||||
 | 
					                        auth_fail: {
 | 
				
			||||||
 | 
					                            isRegister: data.auth.isRegister
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } satisfies IWsMsgToClient)
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (data.logout) {
 | 
				
			||||||
 | 
					            (ws as IWsCustomData).accountId = undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sendWsBroadcast = (data: IWsMsgToClient): void => {
 | 
				
			||||||
    const msg = JSON.stringify(data);
 | 
					    const msg = JSON.stringify(data);
 | 
				
			||||||
    if (wsServer) {
 | 
					    if (wsServer) {
 | 
				
			||||||
        for (const client of wsServer.clients) {
 | 
					        for (const client of wsServer.clients) {
 | 
				
			||||||
@ -109,3 +193,21 @@ export const sendWsBroadcast = <T>(data: T): void => {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void => {
 | 
				
			||||||
 | 
					    const msg = JSON.stringify(data);
 | 
				
			||||||
 | 
					    if (wsServer) {
 | 
				
			||||||
 | 
					        for (const client of wsServer.clients) {
 | 
				
			||||||
 | 
					            if ((client as IWsCustomData).accountId == accountId) {
 | 
				
			||||||
 | 
					                client.send(msg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (wssServer) {
 | 
				
			||||||
 | 
					        for (const client of wssServer.clients) {
 | 
				
			||||||
 | 
					            if ((client as IWsCustomData).accountId == accountId) {
 | 
				
			||||||
 | 
					                client.send(msg);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { Types } from "mongoose";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface IAccountAndLoginResponseCommons {
 | 
					export interface IAccountAndLoginResponseCommons {
 | 
				
			||||||
    DisplayName: string;
 | 
					    DisplayName: string;
 | 
				
			||||||
    CountryCode: string;
 | 
					    CountryCode?: string;
 | 
				
			||||||
    ClientType?: string;
 | 
					    ClientType?: string;
 | 
				
			||||||
    CrossPlatformAllowed?: boolean;
 | 
					    CrossPlatformAllowed?: boolean;
 | 
				
			||||||
    ForceLogoutVersion?: number;
 | 
					    ForceLogoutVersion?: number;
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,7 @@
 | 
				
			|||||||
                <li class="nav-item dropdown user-dropdown">
 | 
					                <li class="nav-item dropdown user-dropdown">
 | 
				
			||||||
                    <button class="nav-link dropdown-toggle displayname" data-bs-toggle="dropdown" aria-expanded="false"></button>
 | 
					                    <button class="nav-link dropdown-toggle displayname" data-bs-toggle="dropdown" aria-expanded="false"></button>
 | 
				
			||||||
                    <ul class="dropdown-menu dropdown-menu-end">
 | 
					                    <ul class="dropdown-menu dropdown-menu-end">
 | 
				
			||||||
                        <li><a class="dropdown-item" href="/webui/" onclick="logout();" data-loc="navbar_logout"></a></li>
 | 
					                        <li><a class="dropdown-item" href="/webui/" onclick="doLogout();" data-loc="navbar_logout"></a></li>
 | 
				
			||||||
                        <li><hr class="dropdown-divider"></li>
 | 
					                        <li><hr class="dropdown-divider"></li>
 | 
				
			||||||
                        <li><a class="dropdown-item" href="#" onclick="event.preventDefault();renameAccount();" data-loc="navbar_renameAccount"></a></li>
 | 
					                        <li><a class="dropdown-item" href="#" onclick="event.preventDefault();renameAccount();" data-loc="navbar_renameAccount"></a></li>
 | 
				
			||||||
                        <li><a class="dropdown-item" href="#" onclick="event.preventDefault();deleteAccount();" data-loc="navbar_deleteAccount"></a></li>
 | 
					                        <li><a class="dropdown-item" href="#" onclick="event.preventDefault();deleteAccount();" data-loc="navbar_deleteAccount"></a></li>
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,28 @@
 | 
				
			|||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
 | 
					/* eslint-disable @typescript-eslint/no-unsafe-member-access */
 | 
				
			||||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
					/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let auth_pending = false,
 | 
				
			||||||
 | 
					    did_initial_auth = false;
 | 
				
			||||||
 | 
					const sendAuth = isRegister => {
 | 
				
			||||||
 | 
					    if (localStorage.getItem("email") && localStorage.getItem("password")) {
 | 
				
			||||||
 | 
					        auth_pending = true;
 | 
				
			||||||
 | 
					        window.ws.send(
 | 
				
			||||||
 | 
					            JSON.stringify({
 | 
				
			||||||
 | 
					                auth: {
 | 
				
			||||||
 | 
					                    email: localStorage.getItem("email"),
 | 
				
			||||||
 | 
					                    password: wp.encSync(localStorage.getItem("password")),
 | 
				
			||||||
 | 
					                    isRegister
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function openWebSocket() {
 | 
					function openWebSocket() {
 | 
				
			||||||
    window.ws = new WebSocket("/custom/ws");
 | 
					    window.ws = new WebSocket("/custom/ws");
 | 
				
			||||||
 | 
					    window.ws.onopen = () => {
 | 
				
			||||||
 | 
					        sendAuth(false);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    window.ws.onmessage = e => {
 | 
					    window.ws.onmessage = e => {
 | 
				
			||||||
        const msg = JSON.parse(e.data);
 | 
					        const msg = JSON.parse(e.data);
 | 
				
			||||||
        if ("ports" in msg) {
 | 
					        if ("ports" in msg) {
 | 
				
			||||||
@ -21,31 +41,9 @@ function openWebSocket() {
 | 
				
			|||||||
                single.loadRoute("/webui/cheats");
 | 
					                single.loadRoute("/webui/cheats");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					        if ("auth_succ" in msg) {
 | 
				
			||||||
    window.ws.onclose = function () {
 | 
					            auth_pending = false;
 | 
				
			||||||
        setTimeout(openWebSocket, 3000);
 | 
					            const data = msg.auth_succ;
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
openWebSocket();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let loginOrRegisterPending = false;
 | 
					 | 
				
			||||||
window.registerSubmit = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function doLogin() {
 | 
					 | 
				
			||||||
    if (loginOrRegisterPending) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    loginOrRegisterPending = true;
 | 
					 | 
				
			||||||
    localStorage.setItem("email", $("#email").val());
 | 
					 | 
				
			||||||
    localStorage.setItem("password", $("#password").val());
 | 
					 | 
				
			||||||
    loginFromLocalStorage();
 | 
					 | 
				
			||||||
    registerSubmit = false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loginFromLocalStorage() {
 | 
					 | 
				
			||||||
    const isRegister = registerSubmit;
 | 
					 | 
				
			||||||
    doLoginRequest(
 | 
					 | 
				
			||||||
        data => {
 | 
					 | 
				
			||||||
            if (single.getCurrentPath() == "/webui/") {
 | 
					            if (single.getCurrentPath() == "/webui/") {
 | 
				
			||||||
                single.loadRoute("/webui/inventory");
 | 
					                single.loadRoute("/webui/inventory");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -55,55 +53,74 @@ function loginFromLocalStorage() {
 | 
				
			|||||||
            if (window.dict) {
 | 
					            if (window.dict) {
 | 
				
			||||||
                updateLocElements();
 | 
					                updateLocElements();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if (!did_initial_auth) {
 | 
				
			||||||
 | 
					                did_initial_auth = true;
 | 
				
			||||||
                updateInventory();
 | 
					                updateInventory();
 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        () => {
 | 
					 | 
				
			||||||
            logout();
 | 
					 | 
				
			||||||
            alert(loc(isRegister ? "code_regFail" : "code_loginFail"));
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    );
 | 
					        }
 | 
				
			||||||
 | 
					        if ("auth_fail" in msg) {
 | 
				
			||||||
 | 
					            auth_pending = false;
 | 
				
			||||||
 | 
					            logout();
 | 
				
			||||||
 | 
					            if (single.getCurrentPath() == "/webui/") {
 | 
				
			||||||
 | 
					                alert(loc(msg.auth_fail.isRegister ? "code_regFail" : "code_loginFail"));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                single.loadRoute("/webui/");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if ("logged_out" in msg) {
 | 
				
			||||||
 | 
					            sendAuth();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    window.ws.onclose = function () {
 | 
				
			||||||
 | 
					        window.ws = undefined;
 | 
				
			||||||
 | 
					        setTimeout(openWebSocket, 3000);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					openWebSocket();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getWebSocket() {
 | 
				
			||||||
 | 
					    return new Promise(resolve => {
 | 
				
			||||||
 | 
					        let interval;
 | 
				
			||||||
 | 
					        interval = setInterval(() => {
 | 
				
			||||||
 | 
					            if (window.ws) {
 | 
				
			||||||
 | 
					                clearInterval(interval);
 | 
				
			||||||
 | 
					                resolve(window.ws);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, 10);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function doLoginRequest(succ_cb, fail_cb) {
 | 
					window.registerSubmit = false;
 | 
				
			||||||
    const req = $.post({
 | 
					
 | 
				
			||||||
        url: "/api/login.php",
 | 
					function doLogin() {
 | 
				
			||||||
        contentType: "text/plain",
 | 
					    if (auth_pending) {
 | 
				
			||||||
        data: JSON.stringify({
 | 
					        return;
 | 
				
			||||||
            email: localStorage.getItem("email").toLowerCase(),
 | 
					    }
 | 
				
			||||||
            password: wp.encSync(localStorage.getItem("password"), "hex"),
 | 
					    localStorage.setItem("email", $("#email").val());
 | 
				
			||||||
            time: parseInt(new Date() / 1000),
 | 
					    localStorage.setItem("password", $("#password").val());
 | 
				
			||||||
            s: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==", // signature of some kind
 | 
					    sendAuth(registerSubmit);
 | 
				
			||||||
            lang: "en",
 | 
					    window.registerSubmit = false;
 | 
				
			||||||
            // eslint-disable-next-line no-loss-of-precision
 | 
					 | 
				
			||||||
            date: 1501230947855458660, // ???
 | 
					 | 
				
			||||||
            ClientType: registerSubmit ? "webui-register" : "webui",
 | 
					 | 
				
			||||||
            PS: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==" // anti-cheat data
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    req.done(succ_cb);
 | 
					 | 
				
			||||||
    req.fail(fail_cb);
 | 
					 | 
				
			||||||
    req.always(() => {
 | 
					 | 
				
			||||||
        loginOrRegisterPending = false;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function revalidateAuthz(succ_cb) {
 | 
					function revalidateAuthz(succ_cb) {
 | 
				
			||||||
    return doLoginRequest(
 | 
					    getWebSocket().then(() => {
 | 
				
			||||||
        data => {
 | 
					        // We have a websocket connection, so authz should be good.
 | 
				
			||||||
            window.authz = "accountId=" + data.id + "&nonce=" + data.Nonce;
 | 
					 | 
				
			||||||
        succ_cb();
 | 
					        succ_cb();
 | 
				
			||||||
        },
 | 
					    });
 | 
				
			||||||
        () => {
 | 
					 | 
				
			||||||
            logout();
 | 
					 | 
				
			||||||
            alert(loc("code_nonValidAuthz"));
 | 
					 | 
				
			||||||
            single.loadRoute("/webui/"); // Show login screen
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function logout() {
 | 
					function logout() {
 | 
				
			||||||
    localStorage.removeItem("email");
 | 
					    localStorage.removeItem("email");
 | 
				
			||||||
    localStorage.removeItem("password");
 | 
					    localStorage.removeItem("password");
 | 
				
			||||||
 | 
					    did_initial_auth = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function doLogout() {
 | 
				
			||||||
 | 
					    logout();
 | 
				
			||||||
 | 
					    if (window.ws) {
 | 
				
			||||||
 | 
					        // Unsubscribe from notifications about nonce invalidation
 | 
				
			||||||
 | 
					        window.ws.send(JSON.stringify({ logout: true }));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function renameAccount() {
 | 
					function renameAccount() {
 | 
				
			||||||
@ -129,10 +146,6 @@ function deleteAccount() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (localStorage.getItem("email") && localStorage.getItem("password")) {
 | 
					 | 
				
			||||||
    loginFromLocalStorage();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
single.on("route_load", function (event) {
 | 
					single.on("route_load", function (event) {
 | 
				
			||||||
    if (event.route.paths[0] != "/webui/") {
 | 
					    if (event.route.paths[0] != "/webui/") {
 | 
				
			||||||
        // Authorised route?
 | 
					        // Authorised route?
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user