-
+ {canCreate && }
}>
diff --git a/apps/docs/src/locales/@vitnode/core/en.json b/apps/docs/src/locales/@vitnode/core/en.json
index 528a31ba2..8aabdd5b0 100644
--- a/apps/docs/src/locales/@vitnode/core/en.json
+++ b/apps/docs/src/locales/@vitnode/core/en.json
@@ -8,7 +8,12 @@
"@vitnode/core:users:can_edit": "Edit users",
"@vitnode/core:users:can_edit_admin": "Edit users with administrator permission",
"@vitnode/core:roles": "Roles",
- "@vitnode/core:roles:can_manage": "Manage roles",
+ "@vitnode/core:roles:can_view": "View roles list",
+ "@vitnode/core:roles:can_create": "Create roles",
+ "@vitnode/core:roles:can_edit": "Edit roles",
+ "@vitnode/core:roles:can_edit_admin": "Edit roles with administrator permission",
+ "@vitnode/core:roles:can_delete": "Delete roles",
+ "@vitnode/core:roles:can_delete_admin": "Delete roles with administrator permission",
"@vitnode/core:debug": "Debug Panel",
"@vitnode/core:debug:can_view": "View debug panel",
"@vitnode/core:debug:can_clear_cache": "Clear cache",
@@ -22,6 +27,9 @@
"@vitnode/core:files:can_delete": "Delete files",
"@vitnode/core:queue": "Queue Tasks",
"@vitnode/core:queue:can_view": "View queue tasks",
+ "@vitnode/core:cron": "Cron Jobs",
+ "@vitnode/core:cron:can_view": "View cron jobs",
+ "@vitnode/core:cron:can_run": "Run cron jobs",
"@vitnode/core:staff_moderators": "Staff: Moderators",
"@vitnode/core:staff_moderators:can_view": "View moderators list",
"@vitnode/core:staff_moderators:can_create": "Create moderators",
@@ -568,6 +576,18 @@
"submit": "Save changes",
"success": "Role updated"
},
+ "delete": {
+ "title": "Delete role",
+ "desc": "Are you sure you want to delete the role \"{name}\"? This action cannot be undone.",
+ "descWithUsers": "The role \"{name}\" is assigned to {count, plural, one {# user} other {# users}}. Choose another role to move them into before deleting. This action cannot be undone.",
+ "moveToLabel": "Move users to",
+ "selectRole": "Select a role...",
+ "confirm": "Delete role",
+ "cancel": "Cancel",
+ "success": "Role deleted",
+ "successDesc": "The role has been permanently deleted.",
+ "successDescMoved": "{count, plural, one {# user was} other {# users were}} moved to \"{name}\" and the role was permanently deleted."
+ },
"tabs": {
"general": "General",
"content": "Content"
diff --git a/apps/docs/src/locales/@vitnode/core/pl.json b/apps/docs/src/locales/@vitnode/core/pl.json
index 2087987d6..c9fe6d7b9 100644
--- a/apps/docs/src/locales/@vitnode/core/pl.json
+++ b/apps/docs/src/locales/@vitnode/core/pl.json
@@ -8,7 +8,12 @@
"@vitnode/core:users:can_edit": "Edytowanie użytkowników",
"@vitnode/core:users:can_edit_admin": "Edytowanie użytkowników z uprawnieniami administratora",
"@vitnode/core:roles": "Role",
- "@vitnode/core:roles:can_manage": "Zarządzanie rolami",
+ "@vitnode/core:roles:can_view": "Wyświetlanie listy ról",
+ "@vitnode/core:roles:can_create": "Tworzenie ról",
+ "@vitnode/core:roles:can_edit": "Edytowanie ról",
+ "@vitnode/core:roles:can_edit_admin": "Edytowanie ról z uprawnieniami administratora",
+ "@vitnode/core:roles:can_delete": "Usuwanie ról",
+ "@vitnode/core:roles:can_delete_admin": "Usuwanie ról z uprawnieniami administratora",
"@vitnode/core:debug": "Panel debugowania",
"@vitnode/core:debug:can_view": "Wyświetlanie panelu debugowania",
"@vitnode/core:debug:can_clear_cache": "Czyszczenie pamięci podręcznej",
@@ -22,6 +27,9 @@
"@vitnode/core:files:can_delete": "Usuwanie plików",
"@vitnode/core:queue": "Zadania w kolejce",
"@vitnode/core:queue:can_view": "Wyświetlanie zadań w kolejce",
+ "@vitnode/core:cron": "Zadania Cron",
+ "@vitnode/core:cron:can_view": "Wyświetlanie zadań cron",
+ "@vitnode/core:cron:can_run": "Uruchamianie zadań cron",
"@vitnode/core:staff_moderators": "Zespół: Moderatorzy",
"@vitnode/core:staff_moderators:can_view": "Wyświetlanie listy moderatorów",
"@vitnode/core:staff_moderators:can_create": "Tworzenie moderatorów",
diff --git a/packages/vitnode/src/api/models/events.ts b/packages/vitnode/src/api/models/events.ts
index 39eb12873..4b5426581 100644
--- a/packages/vitnode/src/api/models/events.ts
+++ b/packages/vitnode/src/api/models/events.ts
@@ -23,10 +23,6 @@ export interface VitNodeEvents {
"role.created": {
roleId: number;
};
- /**
- * Declared for plugins implementing role deletion - core has no role
- * deletion flow yet and never emits this itself.
- */
"role.deleted": {
roleId: number;
};
diff --git a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts
index b82c2c4da..b49a25c9c 100644
--- a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts
+++ b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/get.route.ts
@@ -11,6 +11,7 @@ import { core_cron } from "@/database/cron";
export const getCronsRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
+ adminStaffPermission: { module: "cron", permission: "can_view" },
route: {
method: "get",
description: "Get Admin Cron Logs",
diff --git a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts
index 93b03c529..0433bb834 100644
--- a/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts
+++ b/packages/vitnode/src/api/modules/admin/advanced/cron/routes/run.route.ts
@@ -10,6 +10,7 @@ import { getNextCronRunDate } from "@/lib/api/get-next-cron-run-date";
export const runCronRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
+ adminStaffPermission: { module: "cron", permission: "can_run" },
route: {
method: "post",
description: "Run a specific cron job",
diff --git a/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts b/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts
new file mode 100644
index 000000000..f0e2793dd
--- /dev/null
+++ b/packages/vitnode/src/api/modules/admin/roles/lib/assert-manage-admin-role.ts
@@ -0,0 +1,43 @@
+import type { Context } from "hono";
+
+import { eq } from "drizzle-orm";
+
+import { assertStaffPermission } from "@/api/lib/check-staff-permission";
+import { CONFIG_PLUGIN } from "@/config";
+import { core_admin_permissions } from "@/database/admins";
+
+/**
+ * A role grants admin access when it has a row in `core_admin_permissions`.
+ * Editing or deleting such a role is a higher-privilege action, so it requires
+ * the elevated `_admin` variant of the permission on top of the base route
+ * guard - mirroring how editing an admin *user* requires `users:can_edit_admin`
+ * (see `assertCanAssignPrimaryRole`).
+ *
+ * Roles that do not grant admin access pass through untouched.
+ */
+export const assertCanManageAdminRole = async (
+ c: Context,
+ {
+ roleId,
+ permission,
+ }: {
+ permission: "can_delete_admin" | "can_edit_admin";
+ roleId: number;
+ },
+): Promise
=> {
+ const [adminRole] = await c
+ .get("db")
+ .select({ id: core_admin_permissions.id })
+ .from(core_admin_permissions)
+ .where(eq(core_admin_permissions.roleId, roleId))
+ .limit(1);
+
+ if (!adminRole) return;
+
+ await assertStaffPermission(c, {
+ type: "admin",
+ plugin: CONFIG_PLUGIN.pluginId,
+ module: "roles",
+ permission,
+ });
+};
diff --git a/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts b/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts
index 9b794a9b9..9f6062812 100644
--- a/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts
+++ b/packages/vitnode/src/api/modules/admin/roles/roles.admin.module.ts
@@ -2,6 +2,7 @@ import { buildModule } from "@/api/lib/module";
import { CONFIG_PLUGIN } from "@/config";
import { createRoleAdminRoute } from "./routes/create.route";
+import { deleteRoleAdminRoute } from "./routes/delete.route";
import { listRolesAdminRoute } from "./routes/list.route";
import { showRoleAdminRoute } from "./routes/show.route";
import { updateRoleAdminRoute } from "./routes/update.route";
@@ -14,5 +15,6 @@ export const rolesAdminModule = buildModule({
showRoleAdminRoute,
createRoleAdminRoute,
updateRoleAdminRoute,
+ deleteRoleAdminRoute,
],
});
diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts
index 8cc1395e6..0e8f736f2 100644
--- a/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts
+++ b/packages/vitnode/src/api/modules/admin/roles/routes/create.route.ts
@@ -29,6 +29,7 @@ export const zodCreateRoleAdminSchema = z.object({
export const createRoleAdminRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
+ adminStaffPermission: { module: "roles", permission: "can_create" },
route: {
method: "post",
description: "Create a new role (Admin only)",
diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts
new file mode 100644
index 000000000..e2e57104a
--- /dev/null
+++ b/packages/vitnode/src/api/modules/admin/roles/routes/delete.route.ts
@@ -0,0 +1,178 @@
+import { z } from "@hono/zod-openapi";
+import { and, count, eq, inArray } from "drizzle-orm";
+
+import { buildRoute } from "@/api/lib/route";
+import { assertCanAssignPrimaryRole } from "@/api/modules/admin/users/lib/assert-edit-user-permission";
+import { CONFIG_PLUGIN } from "@/config";
+import { core_languages_words } from "@/database/languages";
+import { core_roles } from "@/database/roles";
+import { core_users, core_users_secondary_roles } from "@/database/users";
+
+import { assertCanManageAdminRole } from "../lib/assert-manage-admin-role";
+
+export const deleteRoleAdminRoute = buildRoute({
+ pluginId: CONFIG_PLUGIN.pluginId,
+ adminStaffPermission: { module: "roles", permission: "can_delete" },
+ route: {
+ method: "delete",
+ description: "Delete a role by id (Admin only)",
+ path: "/{id}",
+ request: {
+ params: z.object({
+ id: z.string().openapi({ example: "1" }),
+ }),
+ query: z.object({
+ // When the role still has members, they must be reassigned to this
+ // role before it can be removed (see the handler for why).
+ moveToRoleId: z.string().optional(),
+ }),
+ },
+ responses: {
+ 200: {
+ description: "Role deleted",
+ },
+ 400: {
+ content: {
+ "application/json": {
+ schema: z.object({ error: z.string() }),
+ },
+ },
+ description: "Role still has members or an invalid target was given",
+ },
+ 403: {
+ content: {
+ "application/json": {
+ schema: z.object({ error: z.string() }),
+ },
+ },
+ description: "Role cannot be deleted",
+ },
+ 404: {
+ content: {
+ "application/json": {
+ schema: z.object({ error: z.string() }),
+ },
+ },
+ description: "Role not found",
+ },
+ },
+ },
+ handler: async c => {
+ const { id } = c.req.valid("param");
+ const { moveToRoleId } = c.req.valid("query");
+ const db = c.get("db");
+
+ const roleId = Number(id);
+ if (!Number.isInteger(roleId)) {
+ return c.json({ error: "Role not found" }, 404);
+ }
+
+ const [role] = await db
+ .select({
+ id: core_roles.id,
+ protected: core_roles.protected,
+ default: core_roles.default,
+ root: core_roles.root,
+ guest: core_roles.guest,
+ })
+ .from(core_roles)
+ .where(eq(core_roles.id, roleId))
+ .limit(1);
+
+ if (!role) {
+ return c.json({ error: "Role not found" }, 404);
+ }
+
+ // System roles are managed by the platform (root/guest access, the default
+ // role for new sign-ups) and must always exist.
+ if (role.protected || role.default || role.root || role.guest) {
+ return c.json({ error: "This role cannot be deleted" }, 403);
+ }
+
+ // Deleting a role that grants admin access requires the elevated permission.
+ await assertCanManageAdminRole(c, {
+ roleId,
+ permission: "can_delete_admin",
+ });
+
+ // Members point at their primary role through a NOT NULL, RESTRICT foreign
+ // key, so anyone in this role has to be moved elsewhere first. Secondary
+ // roles and staff permission entries reference the role with ON DELETE
+ // CASCADE, so those clean themselves up.
+ const [{ total: usersCount }] = await db
+ .select({ total: count() })
+ .from(core_users)
+ .where(eq(core_users.roleId, roleId));
+
+ let targetRoleId: null | number = null;
+ if (usersCount > 0) {
+ if (moveToRoleId === undefined) {
+ return c.json({ error: "Role has assigned users" }, 400);
+ }
+
+ targetRoleId = Number(moveToRoleId);
+ if (!Number.isInteger(targetRoleId) || targetRoleId === roleId) {
+ return c.json({ error: "Invalid target role" }, 400);
+ }
+
+ const [target] = await db
+ .select({ id: core_roles.id, guest: core_roles.guest })
+ .from(core_roles)
+ .where(eq(core_roles.id, targetRoleId))
+ .limit(1);
+
+ if (!target || target.guest) {
+ return c.json({ error: "Invalid target role" }, 400);
+ }
+
+ await assertCanAssignPrimaryRole(c, targetRoleId);
+ }
+
+ await db.transaction(async tx => {
+ if (targetRoleId != null) {
+ const members = await tx
+ .select({ id: core_users.id })
+ .from(core_users)
+ .where(eq(core_users.roleId, roleId));
+ const memberIds = members.map(member => member.id);
+
+ await tx
+ .update(core_users)
+ .set({ roleId: targetRoleId })
+ .where(eq(core_users.roleId, roleId));
+
+ // A role cannot sit in both the primary and secondary slot for the same
+ // user, so remove the now-duplicate secondary entries.
+ if (memberIds.length > 0) {
+ await tx
+ .delete(core_users_secondary_roles)
+ .where(
+ and(
+ eq(core_users_secondary_roles.roleId, targetRoleId),
+ inArray(core_users_secondary_roles.userId, memberIds),
+ ),
+ );
+ }
+ }
+
+ // Role names live in `core_languages_words`, so remove them here to avoid
+ // leaving orphaned translations behind.
+ await tx
+ .delete(core_languages_words)
+ .where(
+ and(
+ eq(core_languages_words.pluginCode, "core"),
+ eq(core_languages_words.tableName, "core_roles"),
+ eq(core_languages_words.variable, "name"),
+ eq(core_languages_words.itemId, roleId),
+ ),
+ );
+
+ await tx.delete(core_roles).where(eq(core_roles.id, roleId));
+ });
+
+ await c.get("events").emit("role.deleted", { roleId });
+
+ return c.body(null, 200);
+ },
+});
diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts
index 04ca95361..efe9f5138 100644
--- a/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts
+++ b/packages/vitnode/src/api/modules/admin/roles/routes/list.route.ts
@@ -8,6 +8,7 @@ import {
zodPaginationQuery,
} from "@/api/lib/with-pagination";
import { CONFIG_PLUGIN } from "@/config";
+import { core_admin_permissions } from "@/database/admins";
import { core_languages_words } from "@/database/languages";
import { core_roles } from "@/database/roles";
import { core_users } from "@/database/users";
@@ -35,6 +36,10 @@ const rolesAdminListSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
usersCount: z.number(),
+ // A role grants admin access when it has a row in
+ // `core_admin_permissions`. Editing/deleting such roles needs the
+ // elevated `can_edit_admin` / `can_delete_admin` permission.
+ grantsAdmin: z.boolean(),
}),
),
pageInfo: zodPaginationPageInfo,
@@ -155,6 +160,15 @@ export const listRolesAdminRoute = buildRoute({
.where(inArray(core_users.roleId, roleIds))
.groupBy(core_users.roleId)
: [];
+ // Roles with a `core_admin_permissions` row grant admin access.
+ const adminRoles = roleIds.length
+ ? await c
+ .get("db")
+ .selectDistinct({ roleId: core_admin_permissions.roleId })
+ .from(core_admin_permissions)
+ .where(inArray(core_admin_permissions.roleId, roleIds))
+ : [];
+ const adminRoleIds = new Set(adminRoles.map(row => row.roleId));
return c.json({
pageInfo: data.pageInfo,
@@ -165,6 +179,7 @@ export const listRolesAdminRoute = buildRoute({
.map(word => ({ name: word.value, languageCode: word.languageCode })),
usersCount:
userCounts.find(item => item.roleId === role.id)?.total ?? 0,
+ grantsAdmin: adminRoleIds.has(role.id),
})),
});
},
diff --git a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
index fdd97f745..23955074b 100644
--- a/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
+++ b/packages/vitnode/src/api/modules/admin/roles/routes/update.route.ts
@@ -6,6 +6,7 @@ import { saveLanguageWords } from "@/api/lib/save-language-words";
import { CONFIG_PLUGIN } from "@/config";
import { core_roles } from "@/database/roles";
+import { assertCanManageAdminRole } from "../lib/assert-manage-admin-role";
import { zodRoleNameSchema, zodRoleStorageSchema } from "./create.route";
export const zodUpdateRoleAdminSchema = z
@@ -23,6 +24,7 @@ export const zodUpdateRoleAdminSchema = z
export const updateRoleAdminRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
+ adminStaffPermission: { module: "roles", permission: "can_edit" },
route: {
method: "patch",
description: "Update a role by id (Admin only)",
@@ -82,6 +84,9 @@ export const updateRoleAdminRoute = buildRoute({
return c.json({ error: "Role not found" }, 404);
}
+ // Editing a role that grants admin access requires the elevated permission.
+ await assertCanManageAdminRole(c, { roleId, permission: "can_edit_admin" });
+
const values: Partial = {
updatedAt: new Date(),
};
diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts
index 8fb405b25..6da77f3f7 100644
--- a/packages/vitnode/src/api/plugin.ts
+++ b/packages/vitnode/src/api/plugin.ts
@@ -29,7 +29,14 @@ export const newBuildPluginApiCore = buildApiPlugin({
{ permission: "can_edit", dependsOn: ["can_view"] },
{ permission: "can_edit_admin", dependsOn: ["can_view"] },
],
- roles: ["can_manage"],
+ roles: [
+ "can_view",
+ { permission: "can_create", dependsOn: ["can_view"] },
+ { permission: "can_edit", dependsOn: ["can_view"] },
+ { permission: "can_edit_admin", dependsOn: ["can_edit"] },
+ { permission: "can_delete", dependsOn: ["can_view"] },
+ { permission: "can_delete_admin", dependsOn: ["can_delete"] },
+ ],
debug: [
"can_view",
{ permission: "can_clear_cache", dependsOn: ["can_view"] },
@@ -45,6 +52,7 @@ export const newBuildPluginApiCore = buildApiPlugin({
{ permission: "can_delete", dependsOn: ["can_view"] },
],
queue: ["can_view"],
+ cron: ["can_view", { permission: "can_run", dependsOn: ["can_view"] }],
staff_moderators: [
"can_view",
{ permission: "can_create", dependsOn: ["can_view"] },
diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json
index 528a31ba2..8aabdd5b0 100644
--- a/packages/vitnode/src/locales/en.json
+++ b/packages/vitnode/src/locales/en.json
@@ -8,7 +8,12 @@
"@vitnode/core:users:can_edit": "Edit users",
"@vitnode/core:users:can_edit_admin": "Edit users with administrator permission",
"@vitnode/core:roles": "Roles",
- "@vitnode/core:roles:can_manage": "Manage roles",
+ "@vitnode/core:roles:can_view": "View roles list",
+ "@vitnode/core:roles:can_create": "Create roles",
+ "@vitnode/core:roles:can_edit": "Edit roles",
+ "@vitnode/core:roles:can_edit_admin": "Edit roles with administrator permission",
+ "@vitnode/core:roles:can_delete": "Delete roles",
+ "@vitnode/core:roles:can_delete_admin": "Delete roles with administrator permission",
"@vitnode/core:debug": "Debug Panel",
"@vitnode/core:debug:can_view": "View debug panel",
"@vitnode/core:debug:can_clear_cache": "Clear cache",
@@ -22,6 +27,9 @@
"@vitnode/core:files:can_delete": "Delete files",
"@vitnode/core:queue": "Queue Tasks",
"@vitnode/core:queue:can_view": "View queue tasks",
+ "@vitnode/core:cron": "Cron Jobs",
+ "@vitnode/core:cron:can_view": "View cron jobs",
+ "@vitnode/core:cron:can_run": "Run cron jobs",
"@vitnode/core:staff_moderators": "Staff: Moderators",
"@vitnode/core:staff_moderators:can_view": "View moderators list",
"@vitnode/core:staff_moderators:can_create": "Create moderators",
@@ -568,6 +576,18 @@
"submit": "Save changes",
"success": "Role updated"
},
+ "delete": {
+ "title": "Delete role",
+ "desc": "Are you sure you want to delete the role \"{name}\"? This action cannot be undone.",
+ "descWithUsers": "The role \"{name}\" is assigned to {count, plural, one {# user} other {# users}}. Choose another role to move them into before deleting. This action cannot be undone.",
+ "moveToLabel": "Move users to",
+ "selectRole": "Select a role...",
+ "confirm": "Delete role",
+ "cancel": "Cancel",
+ "success": "Role deleted",
+ "successDesc": "The role has been permanently deleted.",
+ "successDescMoved": "{count, plural, one {# user was} other {# users were}} moved to \"{name}\" and the role was permanently deleted."
+ },
"tabs": {
"general": "General",
"content": "Content"
diff --git a/packages/vitnode/src/routes/admin/core/advanced/cron/page.tsx b/packages/vitnode/src/routes/admin/core/advanced/cron/page.tsx
index 7a09a3883..167f78c98 100644
--- a/packages/vitnode/src/routes/admin/core/advanced/cron/page.tsx
+++ b/packages/vitnode/src/routes/admin/core/advanced/cron/page.tsx
@@ -1,10 +1,12 @@
import { getTranslations } from "next-intl/server";
import dynamic from "next/dynamic";
+import { notFound } from "next/navigation";
import React from "react";
import { I18nProvider } from "@/components/i18n-provider";
import { DataTableSkeleton } from "@/components/table/data-table";
import { HeaderContent } from "@/components/ui/header-content";
+import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api";
const CronTableView = dynamic(async () =>
import("@/views/admin/views/core/advanced/cron/cron-table-view").then(
@@ -26,7 +28,14 @@ export const generateMetadata = async () => {
export default async function Page(
props: React.ComponentProps,
) {
- const t = await getTranslations("admin.advanced.cron");
+ const [t, canView] = await Promise.all([
+ getTranslations("admin.advanced.cron"),
+ checkAdminPermissionApi({ module: "cron", permission: "can_view" }),
+ ]);
+
+ if (!canView) {
+ notFound();
+ }
return (
diff --git a/packages/vitnode/src/routes/admin/core/users/roles/page.tsx b/packages/vitnode/src/routes/admin/core/users/roles/page.tsx
index 883d4974e..03c0cfdbc 100644
--- a/packages/vitnode/src/routes/admin/core/users/roles/page.tsx
+++ b/packages/vitnode/src/routes/admin/core/users/roles/page.tsx
@@ -2,11 +2,13 @@ import type { Metadata } from "next/dist/types";
import { getTranslations } from "next-intl/server";
import dynamic from "next/dynamic";
+import { notFound } from "next/navigation";
import React from "react";
import { I18nProvider } from "@/components/i18n-provider";
import { DataTableSkeleton } from "@/components/table/data-table";
import { HeaderContent } from "@/components/ui/header-content";
+import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api";
import { ActionsRolesAdmin } from "@/views/admin/views/core/users/roles/actions/actions";
const RolesAdminView = dynamic(async () =>
@@ -28,16 +30,22 @@ export const generateMetadata = async (): Promise => {
export default async function Page(
props: React.ComponentProps,
) {
- const [t, tNav] = await Promise.all([
+ const [t, tNav, canView, canCreate] = await Promise.all([
getTranslations("admin.role.list"),
getTranslations("admin.global.nav.users"),
+ checkAdminPermissionApi({ module: "roles", permission: "can_view" }),
+ checkAdminPermissionApi({ module: "roles", permission: "can_create" }),
]);
+ if (!canView) {
+ notFound();
+ }
+
return (
-
+ {canCreate && }
}>
diff --git a/packages/vitnode/src/views/admin/layouts/sidebar/nav/get-admin-nav.tsx b/packages/vitnode/src/views/admin/layouts/sidebar/nav/get-admin-nav.tsx
index faede5a10..fe7a7d10a 100644
--- a/packages/vitnode/src/views/admin/layouts/sidebar/nav/get-admin-nav.tsx
+++ b/packages/vitnode/src/views/admin/layouts/sidebar/nav/get-admin-nav.tsx
@@ -160,6 +160,11 @@ export const getAdminNav = async ({
{
title: t("admin.global.nav.users.roles"),
href: "/admin/core/users/roles",
+ permission: {
+ plugin: CONFIG_PLUGIN.pluginId,
+ module: "roles",
+ permission: "can_view",
+ },
},
],
},
@@ -205,6 +210,11 @@ export const getAdminNav = async ({
{
title: t("admin.global.nav.advanced.cron"),
href: "/admin/core/advanced/cron",
+ permission: {
+ plugin: CONFIG_PLUGIN.pluginId,
+ module: "cron",
+ permission: "can_view",
+ },
},
{
title: t("admin.global.nav.advanced.queue"),
diff --git a/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx b/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx
index fce76113f..f27ca126c 100644
--- a/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx
+++ b/packages/vitnode/src/views/admin/views/core/advanced/cron/run-action/run-action.tsx
@@ -5,14 +5,21 @@ import { useTranslations } from "next-intl";
import { useActionState } from "react";
import { toast } from "sonner";
+import { useAdminStaffPermission } from "@/components/staff-permission/provider";
import { Button } from "@/components/ui/button";
import { TooltipWithContent } from "@/components/ui/tooltip";
+import { CONFIG_PLUGIN } from "@/config";
import { mutationApi } from "./mutation-api.server";
export const RunActionCronTable = ({ id }: { id: number }) => {
const t = useTranslations("admin.advanced.cron.list.actions.runNow");
const tError = useTranslations("core.global.errors");
+ const canRun = useAdminStaffPermission({
+ plugin: CONFIG_PLUGIN.pluginId,
+ module: "cron",
+ permission: "can_run",
+ });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, formAction, isPending] = useActionState(async () => {
const mutation = await mutationApi(id);
@@ -27,6 +34,10 @@ export const RunActionCronTable = ({ id }: { id: number }) => {
toast.success(t("success"));
}, null);
+ if (!canRun) {
+ return null;
+ }
+
return (