feat(dashboard): add support for importing/exporting cases
This commit is contained in:
parent
f3dae65747
commit
45941e47d6
9 changed files with 593 additions and 2 deletions
163
backend/src/api/guilds/importExport.ts
Normal file
163
backend/src/api/guilds/importExport.ts
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import { requireGuildPermission } from "../permissions";
|
||||||
|
import { clientError, ok } from "../responses";
|
||||||
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Case } from "../../data/entities/Case";
|
||||||
|
|
||||||
|
const caseHandlingModeSchema = z.union([
|
||||||
|
z.literal("replace"),
|
||||||
|
z.literal("bumpExistingCases"),
|
||||||
|
z.literal("bumpImportedCases"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
type CaseHandlingMode = z.infer<typeof caseHandlingModeSchema>;
|
||||||
|
|
||||||
|
const caseNoteData = z.object({
|
||||||
|
mod_id: z.string(),
|
||||||
|
mod_name: z.string(),
|
||||||
|
body: z.string(),
|
||||||
|
created_at: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const caseData = z.object({
|
||||||
|
case_number: z.number(),
|
||||||
|
user_id: z.string(),
|
||||||
|
user_name: z.string(),
|
||||||
|
mod_id: z.nullable(z.string()),
|
||||||
|
mod_name: z.nullable(z.string()),
|
||||||
|
type: z.number(),
|
||||||
|
created_at: z.string(),
|
||||||
|
is_hidden: z.boolean(),
|
||||||
|
pp_id: z.nullable(z.string()),
|
||||||
|
pp_name: z.nullable(z.string()),
|
||||||
|
|
||||||
|
notes: z.array(caseNoteData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const importExportData = z.object({
|
||||||
|
cases: z.array(caseData),
|
||||||
|
});
|
||||||
|
type TImportExportData = z.infer<typeof importExportData>;
|
||||||
|
|
||||||
|
export function initGuildsImportExportAPI(guildRouter: express.Router) {
|
||||||
|
const importExportRouter = express.Router();
|
||||||
|
|
||||||
|
importExportRouter.get(
|
||||||
|
"/:guildId/pre-import",
|
||||||
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||||
|
const minNum = await guildCases.getMinCaseNumber();
|
||||||
|
const maxNum = await guildCases.getMaxCaseNumber();
|
||||||
|
|
||||||
|
return {
|
||||||
|
minCaseNumber: minNum,
|
||||||
|
maxCaseNumber: maxNum,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
importExportRouter.post(
|
||||||
|
"/:guildId/import",
|
||||||
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
let data: TImportExportData;
|
||||||
|
try {
|
||||||
|
data = importExportData.parse(req.body.data);
|
||||||
|
} catch (err) {
|
||||||
|
return clientError(res, "Invalid import data format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let caseHandlingMode: CaseHandlingMode;
|
||||||
|
try {
|
||||||
|
caseHandlingMode = caseHandlingModeSchema.parse(req.body.caseHandlingMode);
|
||||||
|
} catch (err) {
|
||||||
|
return clientError(res, "Invalid case handling mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||||
|
|
||||||
|
// Prepare cases
|
||||||
|
if (caseHandlingMode === "replace") {
|
||||||
|
// Replace existing cases
|
||||||
|
await guildCases.deleteAllCases();
|
||||||
|
} else if (caseHandlingMode === "bumpExistingCases") {
|
||||||
|
// Bump existing numbers
|
||||||
|
const maxNumberInData = data.cases.reduce((max, theCase) => Math.max(max, theCase.case_number), 0);
|
||||||
|
await guildCases.bumpCaseNumbers(maxNumberInData);
|
||||||
|
} else if (caseHandlingMode === "bumpImportedCases") {
|
||||||
|
const maxExistingNumber = await guildCases.getMaxCaseNumber();
|
||||||
|
for (const theCase of data.cases) {
|
||||||
|
theCase.case_number += maxExistingNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import cases
|
||||||
|
for (const theCase of data.cases) {
|
||||||
|
const insertData: any = {
|
||||||
|
...theCase,
|
||||||
|
is_hidden: theCase.is_hidden ? 1 : 0,
|
||||||
|
guild_id: req.params.guildId,
|
||||||
|
notes: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const caseInsertData = await guildCases.createInternal(insertData);
|
||||||
|
for (const note of theCase.notes) {
|
||||||
|
await guildCases.createNote(caseInsertData.identifiers[0].id, note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(res);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const exportBatchSize = 500;
|
||||||
|
importExportRouter.post(
|
||||||
|
"/:guildId/export",
|
||||||
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const guildCases = GuildCases.getGuildInstance(req.params.guildId);
|
||||||
|
|
||||||
|
const data: TImportExportData = {
|
||||||
|
cases: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let n = 0;
|
||||||
|
let cases: Case[];
|
||||||
|
do {
|
||||||
|
cases = await guildCases.getExportCases(n, exportBatchSize);
|
||||||
|
n += cases.length;
|
||||||
|
|
||||||
|
for (const theCase of cases) {
|
||||||
|
data.cases.push({
|
||||||
|
case_number: theCase.case_number,
|
||||||
|
user_id: theCase.user_id,
|
||||||
|
user_name: theCase.user_name,
|
||||||
|
mod_id: theCase.mod_id,
|
||||||
|
mod_name: theCase.mod_name,
|
||||||
|
type: theCase.type,
|
||||||
|
created_at: theCase.created_at,
|
||||||
|
is_hidden: theCase.is_hidden,
|
||||||
|
pp_id: theCase.pp_id,
|
||||||
|
pp_name: theCase.pp_name,
|
||||||
|
|
||||||
|
notes: theCase.notes.map((note) => ({
|
||||||
|
mod_id: note.mod_id,
|
||||||
|
mod_name: note.mod_name,
|
||||||
|
body: note.body,
|
||||||
|
created_at: note.created_at,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} while (cases.length === exportBatchSize);
|
||||||
|
|
||||||
|
res.json(data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
guildRouter.use("/", importExportRouter);
|
||||||
|
}
|
14
backend/src/api/guilds/index.ts
Normal file
14
backend/src/api/guilds/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import express from "express";
|
||||||
|
import { apiTokenAuthHandlers } from "../auth";
|
||||||
|
import { initGuildsMiscAPI } from "./misc";
|
||||||
|
import { initGuildsImportExportAPI } from "./importExport";
|
||||||
|
|
||||||
|
export function initGuildsAPI(app: express.Express) {
|
||||||
|
const guildRouter = express.Router();
|
||||||
|
guildRouter.use(...apiTokenAuthHandlers());
|
||||||
|
|
||||||
|
initGuildsMiscAPI(guildRouter);
|
||||||
|
initGuildsImportExportAPI(guildRouter);
|
||||||
|
|
||||||
|
app.use("/guilds", guildRouter);
|
||||||
|
}
|
180
backend/src/api/guilds/misc.ts
Normal file
180
backend/src/api/guilds/misc.ts
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import { ApiPermissions } from "@shared/apiPermissions";
|
||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import { YAMLException } from "js-yaml";
|
||||||
|
import { validateGuildConfig } from "../../configValidator";
|
||||||
|
import { AllowedGuilds } from "../../data/AllowedGuilds";
|
||||||
|
import { ApiPermissionAssignments, ApiPermissionTypes } from "../../data/ApiPermissionAssignments";
|
||||||
|
import { Configs } from "../../data/Configs";
|
||||||
|
import { apiTokenAuthHandlers } from "../auth";
|
||||||
|
import { hasGuildPermission, requireGuildPermission } from "../permissions";
|
||||||
|
import { clientError, ok, serverError, unauthorized } from "../responses";
|
||||||
|
import { loadYamlSafely } from "../../utils/loadYamlSafely";
|
||||||
|
import { ObjectAliasError } from "../../utils/validateNoObjectAliases";
|
||||||
|
import { isSnowflake } from "../../utils";
|
||||||
|
import moment from "moment-timezone";
|
||||||
|
import { ApiAuditLog } from "../../data/ApiAuditLog";
|
||||||
|
import { AuditLogEventTypes } from "../../data/apiAuditLogTypes";
|
||||||
|
import { Queue } from "../../Queue";
|
||||||
|
import { GuildCases } from "../../data/GuildCases";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const apiPermissionAssignments = new ApiPermissionAssignments();
|
||||||
|
const auditLog = new ApiAuditLog();
|
||||||
|
|
||||||
|
export function initGuildsMiscAPI(router: express.Router) {
|
||||||
|
const allowedGuilds = new AllowedGuilds();
|
||||||
|
const configs = new Configs();
|
||||||
|
|
||||||
|
const miscRouter = express.Router();
|
||||||
|
|
||||||
|
miscRouter.get("/available", async (req: Request, res: Response) => {
|
||||||
|
const guilds = await allowedGuilds.getForApiUser(req.user!.userId);
|
||||||
|
res.json(guilds);
|
||||||
|
});
|
||||||
|
|
||||||
|
miscRouter.get(
|
||||||
|
"/my-permissions", // a
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const permissions = await apiPermissionAssignments.getByUserId(req.user!.userId);
|
||||||
|
res.json(permissions);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
miscRouter.get("/:guildId", async (req: Request, res: Response) => {
|
||||||
|
if (!(await hasGuildPermission(req.user!.userId, req.params.guildId, ApiPermissions.ViewGuild))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guild = await allowedGuilds.find(req.params.guildId);
|
||||||
|
res.json(guild);
|
||||||
|
});
|
||||||
|
|
||||||
|
miscRouter.post("/:guildId/check-permission", async (req: Request, res: Response) => {
|
||||||
|
const permission = req.body.permission;
|
||||||
|
const hasPermission = await hasGuildPermission(req.user!.userId, req.params.guildId, permission);
|
||||||
|
res.json({ result: hasPermission });
|
||||||
|
});
|
||||||
|
|
||||||
|
miscRouter.get(
|
||||||
|
"/:guildId/config",
|
||||||
|
requireGuildPermission(ApiPermissions.ReadConfig),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const config = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
||||||
|
res.json({ config: config ? config.config : "" });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
miscRouter.post("/:guildId/config", requireGuildPermission(ApiPermissions.EditConfig), async (req, res) => {
|
||||||
|
let config = req.body.config;
|
||||||
|
if (config == null) return clientError(res, "No config supplied");
|
||||||
|
|
||||||
|
config = config.trim() + "\n"; // Normalize start/end whitespace in the config
|
||||||
|
|
||||||
|
const currentConfig = await configs.getActiveByKey(`guild-${req.params.guildId}`);
|
||||||
|
if (currentConfig && config === currentConfig.config) {
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate config
|
||||||
|
let parsedConfig;
|
||||||
|
try {
|
||||||
|
parsedConfig = loadYamlSafely(config);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof YAMLException) {
|
||||||
|
return res.status(400).json({ errors: [e.message] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof ObjectAliasError) {
|
||||||
|
return res.status(400).json({ errors: [e.message] });
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line:no-console
|
||||||
|
console.error("Error when loading YAML: " + e.message);
|
||||||
|
return serverError(res, "Server error");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedConfig == null) {
|
||||||
|
parsedConfig = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = await validateGuildConfig(parsedConfig);
|
||||||
|
if (error) {
|
||||||
|
return res.status(422).json({ errors: [error] });
|
||||||
|
}
|
||||||
|
|
||||||
|
await configs.saveNewRevision(`guild-${req.params.guildId}`, config, req.user!.userId);
|
||||||
|
|
||||||
|
ok(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
miscRouter.get(
|
||||||
|
"/:guildId/permissions",
|
||||||
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const permissions = await apiPermissionAssignments.getByGuildId(req.params.guildId);
|
||||||
|
res.json(permissions);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const permissionManagementQueue = new Queue();
|
||||||
|
miscRouter.post(
|
||||||
|
"/:guildId/set-target-permissions",
|
||||||
|
requireGuildPermission(ApiPermissions.ManageAccess),
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
await permissionManagementQueue.add(async () => {
|
||||||
|
const { type, targetId, permissions, expiresAt } = req.body;
|
||||||
|
|
||||||
|
if (type !== ApiPermissionTypes.User) {
|
||||||
|
return clientError(res, "Invalid type");
|
||||||
|
}
|
||||||
|
if (!isSnowflake(targetId)) {
|
||||||
|
return clientError(res, "Invalid targetId");
|
||||||
|
}
|
||||||
|
const validPermissions = new Set(Object.values(ApiPermissions));
|
||||||
|
validPermissions.delete(ApiPermissions.Owner);
|
||||||
|
if (!Array.isArray(permissions) || permissions.some((p) => !validPermissions.has(p))) {
|
||||||
|
return clientError(res, "Invalid permissions");
|
||||||
|
}
|
||||||
|
if (expiresAt != null && !moment.utc(expiresAt).isValid()) {
|
||||||
|
return clientError(res, "Invalid expiresAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAssignment = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, targetId);
|
||||||
|
if (existingAssignment && existingAssignment.permissions.includes(ApiPermissions.Owner)) {
|
||||||
|
return clientError(res, "Can't change owner permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissions.length === 0) {
|
||||||
|
await apiPermissionAssignments.removeUser(req.params.guildId, targetId);
|
||||||
|
await auditLog.addEntry(req.params.guildId, req.user!.userId, AuditLogEventTypes.REMOVE_API_PERMISSION, {
|
||||||
|
type: ApiPermissionTypes.User,
|
||||||
|
target_id: targetId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const existing = await apiPermissionAssignments.getByGuildAndUserId(req.params.guildId, targetId);
|
||||||
|
if (existing) {
|
||||||
|
await apiPermissionAssignments.updateUserPermissions(req.params.guildId, targetId, permissions);
|
||||||
|
await auditLog.addEntry(req.params.guildId, req.user!.userId, AuditLogEventTypes.EDIT_API_PERMISSION, {
|
||||||
|
type: ApiPermissionTypes.User,
|
||||||
|
target_id: targetId,
|
||||||
|
permissions,
|
||||||
|
expires_at: existing.expires_at,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await apiPermissionAssignments.addUser(req.params.guildId, targetId, permissions, expiresAt);
|
||||||
|
await auditLog.addEntry(req.params.guildId, req.user!.userId, AuditLogEventTypes.ADD_API_PERMISSION, {
|
||||||
|
type: ApiPermissionTypes.User,
|
||||||
|
target_id: targetId,
|
||||||
|
permissions,
|
||||||
|
expires_at: expiresAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok(res);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
router.use("/", miscRouter);
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { TokenError } from "passport-oauth2";
|
||||||
import { initArchives } from "./archives";
|
import { initArchives } from "./archives";
|
||||||
import { initAuth } from "./auth";
|
import { initAuth } from "./auth";
|
||||||
import { initDocs } from "./docs";
|
import { initDocs } from "./docs";
|
||||||
import { initGuildsAPI } from "./guilds";
|
import { initGuildsAPI } from "./guilds/index";
|
||||||
import { clientError, error, notFound } from "./responses";
|
import { clientError, error, notFound } from "./responses";
|
||||||
import { startBackgroundTasks } from "./tasks";
|
import { startBackgroundTasks } from "./tasks";
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ import { CaseTypes } from "./CaseTypes";
|
||||||
import { connection } from "./db";
|
import { connection } from "./db";
|
||||||
import { Case } from "./entities/Case";
|
import { Case } from "./entities/Case";
|
||||||
import { CaseNote } from "./entities/CaseNote";
|
import { CaseNote } from "./entities/CaseNote";
|
||||||
import moment = require("moment-timezone");
|
import moment from "moment-timezone";
|
||||||
|
import { chunkArray } from "../utils";
|
||||||
import { Queue } from "../Queue";
|
import { Queue } from "../Queue";
|
||||||
|
|
||||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
||||||
|
@ -111,6 +112,26 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMinCaseNumber(): Promise<number> {
|
||||||
|
const result = await this.cases
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||||
|
.select(["MIN(case_number) AS min_case_number"])
|
||||||
|
.getRawOne<{ min_case_number: number }>();
|
||||||
|
|
||||||
|
return result?.min_case_number || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMaxCaseNumber(): Promise<number> {
|
||||||
|
const result = await this.cases
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||||
|
.select(["MAX(case_number) AS max_case_number"])
|
||||||
|
.getRawOne<{ max_case_number: number }>();
|
||||||
|
|
||||||
|
return result?.max_case_number || 0;
|
||||||
|
}
|
||||||
|
|
||||||
async setHidden(id: number, hidden: boolean): Promise<void> {
|
async setHidden(id: number, hidden: boolean): Promise<void> {
|
||||||
await this.cases.update(
|
await this.cases.update(
|
||||||
{ id },
|
{ id },
|
||||||
|
@ -197,4 +218,42 @@ export class GuildCases extends BaseGuildRepository {
|
||||||
case_id: caseId,
|
case_id: caseId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteAllCases(): Promise<void> {
|
||||||
|
const idRows = await this.cases
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||||
|
.select(["id"])
|
||||||
|
.getRawMany<{ id: number }>();
|
||||||
|
const ids = idRows.map((r) => r.id);
|
||||||
|
const batches = chunkArray(ids, 500);
|
||||||
|
for (const batch of batches) {
|
||||||
|
await this.cases.createQueryBuilder().where("id IN (:ids)", { ids: batch }).delete().execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async bumpCaseNumbers(amount: number): Promise<void> {
|
||||||
|
await this.cases
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where("guild_id = :guildId", { guildId: this.guildId })
|
||||||
|
.update()
|
||||||
|
.set({
|
||||||
|
case_number: () => `case_number + ${parseInt(amount as unknown as string, 10)}`,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
getExportCases(skip: number, take: number): Promise<Case[]> {
|
||||||
|
return this.cases.find({
|
||||||
|
where: {
|
||||||
|
guild_id: this.guildId,
|
||||||
|
},
|
||||||
|
relations: ["notes"],
|
||||||
|
order: {
|
||||||
|
case_number: "ASC",
|
||||||
|
},
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
159
dashboard/src/components/dashboard/GuildImportExport.vue
Normal file
159
dashboard/src/components/dashboard/GuildImportExport.vue
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Import / Export</h1>
|
||||||
|
<p>
|
||||||
|
<strong>Note!</strong>
|
||||||
|
This feature is currently experimental. Make sure to always export a backup before importing server data. If you encounter any issues, please report them on the [Zeppelin Discord server](https://discord.gg/zeppelin).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Export server data</h2>
|
||||||
|
<button class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :class="{ 'bg-gray-800': exportData, 'hover:bg-gray-800': !exportData }" @click="runExport()" :disabled="exportData">Create export file</button>
|
||||||
|
<button class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :class="{ 'bg-gray-800': !exportData, 'hover:bg-gray-800': exportData }" :disabled="!exportData" @click="downloadExportFile">Download export file</button>
|
||||||
|
|
||||||
|
<p v-if="exportError">Error: {{ exportError }}</p>
|
||||||
|
<p v-else-if="exporting">Creating export file...</p>
|
||||||
|
<p v-else> </p>
|
||||||
|
|
||||||
|
<h2>Import server data</h2>
|
||||||
|
<p>
|
||||||
|
<strong>Note!</strong>
|
||||||
|
Always take a backup of your existing data above before importing.
|
||||||
|
</p>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>Import file</h3>
|
||||||
|
<input type="file" @change="selectImportFile($event.target.files[0])">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>Case options</h3>
|
||||||
|
<label><input type="radio" v-model="importCaseMode" value="bumpImportedCases"> Leave existing case numbers, start imported cases from the end</label><br>
|
||||||
|
<label><input type="radio" v-model="importCaseMode" value="bumpExistingCases"> Leave imported case numbers, re-number existing cases to start after imported cases</label><br>
|
||||||
|
<label><input type="radio" v-model="importCaseMode" value="replace"> Replace existing cases (!! THIS WILL DELETE ALL EXISTING CASES !!)</label>
|
||||||
|
</div>
|
||||||
|
<button class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :class="{ 'bg-gray-800': importFile == null, 'hover:bg-gray-800': importFile != null }" @click="runImport()" :disabled="importFile == null">Import selected file</button>
|
||||||
|
|
||||||
|
<p v-if="importError">Error: {{ importError }}</p>
|
||||||
|
<p v-else-if="importing">Importing...</p>
|
||||||
|
<p v-else> </p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { mapState } from "vuex";
|
||||||
|
import { ApiPermissions, hasPermission } from "@shared/apiPermissions";
|
||||||
|
import { AuthState, GuildState } from "../../store/types";
|
||||||
|
import { ApiError } from "../../api";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch("guilds/loadGuild", this.$route.params.guildId);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
this.$router.push('/dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.guild == null) {
|
||||||
|
this.$router.push('/dashboard');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState("guilds", {
|
||||||
|
guild(guilds: GuildState) {
|
||||||
|
return guilds.available.get(this.$route.params.guildId);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
|
||||||
|
importing: false,
|
||||||
|
importError: null,
|
||||||
|
importFile: null,
|
||||||
|
importCaseMode: "bumpImportedCases",
|
||||||
|
|
||||||
|
exporting: false,
|
||||||
|
exportError: null,
|
||||||
|
exportData: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectImportFile(file: File) {
|
||||||
|
this.importFile = file;
|
||||||
|
},
|
||||||
|
async runImport() {
|
||||||
|
if (this.importing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! this.importFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importError = null;
|
||||||
|
this.importing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch("guilds/importData", {
|
||||||
|
guildId: this.$route.params.guildId,
|
||||||
|
data: JSON.parse(await (this.importFile as File).text()),
|
||||||
|
caseHandlingMode: this.importCaseMode,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.importError = String(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this.importing = false;
|
||||||
|
this.importFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.alert("Data imported successfully!");
|
||||||
|
},
|
||||||
|
async runExport() {
|
||||||
|
if (this.exporting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.exportData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.exportError = null;
|
||||||
|
this.exporting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.exportData = await this.$store.dispatch("guilds/exportData", {
|
||||||
|
guildId: this.$route.params.guildId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.exportError = String(err);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
this.exporting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
downloadExportFile() {
|
||||||
|
if (!this.exportData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dl = document.createElement("a");
|
||||||
|
dl.setAttribute("href", `data:application/json,${encodeURIComponent(JSON.stringify(this.exportData, null, 2))}`);
|
||||||
|
dl.setAttribute("download", `export_${this.$route.params.guildId}_${moment().format("YYYY-MM-DD_HH-mm-ss")}.json`);
|
||||||
|
dl.style.display = "none";
|
||||||
|
|
||||||
|
document.body.appendChild(dl);
|
||||||
|
dl.click();
|
||||||
|
document.body.removeChild(dl);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -19,6 +19,7 @@
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<router-link class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/config'">Config</router-link>
|
<router-link class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/config'">Config</router-link>
|
||||||
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/access'">Access</router-link>
|
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/access'">Access</router-link>
|
||||||
|
<router-link v-if="canManageAccess(guild.id)" class="inline-block bg-gray-700 rounded px-1 hover:bg-gray-800" :to="'/dashboard/guilds/' + guild.id + '/import-export'">Import/export</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -86,6 +86,10 @@ export const router = new VueRouter({
|
||||||
path: "guilds/:guildId/access",
|
path: "guilds/:guildId/access",
|
||||||
component: () => import("./components/dashboard/GuildAccess.vue"),
|
component: () => import("./components/dashboard/GuildAccess.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "guilds/:guildId/import-export",
|
||||||
|
component: () => import("./components/dashboard/GuildImportExport.vue"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -66,6 +66,17 @@ export const GuildStore: Module<GuildState, RootState> = {
|
||||||
await post(`guilds/${guildId}/set-target-permissions`, { guildId, targetId, type, permissions, expiresAt });
|
await post(`guilds/${guildId}/set-target-permissions`, { guildId, targetId, type, permissions, expiresAt });
|
||||||
commit("setTargetPermissions", { guildId, targetId, type, permissions, expiresAt });
|
commit("setTargetPermissions", { guildId, targetId, type, permissions, expiresAt });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async importData({ commit }, { guildId, data, caseHandlingMode }) {
|
||||||
|
return post(`guilds/${guildId}/import`, {
|
||||||
|
data,
|
||||||
|
caseHandlingMode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async exportData({ commit }, { guildId }) {
|
||||||
|
return post(`guilds/${guildId}/export`);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mutations: {
|
mutations: {
|
||||||
|
|
Loading…
Add table
Reference in a new issue