diff --git a/.env.development b/.env.development index 6c92036270f..7ca492fae4c 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,6 @@ -VITE_BYPASS_LOGIN=1 +VITE_BYPASS_LOGIN=0 VITE_BYPASS_TUTORIAL=0 -VITE_SERVER_URL=http://localhost:8001 +VITE_SERVER_URL=http://192.168.1.101:8001 VITE_DISCORD_CLIENT_ID=1234567890 VITE_GOOGLE_CLIENT_ID=1234567890 VITE_I18N_DEBUG=1 diff --git a/public/images/ui/link_icon.png b/public/images/ui/link_icon.png new file mode 100644 index 00000000000..db5128140dc Binary files /dev/null and b/public/images/ui/link_icon.png differ diff --git a/public/images/ui/unlink_icon.png b/public/images/ui/unlink_icon.png new file mode 100644 index 00000000000..e683b3652cb Binary files /dev/null and b/public/images/ui/unlink_icon.png differ diff --git a/src/loading-scene.ts b/src/loading-scene.ts index b577ba542bd..21342b50c5c 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase { this.loadImage("discord", "ui"); this.loadImage("google", "ui"); this.loadImage("settings_icon", "ui"); + this.loadImage("link_icon", "ui"); + this.loadImage("unlink_icon", "ui"); this.loadImage("default_bg", "arenas"); // Load arena images diff --git a/src/ui/admin-ui-handler.ts b/src/ui/admin-ui-handler.ts index 9a2109d9c33..f93ef434251 100644 --- a/src/ui/admin-ui-handler.ts +++ b/src/ui/admin-ui-handler.ts @@ -4,10 +4,14 @@ import { Mode } from "./ui"; import * as Utils from "../utils"; import { FormModalUiHandler } from "./form-modal-ui-handler"; import { Button } from "#app/enums/buttons"; +import { TextStyle } from "./text"; export default class AdminUiHandler extends FormModalUiHandler { private adminMode: AdminMode; + private adminResult: AdminSearchInfo; // this is the username that we're looking for + private readonly httpUserNotFoundErrorCode: number = 204; // this is the http response from the server when a username isn't found in the server. This has to be the same error the server is giving + private readonly buttonGap = 10; constructor(scene: BattleScene, mode: Mode | null = null) { super(scene, mode); @@ -25,27 +29,29 @@ export default class AdminUiHandler extends FormModalUiHandler { switch (this.adminMode) { case AdminMode.LINK: return ["Username", "Discord ID"]; - case AdminMode.UNLINK: - return ["Username", "Discord ID"]; + case AdminMode.SEARCH: + return ["Username"]; + case AdminMode.ADMIN: + return ["Username", "Discord ID", "Google ID", "Last played"]; default: return [""]; } } getWidth(config?: ModalConfig): number { - return 160; + return this.adminMode === AdminMode.ADMIN ? 180 : 160; } getMargin(config?: ModalConfig): [number, number, number, number] { - return [0, 0, 48, 0]; + return [0, 0, 0, 0]; } getButtonLabels(config?: ModalConfig): string[] { switch (this.adminMode) { case AdminMode.LINK: - return ["Link account", "Cancel"]; - case AdminMode.UNLINK: - return ["Unlink account", "Cancel"]; + return ["Link Account", "Cancel"]; + case AdminMode.SEARCH: + return ["Find account", "Cancel"]; default: return ["Activate ADMIN", "Cancel"]; } @@ -61,10 +67,9 @@ export default class AdminUiHandler extends FormModalUiHandler { } show(args: any[]): boolean { - // this is used to remove the existing fields on the admin panel so they can be updated - this.modalContainer.list = this.modalContainer.list.filter(mC => !mC.name.includes("formLabel")); - - this.adminMode = args[args.length - 1] as AdminMode; + this.adminMode = args[1] as AdminMode; + this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "" }; + const isMessageError = args[3]; const fields = this.getFields(); const hasTitle = !!this.getModalTitle(); @@ -77,61 +82,51 @@ export default class AdminUiHandler extends FormModalUiHandler { } this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); + if (isMessageError) { + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true)); + } else { + this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_GREEN)); + this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_GREEN, true)); + } if (super.show(args)) { + this.populateFields(this.adminMode, this.adminResult); const config = args[0] as ModalConfig; const originalSubmitAction = this.submitAction; this.submitAction = (_) => { this.submitAction = originalSubmitAction; - this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); - const onFail = error => { - this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() }), this.adminMode); + const showMessage = (message, adminResult: AdminSearchInfo, isError: boolean) => { + this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError); this.scene.ui.playError(); }; - if (!this.inputs[0].text) { - if (this.adminMode === AdminMode.LINK) { - return onFail("Username is required"); - } - if (this.adminMode === AdminMode.UNLINK && !this.inputs[1].text) { - return onFail("Either username or discord Id is required"); - } - } - if (!this.inputs[1].text) { - if (this.adminMode === AdminMode.LINK) { - return onFail("Discord Id is required"); - } - if (this.adminMode === AdminMode.UNLINK && !this.inputs[0].text) { - return onFail("Either username or discord is required"); - } + let adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later + const validFields = this.areFieldsValid(this.adminMode); + if (validFields.error) { + this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error + return showMessage(validFields.errorMessage, adminSearchResult, true); } + this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); if (this.adminMode === AdminMode.LINK) { - Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true) + this.adminLinkUnlink(adminSearchResult, "discord", "link"); + /*this.updateAdminPanelInfo(adminSearchResult, AdminMode.LINK);*/ + return showMessage("Username and discord successfully linked", adminSearchResult, false); + } else if (this.adminMode === AdminMode.SEARCH) { + Utils.apiFetch(`admin/account/admin-search?username=${encodeURIComponent(adminSearchResult.username)}`, true) .then(response => { - if (!response.ok) { + if (!response.ok) { // error console.error(response); + } else if (response.status === this.httpUserNotFoundErrorCode) { // username doesn't exist + return showMessage("Username not found in the database", adminSearchResult, true); + } else { // success + response.json().then(jsonResponse => { + adminSearchResult = jsonResponse; + // we double revert here and below to go back 2 layers of menus + //this.scene.ui.revertMode(); + //this.scene.ui.revertMode(); + this.updateAdminPanelInfo(adminSearchResult); + }); } - this.inputs[0].setText(""); - this.inputs[1].setText(""); - // we double revert here and below to go back 2 layers of menus - this.scene.ui.revertMode(); - this.scene.ui.revertMode(); - }) - .catch((err) => { - console.error(err); - this.scene.ui.revertMode(); - this.scene.ui.revertMode(); - }); - } else if (this.adminMode === AdminMode.UNLINK) { - Utils.apiPost("admin/account/discord-unlink", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true) - .then(response => { - if (!response.ok) { - console.error(response); - } - this.inputs[0].setText(""); - this.inputs[1].setText(""); - // we double revert here and below to go back 2 layers of menus - this.scene.ui.revertMode(); - this.scene.ui.revertMode(); }) .catch((err) => { console.error(err); @@ -148,24 +143,162 @@ export default class AdminUiHandler extends FormModalUiHandler { } + populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) { + switch (adminMode) { + case AdminMode.LINK: + this.inputs[0].setText(adminResult.username); + this.inputs[1].setText(adminResult.discordId); + break; + case AdminMode.SEARCH: + this.inputs[0].setText(adminResult.username); + break; + case AdminMode.ADMIN: + const lockedFields: string[] = ["username", "lastLoggedIn"]; + Object.keys(adminResult).forEach((aR, i) => { + this.inputs[i].setText(adminResult[aR]); + if (aR === "discordId" || aR === "googleId") { + const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice"); + const img = this.scene.add.image(this.inputContainers[i].x + nineSlice.width + this.buttonGap, this.inputContainers[i].y + (Math.floor(nineSlice.height / 2)), adminResult[aR] === "" ? "link_icon" : "unlink_icon"); + img.setName(`adminBtn_${aR}`); + img.setOrigin(0.5, 0.5); + img.setScale(0.5); + img.setInteractive(); + img.on("pointerdown", () => { + this.adminLinkUnlink(this.convertInputsToAdmin(), "discord", adminResult[aR] === "" ? "link" : "unlink"); + this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] }); + this.updateAdminPanelInfo(adminResult); + }); + this.addInteractionHoverEffect(img); + this.modalContainer.add(img); + } + if (lockedFields.includes(aR) || adminResult[aR] !== "") { + this.inputs[i].setReadOnly(true); + } else { + this.inputs[i].setReadOnly(false); + } + }); + break; + } + } + + areFieldsValid(adminMode: AdminMode): { error: boolean; errorMessage?: string; } { + switch (adminMode) { + case AdminMode.LINK: + if (!this.inputs[0].text) { + return { + error: true, + errorMessage: "Username is required" + }; + } + if (!this.inputs[1].text) { + return { + error: true, + errorMessage: "Discord Id is required" + }; + } + case AdminMode.SEARCH: + if (!this.inputs[0].text) { + return { + error: true, + errorMessage: "Username or discord Id is required" + }; + } + } + return { + error: false + }; + } + + convertInputsToAdmin(): AdminSearchInfo { + return { + username: this.inputs[0]?.node ? this.inputs[0].text : "", + discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "", + googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "", + lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "" + }; + } + + adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) { + Utils.apiPost(`admin/account/${service}-${mode}`, `username=${encodeURIComponent(adminSearchResult.username)}&discordId=${encodeURIComponent(adminSearchResult.discordId)}`, "application/x-www-form-urlencoded", true) + .then(response => { + if (!response.ok) { + console.error(response); + } + //// we double revert here and below to go back 2 layers of menus + //this.scene.ui.revertMode(); + //this.scene.ui.revertMode(); + }) + .catch((err) => { + console.error(err); + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + }); + } + + updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) { + mode = mode ?? AdminMode.ADMIN; + this.scene.ui.setMode(Mode.ADMIN, { + buttonActions: [ + // we double revert here and below to go back 2 layers of menus + () => { + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + }, + () => { + this.scene.ui.revertMode(); + this.scene.ui.revertMode(); + } + ] + }, mode, adminSearchResult); + } + clear(): void { super.clear(); + + // this is used to remove the existing fields on the admin panel so they can be updated + + const itemsToRemove: string[] = ["formLabel", "adminBtn"]; // this is the start of the names for each element we want to remove + + //this.modalContainer.list = this.modalContainer.list.filter(mC => !itemsToRemove.some(iTR => mC.name.includes(iTR))); + const removeArray: any[] = []; + const mC = this.modalContainer.list; + for (let i = mC.length - 1; i >= 0; i--) { + /* This code looks for a few things before destroying the specific field; first it looks to see if the name of the element is %like% the itemsToRemove labels + * this means that anything with, for example, "formLabel", will be true. + * It then also checks for any containers that are within this.modalContainer, and checks if any of its child elements are of type rexInputText + * and if either of these conditions are met, the element is destroyed + */ + if (itemsToRemove.some(iTR => mC[i].name.includes(iTR)) || (mC[i].type === "Container" && mC[i].list.find(m => m.type === "rexInputText"))) { + removeArray.push(mC[i]); + } + } + + while (removeArray.length > 0) { + this.modalContainer.remove(removeArray.pop(), true); + } } } export enum AdminMode { LINK, - UNLINK + SEARCH, + ADMIN } - export function getAdminModeName(adminMode: AdminMode): string { switch (adminMode) { case AdminMode.LINK: return "Link"; - case AdminMode.UNLINK: - return "Unlink"; + case AdminMode.SEARCH: + return "Search"; default: return ""; } } + +export interface AdminSearchInfo { + username: string; + discordId: string; + googleId: string; + lastLoggedIn: string; +} diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 9059d1cfa54..3c60fa421af 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -389,8 +389,9 @@ export default class MenuUiHandler extends MessageUiHandler { label: "Admin", handler: () => { + const skippedAdminModes: AdminMode[] = [AdminMode.ADMIN]; // this is here so that we can skip the menu populating enums that aren't meant for the menu, such as the AdminMode.ADMIN const options: OptionSelectItem[] = []; - Object.values(AdminMode).filter((v) => !isNaN(Number(v))).forEach((mode) => { // this gets all the enums in a way we can use + Object.values(AdminMode).filter((v) => !isNaN(Number(v)) && !skippedAdminModes.includes(v as AdminMode)).forEach((mode) => { // this gets all the enums in a way we can use options.push({ label: getAdminModeName(mode as AdminMode), handler: () => { diff --git a/src/utils.ts b/src/utils.ts index b029067c8d6..2d9c3fca65b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -284,16 +284,16 @@ export const isBeta = import.meta.env.MODE === "beta"; // this checks to see if export function setCookie(cName: string, cValue: string): void { const expiration = new Date(); expiration.setTime(new Date().getTime() + 3600000 * 24 * 30 * 3/*7*/); - document.cookie = `${cName}=${cValue};Secure;SameSite=Strict;Domain=${window.location.hostname};Path=/;Expires=${expiration.toUTCString()}`; + document.cookie = `${cName}=${cValue};SameSite=Strict;Domain=${window.location.hostname};Path=/;Expires=${expiration.toUTCString()}`; } export function removeCookie(cName: string): void { if (isBeta) { - document.cookie = `${cName}=;Secure;SameSite=Strict;Domain=pokerogue.net;Path=/;Max-Age=-1`; // we need to remove the cookie from the main domain as well + document.cookie = `${cName}=;SameSite=Strict;Domain=pokerogue.net;Path=/;Max-Age=-1`; // we need to remove the cookie from the main domain as well } - document.cookie = `${cName}=;Secure;SameSite=Strict;Domain=${window.location.hostname};Path=/;Max-Age=-1`; - document.cookie = `${cName}=;Secure;SameSite=Strict;Path=/;Max-Age=-1`; // legacy cookie without domain, for older cookies to prevent a login loop + document.cookie = `${cName}=;SameSite=Strict;Domain=${window.location.hostname};Path=/;Max-Age=-1`; + document.cookie = `${cName}=;SameSite=Strict;Path=/;Max-Age=-1`; // legacy cookie without domain, for older cookies to prevent a login loop } export function getCookie(cName: string): string {