mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-22 15:29:27 +02:00
Updates to admin panel logic
This commit is contained in:
parent
c36caeac1b
commit
f50e27a48c
@ -1,6 +1,6 @@
|
|||||||
VITE_BYPASS_LOGIN=1
|
VITE_BYPASS_LOGIN=0
|
||||||
VITE_BYPASS_TUTORIAL=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_DISCORD_CLIENT_ID=1234567890
|
||||||
VITE_GOOGLE_CLIENT_ID=1234567890
|
VITE_GOOGLE_CLIENT_ID=1234567890
|
||||||
VITE_I18N_DEBUG=1
|
VITE_I18N_DEBUG=1
|
||||||
|
BIN
public/images/ui/link_icon.png
Normal file
BIN
public/images/ui/link_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 460 B |
BIN
public/images/ui/unlink_icon.png
Normal file
BIN
public/images/ui/unlink_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 556 B |
@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase {
|
|||||||
this.loadImage("discord", "ui");
|
this.loadImage("discord", "ui");
|
||||||
this.loadImage("google", "ui");
|
this.loadImage("google", "ui");
|
||||||
this.loadImage("settings_icon", "ui");
|
this.loadImage("settings_icon", "ui");
|
||||||
|
this.loadImage("link_icon", "ui");
|
||||||
|
this.loadImage("unlink_icon", "ui");
|
||||||
|
|
||||||
this.loadImage("default_bg", "arenas");
|
this.loadImage("default_bg", "arenas");
|
||||||
// Load arena images
|
// Load arena images
|
||||||
|
@ -4,10 +4,14 @@ import { Mode } from "./ui";
|
|||||||
import * as Utils from "../utils";
|
import * as Utils from "../utils";
|
||||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||||
import { Button } from "#app/enums/buttons";
|
import { Button } from "#app/enums/buttons";
|
||||||
|
import { TextStyle } from "./text";
|
||||||
|
|
||||||
export default class AdminUiHandler extends FormModalUiHandler {
|
export default class AdminUiHandler extends FormModalUiHandler {
|
||||||
|
|
||||||
private adminMode: AdminMode;
|
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) {
|
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||||
super(scene, mode);
|
super(scene, mode);
|
||||||
@ -25,27 +29,29 @@ export default class AdminUiHandler extends FormModalUiHandler {
|
|||||||
switch (this.adminMode) {
|
switch (this.adminMode) {
|
||||||
case AdminMode.LINK:
|
case AdminMode.LINK:
|
||||||
return ["Username", "Discord ID"];
|
return ["Username", "Discord ID"];
|
||||||
case AdminMode.UNLINK:
|
case AdminMode.SEARCH:
|
||||||
return ["Username", "Discord ID"];
|
return ["Username"];
|
||||||
|
case AdminMode.ADMIN:
|
||||||
|
return ["Username", "Discord ID", "Google ID", "Last played"];
|
||||||
default:
|
default:
|
||||||
return [""];
|
return [""];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getWidth(config?: ModalConfig): number {
|
getWidth(config?: ModalConfig): number {
|
||||||
return 160;
|
return this.adminMode === AdminMode.ADMIN ? 180 : 160;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMargin(config?: ModalConfig): [number, number, number, number] {
|
getMargin(config?: ModalConfig): [number, number, number, number] {
|
||||||
return [0, 0, 48, 0];
|
return [0, 0, 0, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtonLabels(config?: ModalConfig): string[] {
|
getButtonLabels(config?: ModalConfig): string[] {
|
||||||
switch (this.adminMode) {
|
switch (this.adminMode) {
|
||||||
case AdminMode.LINK:
|
case AdminMode.LINK:
|
||||||
return ["Link account", "Cancel"];
|
return ["Link Account", "Cancel"];
|
||||||
case AdminMode.UNLINK:
|
case AdminMode.SEARCH:
|
||||||
return ["Unlink account", "Cancel"];
|
return ["Find account", "Cancel"];
|
||||||
default:
|
default:
|
||||||
return ["Activate ADMIN", "Cancel"];
|
return ["Activate ADMIN", "Cancel"];
|
||||||
}
|
}
|
||||||
@ -61,10 +67,9 @@ export default class AdminUiHandler extends FormModalUiHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show(args: any[]): boolean {
|
show(args: any[]): boolean {
|
||||||
// this is used to remove the existing fields on the admin panel so they can be updated
|
this.adminMode = args[1] as AdminMode;
|
||||||
this.modalContainer.list = this.modalContainer.list.filter(mC => !mC.name.includes("formLabel"));
|
this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "" };
|
||||||
|
const isMessageError = args[3];
|
||||||
this.adminMode = args[args.length - 1] as AdminMode;
|
|
||||||
|
|
||||||
const fields = this.getFields();
|
const fields = this.getFields();
|
||||||
const hasTitle = !!this.getModalTitle();
|
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());
|
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)) {
|
if (super.show(args)) {
|
||||||
|
this.populateFields(this.adminMode, this.adminResult);
|
||||||
const config = args[0] as ModalConfig;
|
const config = args[0] as ModalConfig;
|
||||||
const originalSubmitAction = this.submitAction;
|
const originalSubmitAction = this.submitAction;
|
||||||
this.submitAction = (_) => {
|
this.submitAction = (_) => {
|
||||||
this.submitAction = originalSubmitAction;
|
this.submitAction = originalSubmitAction;
|
||||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] });
|
const showMessage = (message, adminResult: AdminSearchInfo, isError: boolean) => {
|
||||||
const onFail = error => {
|
this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError);
|
||||||
this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() }), this.adminMode);
|
|
||||||
this.scene.ui.playError();
|
this.scene.ui.playError();
|
||||||
};
|
};
|
||||||
if (!this.inputs[0].text) {
|
let adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later
|
||||||
if (this.adminMode === AdminMode.LINK) {
|
const validFields = this.areFieldsValid(this.adminMode);
|
||||||
return onFail("Username is required");
|
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
|
||||||
if (this.adminMode === AdminMode.UNLINK && !this.inputs[1].text) {
|
return showMessage(validFields.errorMessage, adminSearchResult, true);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] });
|
||||||
if (this.adminMode === AdminMode.LINK) {
|
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 => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) { // error
|
||||||
console.error(response);
|
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) => {
|
.catch((err) => {
|
||||||
console.error(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 {
|
clear(): void {
|
||||||
super.clear();
|
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 {
|
export enum AdminMode {
|
||||||
LINK,
|
LINK,
|
||||||
UNLINK
|
SEARCH,
|
||||||
|
ADMIN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function getAdminModeName(adminMode: AdminMode): string {
|
export function getAdminModeName(adminMode: AdminMode): string {
|
||||||
switch (adminMode) {
|
switch (adminMode) {
|
||||||
case AdminMode.LINK:
|
case AdminMode.LINK:
|
||||||
return "Link";
|
return "Link";
|
||||||
case AdminMode.UNLINK:
|
case AdminMode.SEARCH:
|
||||||
return "Unlink";
|
return "Search";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminSearchInfo {
|
||||||
|
username: string;
|
||||||
|
discordId: string;
|
||||||
|
googleId: string;
|
||||||
|
lastLoggedIn: string;
|
||||||
|
}
|
||||||
|
@ -389,8 +389,9 @@ export default class MenuUiHandler extends MessageUiHandler {
|
|||||||
label: "Admin",
|
label: "Admin",
|
||||||
handler: () => {
|
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[] = [];
|
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({
|
options.push({
|
||||||
label: getAdminModeName(mode as AdminMode),
|
label: getAdminModeName(mode as AdminMode),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
|
@ -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 {
|
export function setCookie(cName: string, cValue: string): void {
|
||||||
const expiration = new Date();
|
const expiration = new Date();
|
||||||
expiration.setTime(new Date().getTime() + 3600000 * 24 * 30 * 3/*7*/);
|
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 {
|
export function removeCookie(cName: string): void {
|
||||||
if (isBeta) {
|
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}=;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;Path=/;Max-Age=-1`; // legacy cookie without domain, for older cookies to prevent a login loop
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCookie(cName: string): string {
|
export function getCookie(cName: string): string {
|
||||||
|
Loading…
Reference in New Issue
Block a user