Unverified Commit b7d8000e authored by Lemon-miaow's avatar Lemon-miaow
Browse files

feat(panel): implement dedicated LuckPerms permissions and groups management sub-page

parent 5f402b19
Loading
Loading
Loading
Loading
+152 −1
Changes for panel/dev/mockApi.ts: 152 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -38,12 +38,18 @@ interface MockServer extends ServerInfo {
  owner: AccountID | null;
}

interface PlayerLuckPerms {
  groups: string[];
  permissions: { node: string; value: boolean; world?: string }[];
}

interface AccessState {
  whitelist: string[];
  banned: string[];
  // online is the mock's stand-in for the live RCON "list" roster. Kick and ban
  // splice a player out of it so the demo roster reflects the action on reload.
  online: string[];
  luckperms: Record<string, PlayerLuckPerms>;
}

interface MockState {
@@ -62,6 +68,8 @@ interface MockState {
// rejects a malformed player exactly as the real API would (400 bad_request),
// keeping the panel's error path exercisable in dev.
const PLAYER_NAME = /^[A-Za-z0-9_]{1,16}$/;
const LP_NODE = /^[A-Za-z0-9_.*-]{1,64}$/;
const LP_CTX = /^[A-Za-z0-9_-]{1,48}$/;

interface RequestContext {
  req: IncomingMessage;
@@ -240,6 +248,22 @@ function initialState(): MockState {
          "mock_player", "test_player", "Notch", "Steve", "Alex", "jeb_",
          "Dinnerbone", "Griefer_88", "rndGuest_7", "xX_Raider_Xx", "creeper_fan", "Herobrine",
        ],
        luckperms: {
          Steve: {
            groups: ["vip", "default"],
            permissions: [
              { node: "essentials.fly", value: true },
              { node: "essentials.tpa", value: true },
              { node: "minecraft.command.gamemode", value: false },
            ],
          },
          Alex: {
            groups: ["default"],
            permissions: [
              { node: "essentials.tpa", value: true },
            ],
          },
        },
      },
    },
    backups: mockBackups(),
@@ -1277,7 +1301,7 @@ async function handleRestoreBackupMock(ctx: SessionContext, serverInfo: MockServ
function accessFor(state: MockState, name: string): AccessState {
  let entry = state.access[name];
  if (!entry) {
    entry = { whitelist: [], banned: [], online: [] };
    entry = { whitelist: [], banned: [], online: [], luckperms: {} };
    state.access[name] = entry;
  }
  return entry;
@@ -1326,6 +1350,21 @@ function handleAccessMock(ctx: SessionContext, serverInfo: MockServer): boolean
  const sub = ctx.parts[5];
  const access = accessFor(ctx.state, serverInfo.name);

  if (is("GET", ctx) && sub === "luckperms") {
    const player = ctx.parts[6];
    if (!player || !PLAYER_NAME.test(player)) {
      sendError(ctx.res, 400, "bad_request", "invalid player name");
      return true;
    }
    const lpData = access.luckperms[player] || { groups: ["default"], permissions: [] };
    sendJSON(ctx.res, 200, {
      player,
      groups: lpData.groups,
      permissions: lpData.permissions,
    });
    return true;
  }

  if (is("GET", ctx) && sub === "whitelist") {
    sendJSON(ctx.res, 200, {
      name: serverInfo.name,
@@ -1365,6 +1404,14 @@ function handleAccessMock(ctx: SessionContext, serverInfo: MockServer): boolean
    void handleListMutation(ctx, serverInfo, access, "ban");
    return true;
  }
  if (is("POST", ctx) && sub === "group") {
    void handleGroupMock(ctx, serverInfo);
    return true;
  }
  if (is("POST", ctx) && sub === "permission") {
    void handlePermissionMock(ctx, serverInfo);
    return true;
  }

  return false;
}
@@ -1391,6 +1438,110 @@ async function handleKickMock(
  });
}

async function handleGroupMock(
  ctx: SessionContext,
  serverInfo: MockServer,
): Promise<void> {
  const body = await readJSON<{ action?: string; player?: string; group?: string }>(ctx.req);
  const player = body.player?.trim() ?? "";
  const group = body.group?.trim() ?? "";
  const action = body.action?.trim() ?? "";

  if (!PLAYER_NAME.test(player)) {
    sendError(ctx.res, 400, "bad_request", "invalid player name");
    return;
  }
  if (!LP_CTX.test(group)) {
    sendError(ctx.res, 400, "bad_request", "invalid group name");
    return;
  }
  if (action !== "add" && action !== "remove") {
    sendError(ctx.res, 400, "bad_request", "invalid action");
    return;
  }

  const access = accessFor(ctx.state, serverInfo.name);
  if (!access.luckperms[player]) {
    access.luckperms[player] = { groups: ["default"], permissions: [] };
  }
  const lp = access.luckperms[player];
  if (action === "add") {
    if (!lp.groups.includes(group)) lp.groups.push(group);
  } else if (action === "remove") {
    lp.groups = lp.groups.filter((g) => g !== group);
  }

  sendJSON(ctx.res, 200, {
    name: serverInfo.name,
    action,
    player,
    group,
    output: `[mock-luckperms] Added parent group '${group}' for user ${player} in context global`,
  });
}

async function handlePermissionMock(
  ctx: SessionContext,
  serverInfo: MockServer,
): Promise<void> {
  const body = await readJSON<{
    action?: string;
    player?: string;
    node?: string;
    value?: boolean;
    world?: string;
  }>(ctx.req);
  const player = body.player?.trim() ?? "";
  const node = body.node?.trim() ?? "";
  const action = body.action?.trim() ?? "";
  const value = body.value ?? true;
  const world = body.world?.trim() ?? "";

  if (!PLAYER_NAME.test(player)) {
    sendError(ctx.res, 400, "bad_request", "invalid player name");
    return;
  }
  if (!LP_NODE.test(node)) {
    sendError(ctx.res, 400, "bad_request", "invalid permission node");
    return;
  }
  if (world !== "" && !LP_CTX.test(world)) {
    sendError(ctx.res, 400, "bad_request", "invalid world context");
    return;
  }
  if (action !== "set" && action !== "unset") {
    sendError(ctx.res, 400, "bad_request", "invalid action");
    return;
  }

  const access = accessFor(ctx.state, serverInfo.name);
  if (!access.luckperms[player]) {
    access.luckperms[player] = { groups: ["default"], permissions: [] };
  }
  const lp = access.luckperms[player];
  if (action === "set") {
    lp.permissions = lp.permissions.filter((p) => !(p.node === node && p.world === world));
    lp.permissions.push({ node, value, world: world || undefined });
  } else if (action === "unset") {
    lp.permissions = lp.permissions.filter((p) => !(p.node === node && p.world === world));
  }

  const worldSuffix = world ? ` (world=${world})` : "";
  const outputMsg = action === "set"
    ? `[mock-luckperms] Set permission ${node} to ${value} for ${player}${worldSuffix}`
    : `[mock-luckperms] Unset permission ${node} for ${player}${worldSuffix}`;

  sendJSON(ctx.res, 200, {
    name: serverInfo.name,
    action,
    player,
    node,
    value,
    world: world || undefined,
    output: outputMsg,
  });
}

// handleListMutation backs both POST .../access/whitelist (add|remove) and
// POST .../access/ban (ban|pardon): the same structured {action, player} shape with
// a charset-validated player, echoing back {name, action, player, output}.
+2 −0
Changes for panel/src/App.tsx: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ import { ServersPage } from "@/pages/servers/ServersPage";
import { ServerConsole } from "@/pages/ServerConsole";
import { ServerPlayers } from "@/pages/ServerPlayers";
import { ServerBackups } from "@/pages/ServerBackups";
import { ServerLuckPerms } from "@/pages/ServerLuckPerms";
import { Account } from "@/pages/Account";
import { ImageAdmin } from "@/pages/admin/ImageAdmin";
import { ImageBuildPage } from "@/pages/admin/ImageBuildPage";
@@ -47,6 +48,7 @@ export default function App() {
              <Route path="servers/:name" element={<ServerConsole />} />
              <Route path="servers/:name/players" element={<ServerPlayers />} />
              <Route path="servers/:name/backups" element={<ServerBackups />} />
              <Route path="servers/:name/luckperms" element={<ServerLuckPerms />} />
              <Route path="submissions" element={<MySubmissionsPage />} />
              <Route path="account" element={<Account />} />

+52 −1
Changes for panel/src/i18n/resources/en-US/servers.json: 52 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -47,6 +47,8 @@
  "players_title": "Player management",
  "players_link_title": "Player management",
  "players_link_desc": "Manage the whitelist, online players, and bans.",
  "luckperms_link_title": "LuckPerms Permissions",
  "luckperms_link_desc": "Manage player groups and fine-grained permission nodes.",
  "backups_link_title": "Backups & restore",
  "backups_link_desc": "View and restore world backups for this server.",
  "players_back_to_console": "Back to console",
@@ -135,5 +137,54 @@
  "edit_server_unchanged": "Leave unchanged",
  "edit_server_immutable": "Unchanged (Immutable)",
  "edit_server_submit": "Save Config",
  "edit_server_updating": "Saving…"
  "edit_server_updating": "Saving…",
  "luckperms_dialog_title": "LuckPerms Permissions",
  "luckperms_dialog_desc": "Set or unset LuckPerms permission nodes and parent groups on the running server (requires the LuckPerms plugin to be active).",
  "luckperms_tab_group": "Groups",
  "luckperms_tab_permission": "Permissions",
  "luckperms_player_name": "Player Username",
  "luckperms_group_name": "Group Name",
  "luckperms_node": "Permission Node",
  "luckperms_action": "Action",
  "luckperms_action_add": "Add to Group (add)",
  "luckperms_action_remove": "Remove from Group (remove)",
  "luckperms_action_set": "Set Permission (set)",
  "luckperms_action_unset": "Unset Permission (unset)",
  "luckperms_value": "Value",
  "luckperms_value_grant": "Grant (True)",
  "luckperms_value_deny": "Deny (False)",
  "luckperms_world": "World Context (Optional)",
  "luckperms_confirm": "Apply Changes",
  "luckperms_executing": "Executing command…",
  "luckperms_success": "LuckPerms command executed successfully:",
  "luckperms_error_invalid_player": "Invalid player name (1–16 chars: letters, digits, underscore)",
  "luckperms_error_invalid_node": "Invalid permission node (allowed: letters, digits, ., -, _, *, 1-64 chars)",
  "luckperms_error_invalid_group": "Invalid group name (allowed: letters, digits, -, _, 1-48 chars)",
  "luckperms_error_invalid_world": "Invalid world context (allowed: letters, digits, -, _, 1-48 chars)",
  "luckperms_title": "LuckPerms Permissions",
  "luckperms_desc": "Manage player permission nodes and parent groups on this server (requires the LuckPerms plugin to be active).",
  "luckperms_back_to_console": "Back to console",
  "luckperms_select_player_prompt": "Please select an online player from the list, or enter a username to query.",
  "luckperms_add_group_btn": "Add Parent Group",
  "luckperms_add_perm_btn": "Add Permission",
  "luckperms_groups_title": "Parent Groups",
  "luckperms_permissions_title": "Permission Nodes",
  "luckperms_node_column": "Permission Node",
  "luckperms_value_column": "Value",
  "luckperms_world_column": "World",
  "luckperms_actions_column": "Actions",
  "luckperms_query_btn": "Query Player",
  "luckperms_custom_group_placeholder": "Enter custom group name...",
  "luckperms_batch_players": "Player Usernames List",
  "luckperms_batch_players_placeholder": "Enter player names, support multiple (separated by commas or spaces)",
  "luckperms_recent_actions": "Recent Actions History",
  "luckperms_no_recent_actions": "No recent actions.",
  "luckperms_revert": "Revert",
  "luckperms_reverting": "Reverting...",
  "luckperms_revert_success": "Action reverted successfully!",
  "luckperms_quick_presets": "Common Presets",
  "luckperms_presets": "Presets",
  "luckperms_batch_status": "Batch Progress",
  "luckperms_success_count": "Success: {{count}}",
  "luckperms_failed_count": "Failed: {{count}}"
}
+52 −1
Changes for panel/src/i18n/resources/zh-CN/servers.json: 52 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -47,6 +47,8 @@
  "players_title": "玩家管理",
  "players_link_title": "玩家管理",
  "players_link_desc": "管理白名单、在线玩家与封禁。",
  "luckperms_link_title": "LuckPerms 权限",
  "luckperms_link_desc": "管理玩家用户组与细粒度权限节点。",
  "backups_link_title": "备份与恢复",
  "backups_link_desc": "查看并恢复该服务器的世界备份。",
  "players_back_to_console": "返回控制台",
@@ -135,5 +137,54 @@
  "edit_server_unchanged": "保持不变",
  "edit_server_immutable": "保持不变 (不可修改)",
  "edit_server_submit": "保存配置",
  "edit_server_updating": "正在保存…"
  "edit_server_updating": "正在保存…",
  "luckperms_dialog_title": "LuckPerms 权限管理",
  "luckperms_dialog_desc": "在运行中的服务器上设置或取消玩家的权限节点与用户组(要求 LuckPerms 插件处于运行状态)。",
  "luckperms_tab_group": "用户组管理",
  "luckperms_tab_permission": "细粒度权限",
  "luckperms_player_name": "玩家用户名",
  "luckperms_group_name": "用户组名称",
  "luckperms_node": "权限节点",
  "luckperms_action": "操作类型",
  "luckperms_action_add": "添加至用户组 (add)",
  "luckperms_action_remove": "从用户组移除 (remove)",
  "luckperms_action_set": "设置权限节点 (set)",
  "luckperms_action_unset": "取消权限节点 (unset)",
  "luckperms_value": "权限值 (Value)",
  "luckperms_value_grant": "允许 (True)",
  "luckperms_value_deny": "拒绝 (False)",
  "luckperms_world": "世界范围 context (可选)",
  "luckperms_confirm": "执行变更",
  "luckperms_executing": "正在执行命令…",
  "luckperms_success": "LuckPerms 命令成功执行:",
  "luckperms_error_invalid_player": "玩家用户名格式错误 (支持1-16位英文字母、数字和下划线)",
  "luckperms_error_invalid_node": "权限节点格式错误 (支持1-64位字母、数字、点号、横线、下划线及通配符*)",
  "luckperms_error_invalid_group": "用户组名称格式错误 (支持1-48位字母、数字、横线和下划线)",
  "luckperms_error_invalid_world": "世界范围格式错误 (支持1-48位字母、数字、横线和下划线)",
  "luckperms_title": "LuckPerms 权限管理",
  "luckperms_desc": "精细化管理服务器上玩家的权限节点与用户组(需要 LuckPerms 插件处于运行状态)。",
  "luckperms_back_to_console": "返回控制台",
  "luckperms_select_player_prompt": "请在左侧选择在线玩家,或在上方输入玩家名进行查询",
  "luckperms_add_group_btn": "添加用户组",
  "luckperms_add_perm_btn": "添加权限",
  "luckperms_groups_title": "所属用户组",
  "luckperms_permissions_title": "拥有权限节点",
  "luckperms_node_column": "权限节点",
  "luckperms_value_column": "状态值",
  "luckperms_world_column": "生效世界",
  "luckperms_actions_column": "操作",
  "luckperms_query_btn": "查询玩家",
  "luckperms_custom_group_placeholder": "输入自定义组名...",
  "luckperms_batch_players": "玩家用户名列表",
  "luckperms_batch_players_placeholder": "输入玩家用户名,支持输入多个(用英文逗号或空格分隔)",
  "luckperms_recent_actions": "最近操作历史",
  "luckperms_no_recent_actions": "暂无最近操作历史。",
  "luckperms_revert": "撤销",
  "luckperms_reverting": "正在撤销...",
  "luckperms_revert_success": "已成功撤销该操作!",
  "luckperms_quick_presets": "常用快速预设",
  "luckperms_presets": "预设",
  "luckperms_batch_status": "批量执行进度",
  "luckperms_success_count": "成功: {{count}}",
  "luckperms_failed_count": "失败: {{count}}"
}
+33 −0
Changes for panel/src/lib/api.ts: 33 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -188,6 +188,39 @@ export const api = {
  accessKick: (name: string, player: string) =>
    request<KickResult>("POST", `/servers/${name}/access/kick`, { player }),

  accessLuckPermsInfo: (name: string, player: string) =>
    request<{
      player: string;
      groups: string[];
      permissions: { node: string; value: boolean; world?: string }[];
    }>("GET", `/servers/${name}/access/luckperms/${player}`),

  accessPermission: (
    name: string,
    action: "set" | "unset",
    player: string,
    node: string,
    value?: boolean,
    world?: string
  ) =>
    request<AccessResult & { node: string; value?: boolean; world?: string }>(
      "POST",
      `/servers/${name}/access/permission`,
      { action, player, node, value, world }
    ),

  accessGroup: (
    name: string,
    action: "add" | "remove",
    player: string,
    group: string
  ) =>
    request<AccessResult & { group: string }>(
      "POST",
      `/servers/${name}/access/group`,
      { action, player, group }
    ),

  listImages: () =>
    request<{ images: WhitelistImage[] }>("GET", "/images").then((r) => r.images ?? []),

Loading