mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-14 07:24:52 +09:00
Add bulk user actions (Import/Export/Restore) (#741)
* Add "Bulk Actions" modal to users page * Add control logic without validation * Add restore and better permission check * Add controller logic and automatic backup * Add CTL commands for bulk actions * Add tests for bulk actions * Improve description on permissions
This commit is contained in:
1 parent
1d5040dc0c
commit
0055f28f45
8 files changed
+1044
-5
No files matched your search
@@ -116,6 +116,8 @@ export default class ControlConnection extends BaseConnection {
|
||||
this.handle(lib.UserSetBannedRequest, this.handleUserSetBannedRequest.bind(this));
|
||||
this.handle(lib.UserSetWhitelistedRequest, this.handleUserSetWhitelistedRequest.bind(this));
|
||||
this.handle(lib.UserDeleteRequest, this.handleUserDeleteRequest.bind(this));
|
||||
this.handle(lib.UserBulkImportRequest, this.handleUserBulkImportRequest.bind(this));
|
||||
this.handle(lib.UserBulkExportRequest, this.handleUserBulkExportRequest.bind(this));
|
||||
this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this));
|
||||
}
|
||||
|
||||
@@ -752,6 +754,209 @@ export default class ControlConnection extends BaseConnection {
|
||||
}
|
||||
}
|
||||
|
||||
async handleUserBulkRestoreRequest(request: lib.UserBulkImportRequest, updated: Map<string, ControllerUser>) {
|
||||
if (request.importType === "users") {
|
||||
// Update all fields for all users
|
||||
const users = request.users as Extract<typeof request.users, { username: string }[]>;
|
||||
const admin = new Set(users.filter(u => u.is_admin).map(u => u.username.toLowerCase()));
|
||||
const banned = new Set(users.filter(u => u.is_banned).map(u => u.username.toLowerCase()));
|
||||
const whitelist = new Set(users.filter(u => u.is_whitelisted).map(u => u.username.toLowerCase()));
|
||||
for (const user of this._controller.userManager.users.values()) {
|
||||
if (user.isAdmin && !admin.has(user.name)) {
|
||||
user.isAdmin = false;
|
||||
updated.set(user.id, user);
|
||||
}
|
||||
if (user.isBanned && !banned.has(user.name)) {
|
||||
user.isBanned = false;
|
||||
user.banReason = "";
|
||||
updated.set(user.id, user);
|
||||
}
|
||||
if (user.isWhitelisted && !whitelist.has(user.name)) {
|
||||
user.isWhitelisted = false;
|
||||
updated.set(user.id, user);
|
||||
}
|
||||
}
|
||||
} else if (request.importType === "bans") {
|
||||
// Bans have extra logic to handle ban reason
|
||||
const users = request.users as (string | { username: string, reason: string })[];
|
||||
const expected = new Set(
|
||||
users.map(u => (typeof u === "string" ? u.toLowerCase() : u.username.toLowerCase()))
|
||||
);
|
||||
for (const user of this._controller.userManager.users.values()) {
|
||||
if (user.isBanned && !expected.has(user.name)) {
|
||||
user.isBanned = false;
|
||||
user.banReason = "";
|
||||
updated.set(user.id, user);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Whitelist and admin have the same logic
|
||||
const users = request.users as string[];
|
||||
const expected = new Set(users.map(u => u.toLowerCase()));
|
||||
const prop = request.importType === "admins" ? "isAdmin" : "isWhitelisted";
|
||||
for (const user of this._controller.userManager.users.values()) {
|
||||
if (user[prop] && !expected.has(user.name)) {
|
||||
user[prop] = false;
|
||||
updated.set(user.id, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A large number of small if statements are unavoidable when reducing the number of updates sent
|
||||
// eslint-disable-next-line complexity
|
||||
async handleUserBulkImportRequest(request: lib.UserBulkImportRequest) {
|
||||
let backup: undefined | Awaited<ReturnType<ControlConnection["handleUserBulkExportRequest"]>>;
|
||||
const updated = new Map<ControllerUser["id"], ControllerUser>();
|
||||
if (request.restore) {
|
||||
// Unban / Demote / Unwhitelist players not on the list
|
||||
backup = await this.handleUserBulkExportRequest(new lib.UserBulkExportRequest(request.importType));
|
||||
await this.handleUserBulkRestoreRequest(request, updated);
|
||||
}
|
||||
|
||||
// Will get a user or attempt to create one
|
||||
const getUserOrCreate = (username: string) => {
|
||||
const user = this._controller.userManager.getByName(username);
|
||||
if (user) { return user; }
|
||||
this.user.checkPermission("core.user.create");
|
||||
return this._controller.userManager.createUser(username);
|
||||
};
|
||||
|
||||
// Merge the imported data with existing data, this is strictly additive
|
||||
if (request.importType === "users") {
|
||||
// Update all fields
|
||||
const users = request.users as Extract<typeof request.users, { username: string }[]>;
|
||||
for (const user of users) {
|
||||
const cUser = getUserOrCreate(user.username);
|
||||
if (user.is_admin && !cUser.isAdmin) {
|
||||
cUser.isAdmin = true;
|
||||
updated.set(cUser.id, cUser);
|
||||
}
|
||||
if (user.is_banned && !cUser.isBanned) {
|
||||
cUser.isBanned = true;
|
||||
cUser.banReason = user.ban_reason ?? "";
|
||||
updated.set(cUser.id, cUser);
|
||||
}
|
||||
if (user.is_whitelisted && !cUser.isWhitelisted) {
|
||||
cUser.isWhitelisted = true;
|
||||
updated.set(cUser.id, cUser);
|
||||
}
|
||||
}
|
||||
} else if (request.importType === "bans") {
|
||||
// Bans have extra logic to handle ban reason
|
||||
const users = request.users as (string | { username: string, reason: string })[];
|
||||
for (const user of users) {
|
||||
if (typeof user === "string") {
|
||||
const cUser = getUserOrCreate(user);
|
||||
if (!cUser.isBanned) {
|
||||
cUser.isBanned = true;
|
||||
cUser.banReason = "";
|
||||
updated.set(cUser.id, cUser);
|
||||
}
|
||||
} else {
|
||||
const cUser = getUserOrCreate(user.username);
|
||||
if (!cUser.isBanned) {
|
||||
cUser.isBanned = true;
|
||||
cUser.banReason = user.reason;
|
||||
updated.set(cUser.id, cUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Whitelist and admin have the same logic
|
||||
const users = request.users as string[];
|
||||
const prop = request.importType === "admins" ? "isAdmin" : "isWhitelisted";
|
||||
for (const user of users) {
|
||||
const cUser = getUserOrCreate(user);
|
||||
if (!cUser[prop]) {
|
||||
cUser[prop] = true;
|
||||
updated.set(cUser.id, cUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send the necessary update events
|
||||
const updatedUsers = [...updated.values()];
|
||||
this._controller.usersUpdated(updatedUsers);
|
||||
|
||||
// Resync the host user lists
|
||||
const adminlist: Set<string> = new Set();
|
||||
const banlist: Map<string, string> = new Map();
|
||||
const whitelist: Set<string> = new Set();
|
||||
|
||||
for (let user of this._controller.userManager.users.values()) {
|
||||
if (user.isAdmin) {
|
||||
adminlist.add(user.id);
|
||||
}
|
||||
if (user.isBanned) {
|
||||
banlist.set(user.id, user.banReason);
|
||||
}
|
||||
if (user.isWhitelisted) {
|
||||
whitelist.add(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
this._controller.sendEvent(
|
||||
new lib.SyncUserListsEvent(adminlist, banlist, whitelist),
|
||||
lib.Address.fromShorthand("allHosts")
|
||||
);
|
||||
|
||||
return backup ?? [];
|
||||
}
|
||||
|
||||
async handleUserBulkExportRequest(request: lib.UserBulkExportRequest) {
|
||||
if (request.exportType === "users") {
|
||||
// Send a full user export
|
||||
const usersToSend = [] as lib.ClusterioUserExport["users"];
|
||||
for (const user of this._controller.userManager.users.values()) {
|
||||
let send = false;
|
||||
const userToSend = { username: user.name } as typeof usersToSend[0];
|
||||
if (user.isBanned) {
|
||||
send = true;
|
||||
userToSend.is_banned = true;
|
||||
if (user.banReason !== "") {
|
||||
userToSend.ban_reason = user.banReason;
|
||||
}
|
||||
}
|
||||
if (user.isAdmin) {
|
||||
send = true;
|
||||
userToSend.is_admin = true;
|
||||
}
|
||||
if (user.isWhitelisted) {
|
||||
send = true;
|
||||
userToSend.is_whitelisted = true;
|
||||
}
|
||||
if (send) {
|
||||
usersToSend.push(userToSend);
|
||||
}
|
||||
}
|
||||
return new lib.ClusterioUserExport(usersToSend);
|
||||
} else if (request.exportType === "bans") {
|
||||
// Bans have extra logic to handle ban reason
|
||||
const usersToSend = [] as Array<string | { username: string, reason: string }>;
|
||||
for (const user of this._controller.userManager.users.values()) {
|
||||
if (user.isBanned) {
|
||||
if (user.banReason && user.banReason !== "") {
|
||||
usersToSend.push({ username: user.name, reason: user.banReason });
|
||||
} else {
|
||||
usersToSend.push(user.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return usersToSend;
|
||||
}
|
||||
|
||||
// This is a whitelist or admins export
|
||||
const usersToSend = [] as string[];
|
||||
const prop = request.exportType === "admins" ? "isAdmin" : "isWhitelisted";
|
||||
for (const user of this._controller.userManager.users.values()) {
|
||||
if (user[prop]) {
|
||||
usersToSend.push(user.name);
|
||||
}
|
||||
}
|
||||
return usersToSend;
|
||||
}
|
||||
|
||||
async handleDebugDumpWsRequest(request: lib.DebugDumpWsRequest) {
|
||||
this.ws_dumper = data => {
|
||||
if (this.connector.connected) {
|
||||
|
||||
@@ -1836,6 +1836,122 @@ userCommands.add(new lib.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
async function handleImportOrRestore(args: {
|
||||
filepath: string,
|
||||
users: boolean,
|
||||
bans: boolean,
|
||||
admins: boolean,
|
||||
whitelist: boolean,
|
||||
noBackup?: boolean,
|
||||
}, control: Control, restore: boolean) {
|
||||
let importType: ConstructorParameters<typeof lib.UserBulkImportRequest>[0];
|
||||
const data = JSON.parse((await fs.readFile(args.filepath)).toString());
|
||||
const optionCount = [args.users, args.bans, args.admins, args.whitelist]
|
||||
.reduce((acc, bool) => (bool ? acc + 1 : acc), 0);
|
||||
|
||||
if (optionCount > 1) {
|
||||
throw new lib.CommandError("Can not specify multiple options");
|
||||
}
|
||||
|
||||
// Assign based on options or attempt to guess based on filename
|
||||
const filename = path.basename(args.filepath);
|
||||
if (args.users || (optionCount === 0 && data.export_version)) {
|
||||
importType = "users";
|
||||
} else if (args.bans || (optionCount === 0 && filename.includes("ban"))) {
|
||||
importType = "bans";
|
||||
} else if (args.admins || (optionCount === 0 && filename.includes("admin"))) {
|
||||
importType = "admins";
|
||||
} else if (args.whitelist || (optionCount === 0 && filename.includes("whitelist"))) {
|
||||
importType = "whitelist";
|
||||
} else {
|
||||
throw new lib.CommandError("Unknown json file, please specify an option");
|
||||
}
|
||||
|
||||
const backup = await control.send(
|
||||
new lib.UserBulkImportRequest(importType, importType === "users" ? data.users : data, restore)
|
||||
);
|
||||
|
||||
print(`${restore ? "Restored" : "Imported"} ${importType} from ${args.filepath}`);
|
||||
if (restore && backup && !args.noBackup) {
|
||||
await fs.writeJSON(`${importType}-backup.json`, backup, { spaces: 2 });
|
||||
print(`Wrote backup to ${importType}-backup.json`);
|
||||
}
|
||||
}
|
||||
|
||||
userCommands.add(new lib.Command({
|
||||
definition: ["import <filepath>", "Import user data from a file", (yargs) => {
|
||||
yargs.positional("filepath", { describe: "Path to the file to import", type: "string" });
|
||||
yargs.options({
|
||||
"users": { describe: "Import users json", nargs: 0, type: "boolean", default: false },
|
||||
"bans": { describe: "Import banlist json", nargs: 0, type: "boolean", default: false },
|
||||
"admins": { describe: "Import adminlist json", nargs: 0, type: "boolean", default: false },
|
||||
"whitelist": { describe: "Import whitelist json", nargs: 0, type: "boolean", default: false },
|
||||
});
|
||||
}],
|
||||
handler: async function(args: any, control: any) {
|
||||
await handleImportOrRestore(args, control, false);
|
||||
},
|
||||
}));
|
||||
|
||||
userCommands.add(new lib.Command({
|
||||
definition: ["restore <filepath>", "Restore user data from a file", (yargs) => {
|
||||
yargs.positional("filepath", { describe: "Path to the file to import", type: "string" });
|
||||
yargs.options({
|
||||
"users": { describe: "Restore users json", nargs: 0, type: "boolean", default: false },
|
||||
"bans": { describe: "Restore banlist json", nargs: 0, type: "boolean", default: false },
|
||||
"admins": { describe: "Restore adminlist json", nargs: 0, type: "boolean", default: false },
|
||||
"whitelist": { describe: "Restore whitelist json", nargs: 0, type: "boolean", default: false },
|
||||
"no-backup": { describe: "Don't save a backup to the cwd", nargs: 0, type: "boolean", default: false },
|
||||
});
|
||||
}],
|
||||
handler: async function(args: any, control: any) {
|
||||
await handleImportOrRestore(args, control, true);
|
||||
},
|
||||
}));
|
||||
|
||||
userCommands.add(new lib.Command({
|
||||
definition: ["export <filepath>", "Export user data to a file", (yargs) => {
|
||||
yargs.positional("filepath", { describe: "Path to the file to save to", type: "string" });
|
||||
yargs.options({
|
||||
"users": { describe: "Export users json", nargs: 0, type: "boolean", default: false },
|
||||
"bans": { describe: "Export banlist json", nargs: 0, type: "boolean", default: false },
|
||||
"admins": { describe: "Export adminlist json", nargs: 0, type: "boolean", default: false },
|
||||
"whitelist": { describe: "Export whitelist json", nargs: 0, type: "boolean", default: false },
|
||||
});
|
||||
}],
|
||||
handler: async function(args: {
|
||||
filepath: string,
|
||||
users: boolean,
|
||||
bans: boolean,
|
||||
admins: boolean,
|
||||
whitelist: boolean,
|
||||
}, control: Control) {
|
||||
let exportType = "users" as ConstructorParameters<typeof lib.UserBulkExportRequest>[0];
|
||||
const optionCount = [args.bans, args.admins, args.whitelist]
|
||||
.reduce((acc, bool) => (bool ? acc + 1 : acc), 0);
|
||||
|
||||
if (optionCount > 1) {
|
||||
throw new lib.CommandError("Can not specify multiple options");
|
||||
}
|
||||
|
||||
// Assign based on options or attempt to guess based on filename
|
||||
if (args.users) {
|
||||
exportType = "users";
|
||||
} else if (args.bans) {
|
||||
exportType = "bans";
|
||||
} else if (args.admins) {
|
||||
exportType = "admins";
|
||||
} else if (args.whitelist) {
|
||||
exportType = "whitelist";
|
||||
}
|
||||
|
||||
fs.writeJSON(args.filepath, await control.send(
|
||||
new lib.UserBulkExportRequest(exportType)
|
||||
), { spaces: 2 });
|
||||
print(`Exported ${exportType} to ${args.filepath}`);
|
||||
},
|
||||
}));
|
||||
|
||||
const logCommands = new lib.CommandTree({ name: "log", description: "Log inspection" });
|
||||
logCommands.add(new lib.Command({
|
||||
definition: ["follow", "follow cluster log", (yargs) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import Permission from "./Permission";
|
||||
import Role from "./Role";
|
||||
import User from "./User";
|
||||
import { JsonNumber, jsonArray } from "./composites";
|
||||
import User, { IControllerUser } from "./User";
|
||||
import { JsonNumber, StringEnum, jsonArray, plainJson } from "./composites";
|
||||
import { MessageRequest } from "./messages_core";
|
||||
|
||||
export class PermissionListRequest {
|
||||
declare ["constructor"]: typeof PermissionListRequest;
|
||||
@@ -320,3 +321,129 @@ export class UserUpdatesEvent {
|
||||
return new this(json.updates.map(update => User.fromJSON(update)));
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterioUserExport {
|
||||
public export_version = "2.0.0-alpha.20";
|
||||
|
||||
static clusterioUserSchema = Type.Object({
|
||||
"username": Type.String(),
|
||||
"is_admin": Type.Optional(Type.Boolean()),
|
||||
"is_whitelisted": Type.Optional(Type.Boolean()),
|
||||
"is_banned": Type.Optional(Type.Boolean()),
|
||||
"ban_reason": Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
static factorioUserSchema = Type.Union([
|
||||
Type.String(),
|
||||
Type.Object({
|
||||
"username": Type.String(),
|
||||
"reason": Type.String(),
|
||||
}),
|
||||
]);
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"export_version": Type.String(),
|
||||
"users": Type.Array(this.clusterioUserSchema),
|
||||
});
|
||||
|
||||
constructor(
|
||||
public users: Static<typeof ClusterioUserExport.jsonSchema.properties.users>,
|
||||
) { }
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
const obj = new this(json.users);
|
||||
obj.export_version = json.export_version;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export class UserBulkImportRequest {
|
||||
declare ["constructor"]: typeof UserBulkImportRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static permission(user: IControllerUser, message: MessageRequest) {
|
||||
if (typeof message.data === "object" && message.data !== null) {
|
||||
const data = message.data as Static<typeof UserBulkImportRequest.jsonSchema>;
|
||||
// Check if this is a restore or import request
|
||||
if (data.restore) {
|
||||
user.checkPermission("core.user.bulk_restore");
|
||||
} else {
|
||||
user.checkPermission("core.user.bulk_import");
|
||||
}
|
||||
// Check if they have the permission for that type
|
||||
switch (data.importType) {
|
||||
case "users":
|
||||
user.checkPermission("core.user.set_admin");
|
||||
user.checkPermission("core.user.set_banned");
|
||||
user.checkPermission("core.user.set_whitelisted");
|
||||
break;
|
||||
case "admins":
|
||||
user.checkPermission("core.user.set_admin");
|
||||
break;
|
||||
case "bans":
|
||||
user.checkPermission("core.user.set_banned");
|
||||
break;
|
||||
case "whitelist":
|
||||
user.checkPermission("core.user.set_whitelisted");
|
||||
break;
|
||||
default:
|
||||
// @ts-expect-error Unreachable
|
||||
throw new Error(`Unknown import / restore type: ${data.importType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Response = plainJson(Type.Union([
|
||||
Type.Array(ClusterioUserExport.factorioUserSchema),
|
||||
ClusterioUserExport.jsonSchema,
|
||||
]));
|
||||
|
||||
constructor(
|
||||
public importType: "users" | "bans" | "admins" | "whitelist",
|
||||
public users: Static<typeof ClusterioUserExport.clusterioUserSchema>[]
|
||||
| Static<typeof ClusterioUserExport.factorioUserSchema>[],
|
||||
public restore?: boolean
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Union([
|
||||
Type.Object({
|
||||
"importType": StringEnum(["bans", "admins", "whitelist"]),
|
||||
"users": Type.Array(ClusterioUserExport.factorioUserSchema),
|
||||
"restore": Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
Type.Object({
|
||||
"importType": Type.Literal("users"),
|
||||
"users": Type.Array(ClusterioUserExport.clusterioUserSchema),
|
||||
"restore": Type.Optional(Type.Boolean()),
|
||||
}),
|
||||
]);
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.importType, json.users, json.restore);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserBulkExportRequest {
|
||||
declare ["constructor"]: typeof UserBulkExportRequest;
|
||||
static type = "request" as const;
|
||||
static src = "control" as const;
|
||||
static dst = "controller" as const;
|
||||
static permission = "core.user.bulk_export" as const;
|
||||
static Response = plainJson(Type.Union([
|
||||
Type.Array(ClusterioUserExport.factorioUserSchema),
|
||||
ClusterioUserExport.jsonSchema,
|
||||
]));
|
||||
|
||||
constructor(
|
||||
public exportType: "users" | "bans" | "admins" | "whitelist",
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"exportType": StringEnum(["users", "bans", "admins", "whitelist"]),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.exportType);
|
||||
}
|
||||
}
|
||||
@@ -118,4 +118,6 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
user.UserSetBannedRequest,
|
||||
user.UserDeleteRequest,
|
||||
user.UserUpdatesEvent,
|
||||
user.UserBulkImportRequest,
|
||||
user.UserBulkExportRequest,
|
||||
];
|
||||
@@ -484,6 +484,24 @@ definePermission({
|
||||
title: "Delete user",
|
||||
description: "Delete users and all data stored for them.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.user.bulk_import",
|
||||
title: "Bulk user import",
|
||||
description: "Bulk import users including admin, whitelist, and bans." +
|
||||
" (Imports types are restricted by other permissions, e.g. core.user.set_admin)",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.user.bulk_export",
|
||||
title: "Bulk user export",
|
||||
description: "Bulk export users including admin, whitelist, and bans.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.user.bulk_restore",
|
||||
title: "Bulk user restore",
|
||||
description: "Bulk restore users including admin, whitelist, and bans." +
|
||||
" (Restore types are restricted by other permissions, e.g. core.user.set_admin)",
|
||||
});
|
||||
|
||||
definePermission({
|
||||
name: "core.log.follow",
|
||||
title: "Follow cluster log",
|
||||
|
||||
@@ -24,6 +24,8 @@ function isActiveDropzone(element: HTMLElement | null): boolean {
|
||||
while (element && depth < checkDepth) {
|
||||
if (element.classList.contains("dropzone")) {
|
||||
return element.classList.contains("enabled");
|
||||
} else if (element.classList.contains("ant-upload-drag")) {
|
||||
return !element.classList.contains("disabled");
|
||||
}
|
||||
element = element.parentElement;
|
||||
depth += 1;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useEffect, useContext, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Form, Input, Modal, Space, Table, Tag } from "antd";
|
||||
import { Button, Form, FormInstance, GetProp, Input, Modal, Radio, Space, Table, Tag, Upload, UploadProps } from "antd";
|
||||
import { InboxOutlined } from "@ant-design/icons";
|
||||
import { Static } from "@sinclair/typebox";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
@@ -16,7 +18,6 @@ import Link from "./Link";
|
||||
|
||||
const strcmp = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare;
|
||||
|
||||
|
||||
function CreateUserButton() {
|
||||
let control = useContext(ControlContext);
|
||||
let navigate = useNavigate();
|
||||
@@ -57,6 +58,327 @@ function CreateUserButton() {
|
||||
</>;
|
||||
}
|
||||
|
||||
// This is the most common and best supported method
|
||||
// For full support we should consider using npm:file-saver
|
||||
function saveFile(name: string, blob: Blob) {
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = name;
|
||||
a.addEventListener("click", (e) => {
|
||||
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
|
||||
});
|
||||
a.click();
|
||||
};
|
||||
|
||||
function saveJson(name: string, json: object) {
|
||||
return saveFile(name, new Blob([JSON.stringify(json, null, 2)], { type: "application/json" }));
|
||||
}
|
||||
|
||||
type UserBulkActionProps = {
|
||||
setApplyAction(func: () => Promise<void>): void,
|
||||
form: FormInstance,
|
||||
restore?: boolean,
|
||||
}
|
||||
|
||||
function UserBulkActionImport({ setApplyAction, form, restore }: UserBulkActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
const account = useAccount();
|
||||
|
||||
// Combines an array of users into the main array to be sent to the controller
|
||||
function combineMixed(
|
||||
prop: "is_admin" | "is_banned" | "is_whitelisted",
|
||||
users: Array<Static<typeof lib.ClusterioUserExport.factorioUserSchema>>,
|
||||
usersToSend: Map<string, Static<typeof lib.ClusterioUserExport.clusterioUserSchema>>
|
||||
) {
|
||||
for (const user of users) {
|
||||
if (typeof user !== "string") {
|
||||
// It can only be a ban in this case
|
||||
const existingUser = usersToSend.get(user.username);
|
||||
if (existingUser) {
|
||||
existingUser.is_banned = true;
|
||||
existingUser.ban_reason = existingUser.ban_reason ? existingUser.ban_reason : user.reason;
|
||||
} else {
|
||||
usersToSend.set(user.username, {
|
||||
is_banned: true,
|
||||
...user,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// We only have the username
|
||||
const existingUser = usersToSend.get(user);
|
||||
if (existingUser) {
|
||||
existingUser[prop] = true;
|
||||
} else {
|
||||
usersToSend.set(user, {
|
||||
username: user,
|
||||
[prop]: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a user json and combine into the format to be sent to the controller
|
||||
function parseUsers(
|
||||
users: Array<Static<typeof lib.ClusterioUserExport.clusterioUserSchema>>,
|
||||
usersToSend: Map<string, Static<typeof lib.ClusterioUserExport.clusterioUserSchema>>
|
||||
) {
|
||||
for (const user of users) {
|
||||
const existingUser = usersToSend.get(user.username);
|
||||
// Combine all fields with an existing user
|
||||
if (existingUser) {
|
||||
existingUser.is_admin ||= user.is_admin;
|
||||
existingUser.is_banned ||= user.is_banned;
|
||||
existingUser.is_whitelisted ||= user.is_whitelisted;
|
||||
existingUser.ban_reason = existingUser.ban_reason ? existingUser.ban_reason : user.ban_reason;
|
||||
}
|
||||
// We do not send false values
|
||||
if (user.is_admin === false) {
|
||||
delete user.is_admin;
|
||||
}
|
||||
if (user.is_banned === false) {
|
||||
delete user.is_banned;
|
||||
delete user.ban_reason;
|
||||
}
|
||||
if (user.is_whitelisted === false) {
|
||||
delete user.is_whitelisted;
|
||||
}
|
||||
usersToSend.set(user.username, user);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a ban json and combine into the format to be sent to the controller
|
||||
function parseBans(
|
||||
users: Array<Static<typeof lib.ClusterioUserExport.factorioUserSchema>>,
|
||||
usersToSend: Map<string, string | { username: string, reason: string }>
|
||||
) {
|
||||
for (const user of users) {
|
||||
if (typeof user === "string") {
|
||||
if (!usersToSend.has(user)) {
|
||||
usersToSend.set(user, user);
|
||||
}
|
||||
} else if (!user.reason) {
|
||||
if (!usersToSend.has(user.username)) {
|
||||
usersToSend.set(user.username, user.username);
|
||||
}
|
||||
} else {
|
||||
usersToSend.set(user.username, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse an admin or whitelist json and combine into the format to be sent to the controller
|
||||
function parseAdminsWhitelist(
|
||||
users: Array<string>,
|
||||
usersToSend: Set<string>,
|
||||
) {
|
||||
for (const user of users) {
|
||||
usersToSend.add(user);
|
||||
}
|
||||
}
|
||||
|
||||
setApplyAction(async () => {
|
||||
let backup: object | undefined;
|
||||
const values = form.getFieldsValue();
|
||||
const importType = restore ? values.restoreType : values.importType;
|
||||
switch (importType) {
|
||||
case "mixed": {
|
||||
// Parse and combine multiple json types then send
|
||||
const usersToSend = new Map<string, Static<typeof lib.ClusterioUserExport.clusterioUserSchema>>();
|
||||
for (const file of values.fileList as File[]) {
|
||||
const json = JSON.parse(await file.text());
|
||||
if (json.export_version) {
|
||||
parseUsers(json.users, usersToSend);
|
||||
} else if (file.name.includes("ban")) {
|
||||
const users = new Map();
|
||||
parseBans(json, users);
|
||||
combineMixed("is_banned", [...users.values()], usersToSend);
|
||||
} else if (file.name.includes("admin")) {
|
||||
const users = new Set<string>();
|
||||
parseAdminsWhitelist(json, users);
|
||||
combineMixed("is_admin", [...users.values()], usersToSend);
|
||||
} else if (file.name.includes("whitelist")) {
|
||||
const users = new Set<string>();
|
||||
parseAdminsWhitelist(json, users);
|
||||
combineMixed("is_whitelisted", [...users.values()], usersToSend);
|
||||
} else {
|
||||
throw new Error(`Unknown json (could not guess by file name): ${file.name}`);
|
||||
}
|
||||
}
|
||||
backup = await control.send(new lib.UserBulkImportRequest("users", [...usersToSend.values()], restore));
|
||||
break;
|
||||
}
|
||||
|
||||
case "users": {
|
||||
// Parse and user jsons then send
|
||||
const usersToSend = new Map<string, Static<typeof lib.ClusterioUserExport.clusterioUserSchema>>();
|
||||
for (const file of values.fileList as File[]) {
|
||||
parseUsers(JSON.parse(await file.text()).users, usersToSend);
|
||||
}
|
||||
backup = await control.send(new lib.UserBulkImportRequest("users", [...usersToSend.values()], restore));
|
||||
break;
|
||||
}
|
||||
|
||||
case "bans": {
|
||||
// Parse and combine ban jsons then send
|
||||
const usersToSend = new Map<string, Static<typeof lib.ClusterioUserExport.factorioUserSchema>>();
|
||||
for (const file of values.fileList as File[]) {
|
||||
parseBans(JSON.parse(await file.text()), usersToSend);
|
||||
}
|
||||
backup = await control.send(new lib.UserBulkImportRequest("bans", [...usersToSend.values()], restore));
|
||||
break;
|
||||
}
|
||||
|
||||
case "admins":
|
||||
case "whitelist": {
|
||||
// Parse and combine admin or whitelist jsons then send
|
||||
const usersToSend = new Set<string>();
|
||||
for (const file of values.fileList as File[]) {
|
||||
parseAdminsWhitelist(JSON.parse(await file.text()), usersToSend);
|
||||
}
|
||||
backup = await control.send(
|
||||
new lib.UserBulkImportRequest(importType, [...usersToSend.keys()], restore)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Should be unreachable
|
||||
throw new Error(`Unknown importType: ${importType}`);
|
||||
}
|
||||
}
|
||||
if (restore && backup) {
|
||||
saveJson(`${importType}-backup.json`, backup);
|
||||
}
|
||||
});
|
||||
|
||||
const normaliseFiles = (e: any) => (Array.isArray(e) ? e : e.fileList).map((f: any) => f.originFileObj);
|
||||
const uploadProps: UploadProps = {
|
||||
accept: ".json",
|
||||
multiple: true,
|
||||
beforeUpload: (file) => false,
|
||||
};
|
||||
|
||||
return <>
|
||||
<Form.Item
|
||||
label="Type"
|
||||
name={restore ? "restoreType" : "importType"}
|
||||
initialValue={restore ? "users" : "mixed"}
|
||||
>
|
||||
<Radio.Group>
|
||||
{restore ? undefined : <Radio.Button value="mixed" disabled={!account.hasAnyPermission(
|
||||
"core.user.set_admin", "core.user.set_banned", "core.user.set_whitelisted"
|
||||
)}>Mixed</Radio.Button>}
|
||||
<Radio.Button value="users" disabled={!account.hasAllPermission(
|
||||
"core.user.set_admin", "core.user.set_banned", "core.user.set_whitelisted"
|
||||
)}>Users</Radio.Button>
|
||||
<Radio.Button value="admins" disabled={!account.hasPermission(
|
||||
"core.user.set_admin"
|
||||
)}>Admins</Radio.Button>
|
||||
<Radio.Button value="bans" disabled={!account.hasPermission(
|
||||
"core.user.set_banned"
|
||||
)}>Bans</Radio.Button>
|
||||
<Radio.Button value="whitelist" disabled={!account.hasPermission(
|
||||
"core.user.set_whitelisted"
|
||||
)}>Whitelist</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item name="fileList" valuePropName="fileList" getValueFromEvent={normaliseFiles} noStyle>
|
||||
<Upload.Dragger {...uploadProps}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">Click or drag file to this area to import</p>
|
||||
<p className="ant-upload-hint">Support for a single or bulk import.</p>
|
||||
</Upload.Dragger>
|
||||
</Form.Item>
|
||||
</>;
|
||||
}
|
||||
|
||||
function UserBulkActionExport({ setApplyAction, form }: UserBulkActionProps) {
|
||||
const control = useContext(ControlContext);
|
||||
|
||||
setApplyAction(async () => {
|
||||
const values = form.getFieldsValue();
|
||||
const response = await control.send(new lib.UserBulkExportRequest(values.exportType));
|
||||
switch (values.exportType) {
|
||||
case "users": return saveJson("clusterio-userlist.json", response);
|
||||
case "admins": return saveJson("server-adminlist.json", response);
|
||||
case "bans": return saveJson("server-banlist.json", response);
|
||||
case "whitelist": return saveJson("server-whitelist.json", response);
|
||||
default: throw new Error(`Unknown exportType: ${values.importType}`);
|
||||
}
|
||||
});
|
||||
|
||||
return <>
|
||||
<Form.Item label="Type" name="exportType" initialValue="users">
|
||||
<Radio.Group>
|
||||
<Radio.Button value="users">Users</Radio.Button>
|
||||
<Radio.Button value="admins">Admins</Radio.Button>
|
||||
<Radio.Button value="bans">Bans</Radio.Button>
|
||||
<Radio.Button value="whitelist">Whitelist</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</>;
|
||||
}
|
||||
|
||||
function BulkUserActionButton() {
|
||||
const account = useAccount();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [formAction, setFormAction] = useState<string | undefined>(undefined);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
function onValuesChange({ action } : { action?: string }) {
|
||||
if (action) {
|
||||
setFormAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
let applyAction: () => Promise<void>;
|
||||
const setApplyAction = (func: () => Promise<void>) => { applyAction = func; };
|
||||
async function onOk() {
|
||||
if (!applyAction) {
|
||||
form.setFields([{ name: "action", errors: ["Action is required"] }]);
|
||||
return;
|
||||
}
|
||||
await applyAction();
|
||||
setFormAction(undefined);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => { setOpen(true); }}
|
||||
>Bulk Actions</Button>
|
||||
<Modal
|
||||
title="Bulk Actions"
|
||||
okText="Apply"
|
||||
open={open}
|
||||
okButtonProps={{disabled: formAction === undefined}}
|
||||
onOk={() => { onOk().catch(notifyErrorHandler(`Error running ${formAction}`)); }}
|
||||
onCancel={() => { setOpen(false); }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} onValuesChange={onValuesChange} clearOnDestroy>
|
||||
<Form.Item label="Action" name="action">
|
||||
<Radio.Group value={formAction}>
|
||||
{account.hasPermission("core.user.bulk_import")
|
||||
? <Radio.Button value="import">Import</Radio.Button> : undefined}
|
||||
{account.hasPermission("core.user.bulk_export")
|
||||
? <Radio.Button value="export">Export</Radio.Button> : undefined}
|
||||
{account.hasPermission("core.user.bulk_restore")
|
||||
? <Radio.Button value="restore">Restore</Radio.Button> : undefined}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{formAction === "import" ? <UserBulkActionImport {...{setApplyAction, form}}/> : undefined}
|
||||
{formAction === "export" ? <UserBulkActionExport {...{setApplyAction, form}}/> : undefined}
|
||||
{formAction === "restore" ? <UserBulkActionImport restore {...{setApplyAction, form}}/> : undefined}
|
||||
</Form>
|
||||
</Modal>
|
||||
</>;
|
||||
}
|
||||
|
||||
export default function UsersPage() {
|
||||
let account = useAccount();
|
||||
let control = useContext(ControlContext);
|
||||
@@ -76,7 +398,11 @@ export default function UsersPage() {
|
||||
return <PageLayout nav={[{ name: "Users" }]}>
|
||||
<PageHeader
|
||||
title="Users"
|
||||
extra={account.hasPermission("core.user.create") ? <CreateUserButton /> : undefined}
|
||||
extra={<>
|
||||
{account.hasPermission("core.user.create") ? <CreateUserButton /> : undefined}
|
||||
{account.hasAnyPermission("core.user.bulk_import", "core.user.bulk_export")
|
||||
? <BulkUserActionButton /> : undefined}
|
||||
</>}
|
||||
/>
|
||||
<Table
|
||||
columns={[
|
||||
|
||||
@@ -1397,5 +1397,248 @@ describe("Integration of Clusterio", function() {
|
||||
assert.equal(getControl().userUpdates[0].isDeleted, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("user import", function() {
|
||||
it("should import users", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-users.json"), {
|
||||
export_version: "2.0.0.alpha20",
|
||||
users: [
|
||||
{ username: "import_user_admin", is_admin: true },
|
||||
{ username: "import_user_banned", is_banned: true },
|
||||
{ username: "import_user_ban_reason", is_banned: true, ban_reason: "banned" },
|
||||
{ username: "import_user_whitelist", is_whitelisted: true },
|
||||
{ username: "import_user_none" },
|
||||
],
|
||||
});
|
||||
|
||||
await execCtl("user import --users import-users.json");
|
||||
|
||||
const users = await getUsers();
|
||||
assert(users.get("import_user_admin").isAdmin, "import_user_admin was not admin");
|
||||
assert(users.get("import_user_banned").isBanned, "import_user_banned was not banned");
|
||||
assert(
|
||||
users.get("import_user_whitelist").isWhitelisted,
|
||||
"import_user_whitelist was not whitelisted"
|
||||
);
|
||||
|
||||
const userNone = users.get("import_user_none");
|
||||
assert(!userNone.isAdmin, "import_user_none was admin");
|
||||
assert(!userNone.isBanned, "import_user_none was banned");
|
||||
assert(!userNone.isWhitelisted, "import_user_none was whitelisted");
|
||||
|
||||
const userBanReason = users.get("import_user_ban_reason");
|
||||
assert(userBanReason.isBanned, "import_user_ban_reason was not banned");
|
||||
assert.equal(userBanReason.banReason, "banned", "import_user_ban_reason had incorrect ban reason");
|
||||
});
|
||||
it("should import users by json format", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-users.json"), {
|
||||
export_version: "2.0.0.alpha20",
|
||||
users: [{ username: "import_user", is_admin: true, is_whitelisted: true }],
|
||||
});
|
||||
|
||||
await execCtl("user import import-users.json");
|
||||
const userFoo = (await getUsers()).get("import_user");
|
||||
assert(userFoo.isAdmin, "import_user was not admin");
|
||||
assert(userFoo.isWhitelisted, "import_user was not whitelisted");
|
||||
});
|
||||
it("should import bans", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-bans.json"), [
|
||||
{ username: "import_ban_reason", reason: "banned" },
|
||||
"import_ban",
|
||||
]);
|
||||
|
||||
await execCtl("user import --bans import-bans.json");
|
||||
|
||||
const users = await getUsers();
|
||||
|
||||
const userFoo = users.get("import_ban_reason");
|
||||
assert(userFoo.isBanned, "import_ban_reason was not banned");
|
||||
assert.equal(userFoo.banReason, "banned", "import_ban_reason had incorrect ban reason");
|
||||
|
||||
const userBar = users.get("import_ban");
|
||||
assert(userBar.isBanned, "import_ban was not banned");
|
||||
assert.equal(userBar.banReason, "", "import_ban had incorrect ban reason");
|
||||
});
|
||||
it("should import bans by filename", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-bans.json"), [
|
||||
"import_ban_filename",
|
||||
]);
|
||||
|
||||
await execCtl("user import import-bans.json");
|
||||
assert((await getUsers()).get("import_ban_filename").isBanned, "import_ban_filename was not banned");
|
||||
});
|
||||
it("should import admins", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-admins.json"), [
|
||||
"import_admin",
|
||||
]);
|
||||
|
||||
await execCtl("user import --admins import-admins.json");
|
||||
assert((await getUsers()).get("import_admin").isAdmin, "import_admin was not admin");
|
||||
});
|
||||
it("should import admins by filename", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-admins.json"), [
|
||||
"import_admin_filename",
|
||||
]);
|
||||
|
||||
await execCtl("user import import-admins.json");
|
||||
assert((await getUsers()).get("import_admin_filename").isAdmin, "import_admin_filename was not admin");
|
||||
});
|
||||
it("should import whitelist", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-whitelist.json"), [
|
||||
"import_whitelist",
|
||||
]);
|
||||
|
||||
await execCtl("user import --whitelist import-whitelist.json");
|
||||
assert(
|
||||
(await getUsers()).get("import_whitelist").isWhitelisted,
|
||||
"import_whitelist was not whitelisted"
|
||||
);
|
||||
});
|
||||
it("should import whitelist by filename", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "import-whitelist.json"), [
|
||||
"import_whitelist_filename",
|
||||
]);
|
||||
|
||||
await execCtl("user import import-whitelist.json");
|
||||
assert(
|
||||
(await getUsers()).get("import_whitelist_filename").isWhitelisted,
|
||||
"import_whitelist_filename was not whitelisted"
|
||||
);
|
||||
});
|
||||
it("should reject multiple provided options", async function() {
|
||||
assert.rejects(execCtl("user import --bans --admins import-admins.json"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("user export", function() {
|
||||
before(async function() {
|
||||
await execCtl("user create export_control");
|
||||
});
|
||||
it("should export users", async function() {
|
||||
slowTest(this);
|
||||
await execCtl("user set-admin export_user --create");
|
||||
await execCtl("user set-whitelisted export_user");
|
||||
await execCtl("user export --users user-export.json");
|
||||
const data = await fs.readJson(path.join("temp", "test", "user-export.json"));
|
||||
assert(data.users, "Users array does not exist");
|
||||
assert(!data.users.find(u => u.username === "export_control"), "Control user is present");
|
||||
assert.deepEqual(data.users.find(u => u.username === "export_user"), {
|
||||
username: "export_user", is_admin: true, is_whitelisted: true,
|
||||
});
|
||||
});
|
||||
it("should export bans", async function() {
|
||||
slowTest(this);
|
||||
await execCtl("user set-banned export_ban --create");
|
||||
await execCtl("user set-banned export_ban_reason --create --reason banned");
|
||||
await execCtl("user export --bans ban-export.json");
|
||||
const data = await fs.readJson(path.join("temp", "test", "ban-export.json"));
|
||||
assert.equal(data.indexOf("export_control"), -1, "Control user is present");
|
||||
assert(data.indexOf("export_ban") >= 0, "export_ban is missing");
|
||||
assert.deepEqual(data.find(u => typeof u === "object" && u.username === "export_ban_reason"), {
|
||||
username: "export_ban_reason", reason: "banned",
|
||||
});
|
||||
});
|
||||
it("should export admins", async function() {
|
||||
await execCtl("user set-admin export_admin --create");
|
||||
await execCtl("user export --admins admin-export.json");
|
||||
const data = await fs.readJson(path.join("temp", "test", "admin-export.json"));
|
||||
assert.equal(data.indexOf("export_control"), -1, "Control user is present");
|
||||
assert(data.indexOf("export_admin") >= 0, "export_admin is missing");
|
||||
});
|
||||
it("should export whitelist", async function() {
|
||||
await execCtl("user set-whitelisted export_whitelist --create");
|
||||
await execCtl("user export --whitelist whitelist-export.json");
|
||||
const data = await fs.readJson(path.join("temp", "test", "whitelist-export.json"));
|
||||
assert.equal(data.indexOf("export_control"), -1, "Control user is present");
|
||||
assert(data.indexOf("export_whitelist") >= 0, "export_whitelist is missing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("user restore", function() {
|
||||
beforeEach(async function() {
|
||||
slowTest(this);
|
||||
await execCtl("user set-admin restore_control --create");
|
||||
await execCtl("user set-banned restore_control");
|
||||
await execCtl("user set-whitelisted restore_control");
|
||||
});
|
||||
it("should restore users", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "restore-users.json"), {
|
||||
export_version: "2.0.0.alpha20",
|
||||
users: [{ username: "restore_user", is_admin: true, is_whitelisted: true }],
|
||||
});
|
||||
|
||||
await execCtl("user restore restore-users.json");
|
||||
const users = await getUsers();
|
||||
|
||||
const user = users.get("restore_user");
|
||||
assert(user.isAdmin, "restore_user was not admin");
|
||||
assert(user.isWhitelisted, "restore_user was not whitelisted");
|
||||
|
||||
const control = users.get("restore_control");
|
||||
assert(!control.isAdmin, "restore_user was admin");
|
||||
assert(!control.isBanned, "restore_user was banned");
|
||||
assert(!control.isWhitelisted, "restore_user was whitelisted");
|
||||
|
||||
const data = await fs.readJson(path.join("temp", "test", "users-backup.json"));
|
||||
assert(data.users, "Users array does not exist");
|
||||
assert.deepEqual(data.users.find(u => u.username === "restore_control"), {
|
||||
username: "restore_control", is_admin: true, is_banned: true, is_whitelisted: true,
|
||||
});
|
||||
});
|
||||
it("should restore bans", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "restore-bans.json"), [
|
||||
{ username: "restore_ban_reason", reason: "banned" },
|
||||
"restore_ban",
|
||||
]);
|
||||
|
||||
await execCtl("user restore --bans restore-bans.json");
|
||||
|
||||
const users = await getUsers();
|
||||
|
||||
const userFoo = users.get("restore_ban_reason");
|
||||
assert(userFoo.isBanned, "restore_ban_reason was not banned");
|
||||
assert.equal(userFoo.banReason, "banned", "restore_ban_reason had incorrect ban reason");
|
||||
|
||||
const userBar = users.get("restore_ban");
|
||||
assert(userBar.isBanned, "restore_ban was not banned");
|
||||
assert.equal(userBar.banReason, "", "restore_ban had incorrect ban reason");
|
||||
|
||||
assert(!users.get("restore_control").isBanned, "restore_user was banned");
|
||||
|
||||
const data = await fs.readJson(path.join("temp", "test", "bans-backup.json"));
|
||||
assert(data.indexOf("restore_control") >= 0, "restore_control is missing");
|
||||
});
|
||||
it("should restore admins", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "restore-admins.json"), [
|
||||
"restore_admin",
|
||||
]);
|
||||
|
||||
await execCtl("user restore --admins restore-admins.json");
|
||||
const users = await getUsers();
|
||||
|
||||
assert(users.get("restore_admin").isAdmin, "restore_admin was not admin");
|
||||
assert(!users.get("restore_control").isAdmin, "restore_user was admin");
|
||||
|
||||
const data = await fs.readJson(path.join("temp", "test", "admins-backup.json"));
|
||||
assert(data.indexOf("restore_control") >= 0, "restore_control is missing");
|
||||
});
|
||||
it("should restore whitelist", async function() {
|
||||
await fs.writeJSON(path.join("temp", "test", "restore-whitelist.json"), [
|
||||
"restore_whitelist",
|
||||
]);
|
||||
|
||||
await execCtl("user restore --whitelist restore-whitelist.json");
|
||||
const users = await getUsers();
|
||||
|
||||
assert(users.get("restore_whitelist").isWhitelisted, "restore_whitelist was not whitelisted");
|
||||
assert(!users.get("restore_control").isWhitelisted, "restore_user was whitelisted");
|
||||
|
||||
const data = await fs.readJson(path.join("temp", "test", "whitelist-backup.json"));
|
||||
assert(data.indexOf("restore_control") >= 0, "restore_control is missing");
|
||||
});
|
||||
it("should reject multiple provided options", async function() {
|
||||
assert.rejects(execCtl("user restore --bans --admins restore-admins.json"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user