diff --git a/.github/workflows/build-lint-test.yml b/.github/workflows/build-lint-test.yml index f29bdaf3c..ceb2f9aa4 100644 --- a/.github/workflows/build-lint-test.yml +++ b/.github/workflows/build-lint-test.yml @@ -22,7 +22,7 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 11.1.3 + version: 11.9.0 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/bump_publish.yml b/.github/workflows/bump_publish.yml index 8ef3b1ead..407219558 100644 --- a/.github/workflows/bump_publish.yml +++ b/.github/workflows/bump_publish.yml @@ -52,7 +52,7 @@ jobs: - uses: pnpm/action-setup@v4 name: Install pnpm with: - version: 11.1.3 + version: 11.9.0 - name: Install Node.js uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index 837e8c701..54dbf8e35 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ yarn-error.log* # Others /docker -.vercel \ No newline at end of file +.vercel +# next-agents-md +.next-docs/ diff --git a/apps/api/src/locales/@vitnode/blog/en.json b/apps/api/src/locales/@vitnode/blog/en.json index 65f92dfff..cef3d9ac1 100644 --- a/apps/api/src/locales/@vitnode/blog/en.json +++ b/apps/api/src/locales/@vitnode/blog/en.json @@ -66,5 +66,15 @@ } } } - } + }, + "@vitnode/blog:posts": "Posts", + "@vitnode/blog:posts:can_view": "View posts list", + "@vitnode/blog:posts:can_create": "Create posts", + "@vitnode/blog:posts:can_edit": "Edit posts", + "@vitnode/blog:posts:can_delete": "Delete posts", + "@vitnode/blog:categories": "Categories", + "@vitnode/blog:categories:can_view": "View categories list", + "@vitnode/blog:categories:can_create": "Create categories", + "@vitnode/blog:categories:can_edit": "Edit categories", + "@vitnode/blog:categories:can_delete": "Delete categories" } diff --git a/apps/api/src/locales/@vitnode/core/en.json b/apps/api/src/locales/@vitnode/core/en.json index f19e5c2da..037363e32 100644 --- a/apps/api/src/locales/@vitnode/core/en.json +++ b/apps/api/src/locales/@vitnode/core/en.json @@ -1,4 +1,27 @@ { + "@vitnode/core": { + "title": "Core" + }, + "@vitnode/core:users": "Users", + "@vitnode/core:users:can_view": "View users list", + "@vitnode/core:users:can_create": "Create users", + "@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:debug": "Debug Panel", + "@vitnode/core:debug:can_view": "View debug panel", + "@vitnode/core:debug:can_clear_cache": "Clear cache", + "@vitnode/core:staff_moderators": "Staff: Moderators", + "@vitnode/core:staff_moderators:can_view": "View moderators list", + "@vitnode/core:staff_moderators:can_create": "Create moderators", + "@vitnode/core:staff_moderators:can_edit": "Edit moderator permissions", + "@vitnode/core:staff_moderators:can_delete": "Remove moderators", + "@vitnode/core:staff_admins": "Staff: Administrators", + "@vitnode/core:staff_admins:can_view": "View administrators list", + "@vitnode/core:staff_admins:can_create": "Create administrators", + "@vitnode/core:staff_admins:can_edit": "Edit administrator permissions", + "@vitnode/core:staff_admins:can_delete": "Remove administrators", "core": { "global": { "close": "Close", @@ -230,8 +253,12 @@ "users": { "title": "Users", "list": "User List", - "roles": "Roles", - "staff": "Staff" + "roles": "Roles" + }, + "staff": { + "title": "Staff", + "moderators": "Moderators", + "admins": "Administrators" }, "user_bar": { "home_page": "Home Page", @@ -362,6 +389,14 @@ "staff": { "title": "Staff", "desc": "Manage the staff of your application.", + "protected": "Protected", + "self": "You cannot edit your own permissions", + "delete": { + "title": "Remove staff member?", + "desc": "This revokes the assigned staff access. This action cannot be undone.", + "confirm": "Yes, remove", + "success": "Staff member removed." + }, "tabs": { "moderators": "Moderators", "admins": "Administrators" @@ -369,15 +404,73 @@ "table": { "role": "Role", "user": "User", - "updatedAt": "Updated At" + "permissions": "Permissions", + "unrestricted": "Unrestricted", + "restricted": "Restricted", + "updatedAt": "Updated At", + "edit": "Edit permissions" + }, + "edit": { + "title": "Edit permissions", + "subject": "For", + "back": "Back", + "save": "Save changes", + "success": "Permissions updated successfully.", + "error": "Failed to update permissions.", + "protected": "This entry is protected and its permissions cannot be edited.", + "self": "You cannot edit your own staff permissions, including the entry for your main role.", + "no_permissions": "No plugins have declared staff permissions yet.", + "select_all": "Enable all", + "clear_all": "Disable all", + "search_plugins": "Search plugins", + "search_empty": "No plugins match your search.", + "granted": "{granted}/{total} granted", + "requires": "Requires {permission}", + "mode": { + "label": "Access level", + "unrestricted": { + "label": "Unrestricted", + "desc": "Grant every permission, including ones added later." + }, + "restricted": { + "label": "Restricted", + "desc": "Choose exactly which permissions apply." + } + } + }, + "create": { + "admins": "Add administrator", + "moderators": "Add moderator", + "desc": "Grant staff access to a role or a specific user.", + "button": "Add member", + "back": "Back", + "assign_to": "Assign to", + "tabs": { + "role": "Role", + "role_desc": "Grant staff access to everyone with a role.", + "user": "User", + "user_desc": "Grant staff access to a single person." + }, + "select_role": "Select a role", + "search_user": "Search by name or email", + "submit": "Add member", + "success": "Staff member added.", + "error": "Failed to add staff member.", + "already_exists": "This role or user is already a staff member." }, "moderators": { + "title": "Moderators", + "desc": "Manage the moderators of your application.", + "create": "Add Moderator", "noResults": { "title": "No moderators found", "description": "Assign a role or user to grant moderator permissions." } }, "admins": { + "title": "Administrators", + "desc": "Manage the administrators of your application.", + "create": "Add Administrator", "noResults": { "title": "No administrators found", "description": "Assign a role or user to grant administrator permissions." diff --git a/apps/docs/content/docs/dev/plugins/admin-page.mdx b/apps/docs/content/docs/dev/plugins/admin-page.mdx index 57b346ad5..c8fce3c43 100644 --- a/apps/docs/content/docs/dev/plugins/admin-page.mdx +++ b/apps/docs/content/docs/dev/plugins/admin-page.mdx @@ -147,6 +147,42 @@ export const blogPlugin = () => { }; ``` +#### Gate by permission + +To hide a navigation item from admins who lack a [staff permission](/docs/dev/working-with-users/staff-permissions), add a `permission`. It takes `{ module, permission }` — the `plugin` is inferred from your plugin, so you only reference the module and permission id you declared in `permissionStaff`: + +```tsx title="plugins/{plugin_name}/src/plugin.tsx" +import { buildPlugin } from '@vitnode/core/lib/plugin'; +import { ListIcon } from 'lucide-react'; +import { configPlugin } from './config'; + +export const blogPlugin = () => { + return buildPlugin({ + ...configPlugin, + admin: { + nav: [ + { + id: 'categories', + href: '/admin/blog/categories', + icon: , + // [!code ++] + permission: { module: 'categories', permission: 'can_view' }, + }, + ], + }, + }); +}; +``` + +The same `permission` field works on nested `items`. A parent item disappears once all of its children are hidden, and items without a `permission` are always shown. + + + Hiding a nav item only hides the link — it does not protect the route. Guard + the page itself and the underlying API too. See [Staff + Permissions](/docs/dev/working-with-users/staff-permissions) for the full + pattern. + + diff --git a/apps/docs/content/docs/dev/working-with-users/meta.json b/apps/docs/content/docs/dev/working-with-users/meta.json index 3506e5448..7a573ea53 100644 --- a/apps/docs/content/docs/dev/working-with-users/meta.json +++ b/apps/docs/content/docs/dev/working-with-users/meta.json @@ -2,5 +2,5 @@ "title": "Working with Users", "description": "Learn how to manage users and roles in VitNode with our comprehensive guide.", "icon": "Users", - "pages": ["users", "roles", "..."] + "pages": ["users", "roles", "staff-permissions", "..."] } diff --git a/apps/docs/content/docs/dev/working-with-users/staff-permissions.mdx b/apps/docs/content/docs/dev/working-with-users/staff-permissions.mdx new file mode 100644 index 000000000..0ecb5ae52 --- /dev/null +++ b/apps/docs/content/docs/dev/working-with-users/staff-permissions.mdx @@ -0,0 +1,292 @@ +--- +title: Staff Permissions +description: Declare plugin-based staff permissions and check them on the backend and frontend. +--- + +VitNode has two kinds of staff: **moderators** and **admins**. A staff entry links either a whole role or a single user to a staff group (stored in `core_moderators_permissions` / `core_admin_permissions`). On top of that, plugins can declare **granular permissions** that determine what each staff entry is actually allowed to do. + +Permissions are: + +- **Plugin-based** — every plugin declares its own catalog in its API config. +- **Grouped by module** and split into a `moderator` and an `admin` set. +- **Granted per staff entry** — toggled from the admin panel and stored in the entry's `data` JSON column. + +## Declaring permissions + +Add a `permissionStaff` object to your plugin's `buildApiPlugin` config. Each side (`moderator`, `admin`) is keyed by **module**, and each module lists permission ids: + +```ts title="plugins/{plugin_name}/src/config.api.ts" +import { buildApiPlugin } from "@vitnode/core/api/lib/plugin"; + +export const blogApiPlugin = () => + buildApiPlugin({ + pluginId: "@vitnode/blog", + modules: [postsModule, categoriesModule], + permissionStaff: { + moderator: { + posts: ["can_edit", "can_delete"], + }, + admin: { + posts: ["can_create", "can_edit", "can_delete"], + categories: ["can_manage"], + }, + }, + }); +``` + +### Dependent permissions + +A permission can depend on other permissions in the **same module**. Declare it as an object with `dependsOn` instead of a plain string, and the editor keeps it **hidden until every permission it depends on is enabled** — a `can_view` gate that reveals the rest is the typical use: + +```ts title="plugins/{plugin_name}/src/config.api.ts" +permissionStaff: { + admin: { + posts: [ + "can_view", + // [!code highlight:3] + { permission: "can_create", dependsOn: ["can_view"] }, + { permission: "can_edit", dependsOn: ["can_view"] }, + { permission: "can_delete", dependsOn: ["can_view"] }, + ], + }, +}, +``` + +`dependsOn` is generic, not tied to `can_view`: it accepts any permission id from the same module, multiple gates, and dependency chains (a → b → c). Plain strings and `dependsOn` objects can be mixed freely in the same module. + +When a gate is switched off in the editor, everything that (transitively) depends on it is hidden **and** un-granted in the same step. The dependency is also enforced on the backend when saving, so a forged or stale request can't persist `can_create` without its `can_view`. + + + `dependsOn` controls what an admin can **see and grant**; it does not change how + individual checks evaluate. `checkStaffPermission` still tests each permission on + its own. Keep guarding every action explicitly (see [Backend](#backend)). + + +### Permission labels (i18n) + +Each permission needs a human-readable label. Add it to your plugin's locale file as a **flat top-level key** following the `{pluginId}:{module}:{permission}` convention. You may also add a `{pluginId}:{module}` key for the module heading: + +```json title="plugins/{plugin_name}/src/locales/en.json" +{ + "@vitnode/blog": { "title": "Blog" }, + + "@vitnode/blog:posts": "Posts", + "@vitnode/blog:posts:can_create": "Create posts", + "@vitnode/blog:posts:can_edit": "Edit posts", + "@vitnode/blog:posts:can_delete": "Delete posts", + "@vitnode/blog:categories": "Categories", + "@vitnode/blog:categories:can_manage": "Manage categories" +} +``` + + + Staff entries are listed under **Admin → Core → Staff**. Use **Add moderators/admins** to create + an entry for a role or a specific user (searchable by name or email); after creating it + you land straight on its permission editor. The edit (pencil) action on a row reopens + that editor, where you first pick an access level: + +- **Unrestricted** — the entry is granted every permission for its staff type, + including ones added later. `resolveStaffPermissions` returns `root: true` for it, + so all checks pass. +- **Restricted** — choose exactly which permissions apply, shown as one **tab per + plugin** (with "Enable all" / "Disable all" per tab). The admin set is shown for + administrator entries, the moderator set for moderator entries. Permissions that + [depend on another](#dependent-permissions) stay hidden until their gate is enabled. + +The staff list also shows an **Unrestricted / Restricted** badge per entry. + + + +## Backend + +Use the helpers from `@vitnode/core/api/lib/check-staff-permission` inside a route handler. They read the current user from the request context (`c.get("user")`) and resolve the effective permissions across the user's direct grant and all of their roles (primary + secondary). A user whose role is `root` always passes. + +### Guarding a route + +The simplest way to gate an admin route is to declare `adminStaffPermission` on `buildRoute`. It guards the route with a 403 **before the handler runs**, so the handler stays focused on its logic. `plugin` defaults to the route's own `pluginId`, so you usually only pass `module` and `permission`: + +```ts title="plugins/{plugin_name}/src/api/modules/admin/posts/routes/delete.route.ts" +import { buildRoute } from "@vitnode/core/api/lib/route"; + +export const deletePostRoute = buildRoute({ + pluginId: "@vitnode/blog", + // [!code highlight] + adminStaffPermission: { module: "posts", permission: "can_delete" }, + route: { + method: "delete", + path: "/{id}", + // ... + }, + handler: async c => { + // ...delete the post — the permission is already enforced + }, +}); +``` + + + `adminStaffPermission` always guards the `admin` staff type. For moderator + routes, or checks that depend on runtime data, assert manually (below). + + +### Asserting manually + +For checks that can't be declared statically — for example an extra permission +required only for some targets — call `assertStaffPermission` inside the +handler. It throws `HTTPException(403)` when the permission is missing: + +```ts +import { assertStaffPermission } from "@vitnode/core/api/lib/check-staff-permission"; + +await assertStaffPermission(c, { + type: "admin", + plugin: "@vitnode/blog", + module: "posts", + permission: "can_delete", +}); +``` + +### Checking without throwing + +`checkStaffPermission` returns a boolean instead: + +```ts +import { checkStaffPermission } from "@vitnode/core/api/lib/check-staff-permission"; + +const canEdit = await checkStaffPermission(c, { + type: "moderator", + plugin: "@vitnode/blog", + module: "posts", + permission: "can_edit", +}); +``` + +For the full set (e.g. to return it from an endpoint), use `resolveStaffPermissions(c, { type, user })`, which returns `{ root, permissions }`. + +## Frontend + +### Admin panel (server components) + +`getSessionAdminApi()` returns the admin's `permissions` set. Combine it with the pure `hasStaffPermission` helper: + +```tsx +import { getSessionAdminApi } from "@vitnode/core/lib/api/get-session-admin-api"; +import { hasStaffPermission } from "@vitnode/core/api/lib/staff-permission"; + +export default async function Page() { + const session = await getSessionAdminApi(); + if (!session) return null; + + const canView = hasStaffPermission(session.permissions, { + plugin: "@vitnode/blog", + module: "categories", + permission: "can_view", + }); + + return canView ? : null; +} +``` + +Or use the `checkAdminPermissionApi` shortcut, which resolves the session and +checks a single permission in one call (`plugin` defaults to `@vitnode/core`): + +```tsx +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; + +const canView = await checkAdminPermissionApi({ + plugin: "@vitnode/blog", + module: "categories", + permission: "can_view", +}); +``` + +### Admin panel (client components) + +The current admin's effective permissions are loaded with the admin session and provided through a context. Gate UI with the `useAdminStaffPermission` hook or the `` component from `@vitnode/core/components/staff-permission/provider`: + +```tsx +"use client"; + +import { + AdminStaffPermissionGate, + useAdminStaffPermission, +} from "@vitnode/core/components/staff-permission/provider"; + +const DeleteButton = () => { + const canDelete = useAdminStaffPermission({ + plugin: "@vitnode/blog", + module: "posts", + permission: "can_delete", + }); + + if (!canDelete) return null; + + return ; +}; + +// or declaratively: + + +; +``` + +### Admin sidebar nav + +Admin nav items can be gated by a permission so the sidebar entry is hidden from +admins who lack it. Add a `permission` (`{ module, permission }` — the `plugin` +is inferred from the declaring plugin) to any nav item in `buildPlugin`: + +```tsx title="plugins/{plugin_name}/src/config.tsx" +import { buildPlugin } from "@vitnode/core/lib/plugin"; +import { ListIcon } from "lucide-react"; + +export const blogPlugin = () => + buildPlugin({ + pluginId: "@vitnode/blog", + admin: { + nav: [ + { + id: "categories", + href: "/admin/blog/categories", + icon: , + permission: { module: "categories", permission: "can_view" }, + }, + ], + }, + }); +``` + +The same `permission` field works on sub-items. A parent group disappears once +all of its children are hidden. Items without a `permission` are always shown. + + + Hiding the nav entry does not protect the route. Guard the page itself (e.g. + `notFound()` when `checkAdminPermissionApi` is false) and the underlying + action on the backend with `assertStaffPermission`. + + +### Public site (moderators) + +For moderator actions on the public (non-admin) site, use `checkModeratorPermissionApi` (or `getModeratorPermissionsApi` for the whole set) from `@vitnode/core/lib/api/get-moderator-permissions-api` in a Server Component: + +```tsx +import { checkModeratorPermissionApi } from "@vitnode/core/lib/api/get-moderator-permissions-api"; + +export const PostActions = async () => { + const canEdit = await checkModeratorPermissionApi({ + plugin: "@vitnode/blog", + module: "posts", + permission: "can_edit", + }); + + return canEdit ? : null; +}; +``` + + + Frontend checks only hide UI. Always guard the underlying action on the + backend with `assertStaffPermission` as well. + diff --git a/apps/docs/migrations/0004_dark_iron_lad.sql b/apps/docs/migrations/0004_dark_iron_lad.sql new file mode 100644 index 000000000..cd09bca33 --- /dev/null +++ b/apps/docs/migrations/0004_dark_iron_lad.sql @@ -0,0 +1,2 @@ +ALTER TABLE "core_admin_permissions" ADD COLUMN "data" jsonb DEFAULT '{"permissions":[]}'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "core_moderators_permissions" ADD COLUMN "data" jsonb DEFAULT '{"permissions":[]}'::jsonb NOT NULL; \ No newline at end of file diff --git a/apps/docs/migrations/0005_stale_firebird.sql b/apps/docs/migrations/0005_stale_firebird.sql new file mode 100644 index 000000000..3a502edd5 --- /dev/null +++ b/apps/docs/migrations/0005_stale_firebird.sql @@ -0,0 +1,2 @@ +ALTER TABLE "core_admin_permissions" ALTER COLUMN "data" SET DEFAULT '{"unrestricted":false,"permissions":[]}'::jsonb;--> statement-breakpoint +ALTER TABLE "core_moderators_permissions" ALTER COLUMN "data" SET DEFAULT '{"unrestricted":false,"permissions":[]}'::jsonb; \ No newline at end of file diff --git a/apps/docs/migrations/meta/0004_snapshot.json b/apps/docs/migrations/meta/0004_snapshot.json new file mode 100644 index 000000000..5896861a1 --- /dev/null +++ b/apps/docs/migrations/meta/0004_snapshot.json @@ -0,0 +1,1628 @@ +{ + "id": "81056263-a299-4cc5-a965-07deade32e3e", + "prevId": "4b58db56-325c-4831-b75f-e83dede76099", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.core_admin_permissions": { + "name": "core_admin_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"permissions\":[]}'::jsonb" + } + }, + "indexes": { + "core_admin_permissions_role_id_idx": { + "name": "core_admin_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_permissions_user_id_idx": { + "name": "core_admin_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_permissions_roleId_core_roles_id_fk": { + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_permissions_userId_core_users_id_fk": { + "name": "core_admin_permissions_userId_core_users_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_admin_sessions": { + "name": "core_admin_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_admin_sessions_token_idx": { + "name": "core_admin_sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_sessions_user_id_idx": { + "name": "core_admin_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_sessions_userId_core_users_id_fk": { + "name": "core_admin_sessions_userId_core_users_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_admin_sessions_token_unique": { + "name": "core_admin_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_cron": { + "name": "core_cron", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "lastRun": { + "name": "lastRun", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "module": { + "name": "module", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "nextRun": { + "name": "nextRun", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages": { + "name": "core_languages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "time24": { + "name": "time24", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "core_languages_code_idx": { + "name": "core_languages_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_languages_name_idx": { + "name": "core_languages_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_languages_code_unique": { + "name": "core_languages_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages_words": { + "name": "core_languages_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "languageCode": { + "name": "languageCode", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pluginCode": { + "name": "pluginCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "itemId": { + "name": "itemId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tableName": { + "name": "tableName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "variable": { + "name": "variable", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_languages_words_lang_code_idx": { + "name": "core_languages_words_lang_code_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_languages_words_languageCode_core_languages_code_fk": { + "name": "core_languages_words_languageCode_core_languages_code_fk", + "tableFrom": "core_languages_words", + "tableTo": "core_languages", + "columnsFrom": [ + "languageCode" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_logs": { + "name": "core_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'GET'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'localhost'" + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "statusCode": { + "name": "statusCode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "test123": { + "name": "test123", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "core_logs_userId_core_users_id_fk": { + "name": "core_logs_userId_core_users_id_fk", + "tableFrom": "core_logs", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_moderators_permissions": { + "name": "core_moderators_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"permissions\":[]}'::jsonb" + } + }, + "indexes": { + "core_moderators_permissions_role_id_idx": { + "name": "core_moderators_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_moderators_permissions_user_id_idx": { + "name": "core_moderators_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_moderators_permissions_roleId_core_roles_id_fk": { + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_moderators_permissions_userId_core_users_id_fk": { + "name": "core_moderators_permissions_userId_core_users_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_roles": { + "name": "core_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "root": { + "name": "root", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "guest": { + "name": "guest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "type": "varchar(19)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions": { + "name": "core_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_sessions_user_id_idx": { + "name": "core_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_sessions_userId_core_users_id_fk": { + "name": "core_sessions_userId_core_users_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_token_unique": { + "name": "core_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions_known_devices": { + "name": "core_sessions_known_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_sessions_known_devices_ip_address_idx": { + "name": "core_sessions_known_devices_ip_address_idx", + "columns": [ + { + "expression": "ipAddress", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_known_devices_publicId_unique": { + "name": "core_sessions_known_devices_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users": { + "name": "core_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "nameCode": { + "name": "nameCode", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "newsletter": { + "name": "newsletter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatarColor": { + "name": "avatarColor", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "birthday": { + "name": "birthday", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'en'" + } + }, + "indexes": { + "core_users_name_code_idx": { + "name": "core_users_name_code_idx", + "columns": [ + { + "expression": "nameCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_name_idx": { + "name": "core_users_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_email_idx": { + "name": "core_users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_roleId_core_roles_id_fk": { + "name": "core_users_roleId_core_roles_id_fk", + "tableFrom": "core_users", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "core_users_language_core_languages_code_fk": { + "name": "core_users_language_core_languages_code_fk", + "tableFrom": "core_users", + "tableTo": "core_languages", + "columnsFrom": [ + "language" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set default", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_nameCode_unique": { + "name": "core_users_nameCode_unique", + "nullsNotDistinct": false, + "columns": [ + "nameCode" + ] + }, + "core_users_name_unique": { + "name": "core_users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "core_users_email_unique": { + "name": "core_users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_confirm_emails": { + "name": "core_users_confirm_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_confirm_emails_userId_core_users_id_fk": { + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "tableFrom": "core_users_confirm_emails", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_confirm_emails_token_unique": { + "name": "core_users_confirm_emails_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_forgot_password": { + "name": "core_users_forgot_password", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_forgot_password_userId_core_users_id_fk": { + "name": "core_users_forgot_password_userId_core_users_id_fk", + "tableFrom": "core_users_forgot_password", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_forgot_password_userId_unique": { + "name": "core_users_forgot_password_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + }, + "core_users_forgot_password_token_unique": { + "name": "core_users_forgot_password_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_secondary_roles": { + "name": "core_users_secondary_roles", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_users_secondary_roles_user_id_idx": { + "name": "core_users_secondary_roles_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_secondary_roles_role_id_idx": { + "name": "core_users_secondary_roles_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_secondary_roles_userId_core_users_id_fk": { + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "tableFrom": "core_users_secondary_roles", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_users_secondary_roles_roleId_core_roles_id_fk": { + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "tableFrom": "core_users_secondary_roles", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "core_users_secondary_roles_userId_roleId_pk": { + "name": "core_users_secondary_roles_userId_roleId_pk", + "columns": [ + "userId", + "roleId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_sso": { + "name": "core_users_sso", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_users_sso_user_id_idx": { + "name": "core_users_sso_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_sso_userId_core_users_id_fk": { + "name": "core_users_sso_userId_core_users_id_fk", + "tableFrom": "core_users_sso", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_categories": { + "name": "blog_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "titleSeo": { + "name": "titleSeo", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blog_categories_titleSeo_unique": { + "name": "blog_categories_titleSeo_unique", + "nullsNotDistinct": false, + "columns": [ + "titleSeo" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_posts": { + "name": "blog_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "titleSeo": { + "name": "titleSeo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "categoryId": { + "name": "categoryId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "blog_posts_categoryId_blog_categories_id_fk": { + "name": "blog_posts_categoryId_blog_categories_id_fk", + "tableFrom": "blog_posts", + "tableTo": "blog_categories", + "columnsFrom": [ + "categoryId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blog_posts_titleSeo_unique": { + "name": "blog_posts_titleSeo_unique", + "nullsNotDistinct": false, + "columns": [ + "titleSeo" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/docs/migrations/meta/0005_snapshot.json b/apps/docs/migrations/meta/0005_snapshot.json new file mode 100644 index 000000000..8f614a8f4 --- /dev/null +++ b/apps/docs/migrations/meta/0005_snapshot.json @@ -0,0 +1,1628 @@ +{ + "id": "755b9c3b-3019-488c-b361-448bcdeedb47", + "prevId": "81056263-a299-4cc5-a965-07deade32e3e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.core_admin_permissions": { + "name": "core_admin_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"unrestricted\":false,\"permissions\":[]}'::jsonb" + } + }, + "indexes": { + "core_admin_permissions_role_id_idx": { + "name": "core_admin_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_permissions_user_id_idx": { + "name": "core_admin_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_permissions_roleId_core_roles_id_fk": { + "name": "core_admin_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_permissions_userId_core_users_id_fk": { + "name": "core_admin_permissions_userId_core_users_id_fk", + "tableFrom": "core_admin_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_admin_sessions": { + "name": "core_admin_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_admin_sessions_token_idx": { + "name": "core_admin_sessions_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_admin_sessions_user_id_idx": { + "name": "core_admin_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_admin_sessions_userId_core_users_id_fk": { + "name": "core_admin_sessions_userId_core_users_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_admin_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_admin_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_admin_sessions_token_unique": { + "name": "core_admin_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_cron": { + "name": "core_cron", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "lastRun": { + "name": "lastRun", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "module": { + "name": "module", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "nextRun": { + "name": "nextRun", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "schedule": { + "name": "schedule", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages": { + "name": "core_languages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "time24": { + "name": "time24", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "core_languages_code_idx": { + "name": "core_languages_code_idx", + "columns": [ + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_languages_name_idx": { + "name": "core_languages_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_languages_code_unique": { + "name": "core_languages_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_languages_words": { + "name": "core_languages_words", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "languageCode": { + "name": "languageCode", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pluginCode": { + "name": "pluginCode", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "itemId": { + "name": "itemId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tableName": { + "name": "tableName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "variable": { + "name": "variable", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_languages_words_lang_code_idx": { + "name": "core_languages_words_lang_code_idx", + "columns": [ + { + "expression": "languageCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_languages_words_languageCode_core_languages_code_fk": { + "name": "core_languages_words_languageCode_core_languages_code_fk", + "tableFrom": "core_languages_words", + "tableTo": "core_languages", + "columnsFrom": [ + "languageCode" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_logs": { + "name": "core_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "pluginId": { + "name": "pluginId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "default": "'GET'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'localhost'" + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "statusCode": { + "name": "statusCode", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 500 + }, + "userId": { + "name": "userId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "test123": { + "name": "test123", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "core_logs_userId_core_users_id_fk": { + "name": "core_logs_userId_core_users_id_fk", + "tableFrom": "core_logs", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_moderators_permissions": { + "name": "core_moderators_permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"unrestricted\":false,\"permissions\":[]}'::jsonb" + } + }, + "indexes": { + "core_moderators_permissions_role_id_idx": { + "name": "core_moderators_permissions_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_moderators_permissions_user_id_idx": { + "name": "core_moderators_permissions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_moderators_permissions_roleId_core_roles_id_fk": { + "name": "core_moderators_permissions_roleId_core_roles_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_moderators_permissions_userId_core_users_id_fk": { + "name": "core_moderators_permissions_userId_core_users_id_fk", + "tableFrom": "core_moderators_permissions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_roles": { + "name": "core_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "protected": { + "name": "protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "root": { + "name": "root", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "guest": { + "name": "guest", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "color": { + "name": "color", + "type": "varchar(19)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions": { + "name": "core_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deviceId": { + "name": "deviceId", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_sessions_user_id_idx": { + "name": "core_sessions_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_sessions_userId_core_users_id_fk": { + "name": "core_sessions_userId_core_users_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_sessions_deviceId_core_sessions_known_devices_id_fk": { + "name": "core_sessions_deviceId_core_sessions_known_devices_id_fk", + "tableFrom": "core_sessions", + "tableTo": "core_sessions_known_devices", + "columnsFrom": [ + "deviceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_token_unique": { + "name": "core_sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_sessions_known_devices": { + "name": "core_sessions_known_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "publicId": { + "name": "publicId", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lastSeen": { + "name": "lastSeen", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_sessions_known_devices_ip_address_idx": { + "name": "core_sessions_known_devices_ip_address_idx", + "columns": [ + { + "expression": "ipAddress", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_sessions_known_devices_publicId_unique": { + "name": "core_sessions_known_devices_publicId_unique", + "nullsNotDistinct": false, + "columns": [ + "publicId" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users": { + "name": "core_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "nameCode": { + "name": "nameCode", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "newsletter": { + "name": "newsletter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "avatarColor": { + "name": "avatarColor", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "birthday": { + "name": "birthday", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "default": "'en'" + } + }, + "indexes": { + "core_users_name_code_idx": { + "name": "core_users_name_code_idx", + "columns": [ + { + "expression": "nameCode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_name_idx": { + "name": "core_users_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_email_idx": { + "name": "core_users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_roleId_core_roles_id_fk": { + "name": "core_users_roleId_core_roles_id_fk", + "tableFrom": "core_users", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "core_users_language_core_languages_code_fk": { + "name": "core_users_language_core_languages_code_fk", + "tableFrom": "core_users", + "tableTo": "core_languages", + "columnsFrom": [ + "language" + ], + "columnsTo": [ + "code" + ], + "onDelete": "set default", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_nameCode_unique": { + "name": "core_users_nameCode_unique", + "nullsNotDistinct": false, + "columns": [ + "nameCode" + ] + }, + "core_users_name_unique": { + "name": "core_users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "core_users_email_unique": { + "name": "core_users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_confirm_emails": { + "name": "core_users_confirm_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_confirm_emails_userId_core_users_id_fk": { + "name": "core_users_confirm_emails_userId_core_users_id_fk", + "tableFrom": "core_users_confirm_emails", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_confirm_emails_token_unique": { + "name": "core_users_confirm_emails_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_forgot_password": { + "name": "core_users_forgot_password", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "ipAddress": { + "name": "ipAddress", + "type": "varchar(40)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "core_users_forgot_password_userId_core_users_id_fk": { + "name": "core_users_forgot_password_userId_core_users_id_fk", + "tableFrom": "core_users_forgot_password", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "core_users_forgot_password_userId_unique": { + "name": "core_users_forgot_password_userId_unique", + "nullsNotDistinct": false, + "columns": [ + "userId" + ] + }, + "core_users_forgot_password_token_unique": { + "name": "core_users_forgot_password_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_secondary_roles": { + "name": "core_users_secondary_roles", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "roleId": { + "name": "roleId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "core_users_secondary_roles_user_id_idx": { + "name": "core_users_secondary_roles_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "core_users_secondary_roles_role_id_idx": { + "name": "core_users_secondary_roles_role_id_idx", + "columns": [ + { + "expression": "roleId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_secondary_roles_userId_core_users_id_fk": { + "name": "core_users_secondary_roles_userId_core_users_id_fk", + "tableFrom": "core_users_secondary_roles", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "core_users_secondary_roles_roleId_core_roles_id_fk": { + "name": "core_users_secondary_roles_roleId_core_roles_id_fk", + "tableFrom": "core_users_secondary_roles", + "tableTo": "core_roles", + "columnsFrom": [ + "roleId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "core_users_secondary_roles_userId_roleId_pk": { + "name": "core_users_secondary_roles_userId_roleId_pk", + "columns": [ + "userId", + "roleId" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.core_users_sso": { + "name": "core_users_sso", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "core_users_sso_user_id_idx": { + "name": "core_users_sso_user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "core_users_sso_userId_core_users_id_fk": { + "name": "core_users_sso_userId_core_users_id_fk", + "tableFrom": "core_users_sso", + "tableTo": "core_users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_categories": { + "name": "blog_categories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "titleSeo": { + "name": "titleSeo", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blog_categories_titleSeo_unique": { + "name": "blog_categories_titleSeo_unique", + "nullsNotDistinct": false, + "columns": [ + "titleSeo" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.blog_posts": { + "name": "blog_posts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "titleSeo": { + "name": "titleSeo", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "categoryId": { + "name": "categoryId", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "blog_posts_categoryId_blog_categories_id_fk": { + "name": "blog_posts_categoryId_blog_categories_id_fk", + "tableFrom": "blog_posts", + "tableTo": "blog_categories", + "columnsFrom": [ + "categoryId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "blog_posts_titleSeo_unique": { + "name": "blog_posts_titleSeo_unique", + "nullsNotDistinct": false, + "columns": [ + "titleSeo" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/docs/migrations/meta/_journal.json b/apps/docs/migrations/meta/_journal.json index a741dab54..e61d4ed6a 100644 --- a/apps/docs/migrations/meta/_journal.json +++ b/apps/docs/migrations/meta/_journal.json @@ -29,6 +29,20 @@ "when": 1782410108192, "tag": "0003_add_users_secondary_roles", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1782574579285, + "tag": "0004_dark_iron_lad", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1782638542375, + "tag": "0005_stale_firebird", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/docs/package.json b/apps/docs/package.json index 117742745..e1879f427 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -34,7 +34,7 @@ "hono": "^4.12.27", "lucide-react": "^1.21.0", "motion": "^12.41.0", - "next": "16.3.0-preview.4", + "next": "16.3.0-preview.5", "next-intl": "^4.13.0", "node-cron": "^4.5.0", "react": "^19.2.7", diff --git a/apps/docs/src/app/[locale]/(main)/(home)/page.tsx b/apps/docs/src/app/[locale]/(main)/(home)/page.tsx index 5d6c88687..fdbcec642 100644 --- a/apps/docs/src/app/[locale]/(main)/(home)/page.tsx +++ b/apps/docs/src/app/[locale]/(main)/(home)/page.tsx @@ -12,7 +12,7 @@ import { CallToActionSection } from "./sections/call-to-action"; import { PoweringBySection } from "./sections/powering-by/powering-by"; export const metadata: Metadata = { - title: "VitNode: Extendable Framework for Building Apps", + title: "VitNode: Community Framework for Building Apps", description: "Build with Next.js and Hono.js. It provides a structured, plugin-based architecture with Admin Control Panel that makes development faster and less complex.", }; @@ -23,7 +23,7 @@ export default function HomePage() {

- Extendable Framework for + Community Framework for Building Apps

diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx index 570a0f665..3dfeea398 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/categories/page.tsx @@ -3,10 +3,13 @@ import type { Metadata } from "next"; import { I18nProvider } from "@vitnode/core/components/i18n-provider"; import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; import { getTranslations } from "next-intl/server"; import dynamic from "next/dynamic"; +import { notFound } from "next/navigation"; import React from "react"; +import { CONFIG_PLUGIN } from "@vitnode/blog/const"; import { ActionsCategoriesAdmin } from "@vitnode/blog/views/admin/categories/actions/actions"; const CategoriesAdminView = dynamic(async () => @@ -26,16 +29,30 @@ export const generateMetadata = async (): Promise => { export default async function CategoriesPage( params: React.ComponentProps, ) { - const [t, tNav] = await Promise.all([ + const [t, tNav, canView, canCreate] = await Promise.all([ getTranslations("@vitnode/blog.admin.categories"), getTranslations("@vitnode/blog.admin.nav"), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "categories", + permission: "can_view", + }), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "categories", + permission: "can_create", + }), ]); + if (!canView) { + notFound(); + } + return (
- + {canCreate && } }> diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx index 439d3f974..99d58036d 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-blog)/blog/posts/page.tsx @@ -3,10 +3,13 @@ import type { Metadata } from "next"; import { I18nProvider } from "@vitnode/core/components/i18n-provider"; import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; import { getTranslations } from "next-intl/server"; import dynamic from "next/dynamic"; +import { notFound } from "next/navigation"; import React from "react"; +import { CONFIG_PLUGIN } from "@vitnode/blog/const"; import { ActionsPostsAdmin } from "@vitnode/blog/views/admin/posts/actions/actions"; const PostsAdminView = dynamic(async () => @@ -26,16 +29,30 @@ export const generateMetadata = async (): Promise => { export default async function PostsPage( params: React.ComponentProps, ) { - const [t, tNav] = await Promise.all([ + const [t, tNav, canView, canCreate] = await Promise.all([ getTranslations("@vitnode/blog.admin.posts"), getTranslations("@vitnode/blog.admin.nav"), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "posts", + permission: "can_view", + }), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "posts", + permission: "can_create", + }), ]); + if (!canView) { + notFound(); + } + return (
- + {canCreate && } }> diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx index 1d2ee8e22..17adac4dd 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/debug/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 "@vitnode/core/components/i18n-provider"; import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; import { ClearCacheAction } from "@vitnode/core/views/admin/views/core/debug/actions/clear-cache/clear-cache"; const SystemLogsView = dynamic(async () => @@ -27,13 +29,21 @@ export const generateMetadata = async () => { export default async function Page( props: React.ComponentProps, ) { - const t = await getTranslations("admin.debug"); + const [t, canView, canClearCache] = await Promise.all([ + getTranslations("admin.debug"), + checkAdminPermissionApi({ module: "debug", permission: "can_view" }), + checkAdminPermissionApi({ module: "debug", permission: "can_clear_cache" }), + ]); + + if (!canView) { + notFound(); + } return (
- + {canClearCache && } diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/create/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/create/page.tsx new file mode 100644 index 000000000..765c9f777 --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/create/page.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { Loader } from "@vitnode/core/components/ui/loader"; +import { CreateStaffPermissionsView } from "@vitnode/core/views/admin/views/core/staff/create/create-staff-permissions-view"; + +export default function Page() { + return ( + +
+ }> + + +
+
+ ); +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/edit/[id]/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/edit/[id]/page.tsx new file mode 100644 index 000000000..b9eaf5147 --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/edit/[id]/page.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { Loader } from "@vitnode/core/components/ui/loader"; +import { EditStaffPermissionsView } from "@vitnode/core/views/admin/views/core/staff/edit/edit-staff-permissions-view"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + +
+ }> + + +
+
+ ); +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/admins/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/page.tsx similarity index 53% rename from apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/admins/page.tsx rename to apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/page.tsx index 0600bde7d..5eba0a3c1 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/admins/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/admins/page.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; -import { AdminsStaffAdminView } from "@vitnode/core/views/admin/views/core/staff/admins/admins-staff-view"; +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { AdminsStaffAdminView } from "@vitnode/core/views/admin/views/core/staff/views/admins/admins-staff-view"; export default function Page( props: React.ComponentProps, ) { return ( - }> + - + ); } diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/create/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/create/page.tsx new file mode 100644 index 000000000..84e7698da --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/create/page.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { Loader } from "@vitnode/core/components/ui/loader"; +import { CreateStaffPermissionsView } from "@vitnode/core/views/admin/views/core/staff/create/create-staff-permissions-view"; + +export default function Page() { + return ( + +
+ }> + + +
+
+ ); +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/edit/[id]/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/edit/[id]/page.tsx new file mode 100644 index 000000000..85289e248 --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/edit/[id]/page.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { Loader } from "@vitnode/core/components/ui/loader"; +import { EditStaffPermissionsView } from "@vitnode/core/views/admin/views/core/staff/edit/edit-staff-permissions-view"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + +
+ }> + + +
+
+ ); +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/moderators/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/page.tsx similarity index 53% rename from apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/moderators/page.tsx rename to apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/page.tsx index 269e8fa68..b899bddf5 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/moderators/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/staff/moderators/page.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; -import { ModeratorsStaffAdminView } from "@vitnode/core/views/admin/views/core/staff/moderators/moderators-staff-view"; +import { I18nProvider } from "@vitnode/core/components/i18n-provider"; +import { ModeratorsStaffAdminView } from "@vitnode/core/views/admin/views/core/staff/views/moderators/moderators-staff-view"; export default function Page( props: React.ComponentProps, ) { return ( - }> + - + ); } diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx index 2d4e42a72..3dd4691f9 100644 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/page.tsx +++ b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/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 "@vitnode/core/components/i18n-provider"; import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; import { CreateUserAdmin } from "@vitnode/core/views/admin/views/core/users/actions/create/create"; const UsersAdminView = dynamic(async () => @@ -26,16 +28,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.user.list"), getTranslations("admin.global.nav.users"), + checkAdminPermissionApi({ module: "users", permission: "can_view" }), + checkAdminPermissionApi({ module: "users", permission: "can_create" }), ]); + if (!canView) { + notFound(); + } + return (
- + {canCreate && } }> diff --git a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/layout.tsx b/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/layout.tsx deleted file mode 100644 index a1aebf3f5..000000000 --- a/apps/docs/src/app/[locale]/admin/(auth)/(plugins)/(vitnode-core)/core/users/staff/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { LayoutStaffAdmin } from "@vitnode/core/views/admin/views/core/staff/layout"; - -export default function Layout( - props: React.ComponentProps, -) { - return ; -} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/create/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/create/page.tsx new file mode 100644 index 000000000..e6858f1ec --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/create/page.tsx @@ -0,0 +1,5 @@ +import { BreadcrumbStaffCreateAdmin } from "@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin"; + +export default function BreadcrumbSlot() { + return ; +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/edit/[id]/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/edit/[id]/page.tsx new file mode 100644 index 000000000..2dbb8c87e --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/edit/[id]/page.tsx @@ -0,0 +1,11 @@ +import { BreadcrumbStaffEditAdmin } from "@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin"; + +export default async function BreadcrumbSlot({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ; +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/staff/admins/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/page.tsx similarity index 100% rename from apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/staff/admins/page.tsx rename to apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/admins/page.tsx diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/create/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/create/page.tsx new file mode 100644 index 000000000..dd4ccc9f6 --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/create/page.tsx @@ -0,0 +1,5 @@ +import { BreadcrumbStaffCreateAdmin } from "@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin"; + +export default function BreadcrumbSlot() { + return ; +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/edit/[id]/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/edit/[id]/page.tsx new file mode 100644 index 000000000..753c353b2 --- /dev/null +++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/edit/[id]/page.tsx @@ -0,0 +1,11 @@ +import { BreadcrumbStaffEditAdmin } from "@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin"; + +export default async function BreadcrumbSlot({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ; +} diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/staff/moderators/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/page.tsx similarity index 100% rename from apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/staff/moderators/page.tsx rename to apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/staff/moderators/page.tsx diff --git a/apps/docs/src/examples/hover-card.tsx b/apps/docs/src/examples/hover-card.tsx index 59f51700e..b9e15734e 100644 --- a/apps/docs/src/examples/hover-card.tsx +++ b/apps/docs/src/examples/hover-card.tsx @@ -11,7 +11,7 @@ export default function HoverCardExample() { Hover - Extendable Framework - created and maintained by @axendev. + Community Framework - created and maintained by @axendev. ); diff --git a/apps/docs/src/locales/@vitnode/blog/en.json b/apps/docs/src/locales/@vitnode/blog/en.json index 65f92dfff..cef3d9ac1 100644 --- a/apps/docs/src/locales/@vitnode/blog/en.json +++ b/apps/docs/src/locales/@vitnode/blog/en.json @@ -66,5 +66,15 @@ } } } - } + }, + "@vitnode/blog:posts": "Posts", + "@vitnode/blog:posts:can_view": "View posts list", + "@vitnode/blog:posts:can_create": "Create posts", + "@vitnode/blog:posts:can_edit": "Edit posts", + "@vitnode/blog:posts:can_delete": "Delete posts", + "@vitnode/blog:categories": "Categories", + "@vitnode/blog:categories:can_view": "View categories list", + "@vitnode/blog:categories:can_create": "Create categories", + "@vitnode/blog:categories:can_edit": "Edit categories", + "@vitnode/blog:categories:can_delete": "Delete categories" } diff --git a/apps/docs/src/locales/@vitnode/core/en.json b/apps/docs/src/locales/@vitnode/core/en.json index f19e5c2da..037363e32 100644 --- a/apps/docs/src/locales/@vitnode/core/en.json +++ b/apps/docs/src/locales/@vitnode/core/en.json @@ -1,4 +1,27 @@ { + "@vitnode/core": { + "title": "Core" + }, + "@vitnode/core:users": "Users", + "@vitnode/core:users:can_view": "View users list", + "@vitnode/core:users:can_create": "Create users", + "@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:debug": "Debug Panel", + "@vitnode/core:debug:can_view": "View debug panel", + "@vitnode/core:debug:can_clear_cache": "Clear cache", + "@vitnode/core:staff_moderators": "Staff: Moderators", + "@vitnode/core:staff_moderators:can_view": "View moderators list", + "@vitnode/core:staff_moderators:can_create": "Create moderators", + "@vitnode/core:staff_moderators:can_edit": "Edit moderator permissions", + "@vitnode/core:staff_moderators:can_delete": "Remove moderators", + "@vitnode/core:staff_admins": "Staff: Administrators", + "@vitnode/core:staff_admins:can_view": "View administrators list", + "@vitnode/core:staff_admins:can_create": "Create administrators", + "@vitnode/core:staff_admins:can_edit": "Edit administrator permissions", + "@vitnode/core:staff_admins:can_delete": "Remove administrators", "core": { "global": { "close": "Close", @@ -230,8 +253,12 @@ "users": { "title": "Users", "list": "User List", - "roles": "Roles", - "staff": "Staff" + "roles": "Roles" + }, + "staff": { + "title": "Staff", + "moderators": "Moderators", + "admins": "Administrators" }, "user_bar": { "home_page": "Home Page", @@ -362,6 +389,14 @@ "staff": { "title": "Staff", "desc": "Manage the staff of your application.", + "protected": "Protected", + "self": "You cannot edit your own permissions", + "delete": { + "title": "Remove staff member?", + "desc": "This revokes the assigned staff access. This action cannot be undone.", + "confirm": "Yes, remove", + "success": "Staff member removed." + }, "tabs": { "moderators": "Moderators", "admins": "Administrators" @@ -369,15 +404,73 @@ "table": { "role": "Role", "user": "User", - "updatedAt": "Updated At" + "permissions": "Permissions", + "unrestricted": "Unrestricted", + "restricted": "Restricted", + "updatedAt": "Updated At", + "edit": "Edit permissions" + }, + "edit": { + "title": "Edit permissions", + "subject": "For", + "back": "Back", + "save": "Save changes", + "success": "Permissions updated successfully.", + "error": "Failed to update permissions.", + "protected": "This entry is protected and its permissions cannot be edited.", + "self": "You cannot edit your own staff permissions, including the entry for your main role.", + "no_permissions": "No plugins have declared staff permissions yet.", + "select_all": "Enable all", + "clear_all": "Disable all", + "search_plugins": "Search plugins", + "search_empty": "No plugins match your search.", + "granted": "{granted}/{total} granted", + "requires": "Requires {permission}", + "mode": { + "label": "Access level", + "unrestricted": { + "label": "Unrestricted", + "desc": "Grant every permission, including ones added later." + }, + "restricted": { + "label": "Restricted", + "desc": "Choose exactly which permissions apply." + } + } + }, + "create": { + "admins": "Add administrator", + "moderators": "Add moderator", + "desc": "Grant staff access to a role or a specific user.", + "button": "Add member", + "back": "Back", + "assign_to": "Assign to", + "tabs": { + "role": "Role", + "role_desc": "Grant staff access to everyone with a role.", + "user": "User", + "user_desc": "Grant staff access to a single person." + }, + "select_role": "Select a role", + "search_user": "Search by name or email", + "submit": "Add member", + "success": "Staff member added.", + "error": "Failed to add staff member.", + "already_exists": "This role or user is already a staff member." }, "moderators": { + "title": "Moderators", + "desc": "Manage the moderators of your application.", + "create": "Add Moderator", "noResults": { "title": "No moderators found", "description": "Assign a role or user to grant moderator permissions." } }, "admins": { + "title": "Administrators", + "desc": "Manage the administrators of your application.", + "create": "Add Administrator", "noResults": { "title": "No administrators found", "description": "Assign a role or user to grant administrator permissions." diff --git a/package.json b/package.json index ff906d3a4..359adbe05 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "engines": { "node": ">=22" }, - "packageManager": "pnpm@11.1.3", + "packageManager": "pnpm@11.9.0", "workspaces": [ "apps/*", "packages/*", diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/[...rest]/page.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/[...rest]/page.tsx new file mode 100644 index 000000000..e66aa3f96 --- /dev/null +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/[...rest]/page.tsx @@ -0,0 +1,3 @@ +export default function BreadcrumbSlot() { + return null; +} diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/default.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/default.tsx index 9e01849ad..4c84c0634 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/default.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/default.tsx @@ -1,6 +1,4 @@ -// Root fallback for the @breadcrumb slot. Required so unmatched routes (pages -// without an explicit breadcrumb override) don't 404 the slot on hard loads. -// Plugin breadcrumb routes are copied alongside this file by `vitnode`. +// Fallback for the @breadcrumb slot on unmatched soft-navigation/hard loads. export default function Default() { return null; } diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/page.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/page.tsx index 403a03499..6ab2ce8a2 100644 --- a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/page.tsx +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/(main)/@breadcrumb/page.tsx @@ -1,5 +1,6 @@ // Home page (/) has no breadcrumb. An explicit slot page (not just default.tsx) -// keeps client-side navigation back to "/" from showing a stale breadcrumb. +// is required so client-side navigation back to "/" clears a previously-rendered +// breadcrumb — otherwise Next.js keeps the slot's stale active state on soft nav. export default function BreadcrumbSlot() { return null; } diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/@breadcrumb/[...all]/page.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/@breadcrumb/[...all]/page.tsx new file mode 100644 index 000000000..e66aa3f96 --- /dev/null +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/@breadcrumb/[...all]/page.tsx @@ -0,0 +1,3 @@ +export default function BreadcrumbSlot() { + return null; +} diff --git a/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/@breadcrumb/default.tsx b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/@breadcrumb/default.tsx new file mode 100644 index 000000000..4c84c0634 --- /dev/null +++ b/packages/create-vitnode-app/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/@breadcrumb/default.tsx @@ -0,0 +1,4 @@ +// Fallback for the @breadcrumb slot on unmatched soft-navigation/hard loads. +export default function Default() { + return null; +} diff --git a/packages/create-vitnode-app/src/create/package-versions.ts b/packages/create-vitnode-app/src/create/package-versions.ts index 3644c9df5..2dbae0197 100644 --- a/packages/create-vitnode-app/src/create/package-versions.ts +++ b/packages/create-vitnode-app/src/create/package-versions.ts @@ -18,7 +18,7 @@ export const versionsPackageJson = { react: "^19.2", reactDom: "^19.2", - nextSingle: "16.3.0-preview.4", + nextSingle: "16.3.0-preview.5", nextIntl: "^4.13", useIntl: "^4.13", rhf: "^7.80", diff --git a/packages/vitnode/package.json b/packages/vitnode/package.json index 63cb460b9..ffda506d2 100644 --- a/packages/vitnode/package.json +++ b/packages/vitnode/package.json @@ -60,7 +60,7 @@ "jiti": "^2.7.0", "jsdom": "^29.1.1", "lucide-react": "^1.21.0", - "next": "16.3.0-preview.4", + "next": "16.3.0-preview.5", "next-intl": "^4.13.0", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/packages/vitnode/scripts/prepare-database.ts b/packages/vitnode/scripts/prepare-database.ts index 40fb0f58e..3763adc1f 100644 --- a/packages/vitnode/scripts/prepare-database.ts +++ b/packages/vitnode/scripts/prepare-database.ts @@ -129,10 +129,12 @@ export const initialDataForDatabase = async () => { { roleId: roles[2].id, protected: true, + data: { unrestricted: true, permissions: [] }, }, { roleId: roles[3].id, protected: true, + data: { unrestricted: true, permissions: [] }, }, ]); @@ -140,6 +142,7 @@ export const initialDataForDatabase = async () => { await dbClient.insert(core_admin_permissions).values({ roleId: roles[3].id, protected: true, + data: { unrestricted: true, permissions: [] }, }); } }; diff --git a/packages/vitnode/src/api/lib/check-staff-permission.ts b/packages/vitnode/src/api/lib/check-staff-permission.ts new file mode 100644 index 000000000..670fd0633 --- /dev/null +++ b/packages/vitnode/src/api/lib/check-staff-permission.ts @@ -0,0 +1,102 @@ +import type { Context } from "hono"; + +import { and, eq, inArray, or } from "drizzle-orm"; +import { HTTPException } from "hono/http-exception"; + +import { core_admin_permissions } from "@/database/admins"; +import { core_moderators_permissions } from "@/database/moderators"; +import { core_roles } from "@/database/roles"; +import { core_users_secondary_roles } from "@/database/users"; + +import type { + PermissionsStaffArgs, + PermissionStaffType, + StaffPermissionSet, +} from "./permission-staff"; + +import { hasStaffPermission, staffPermissionKey } from "./staff-permission"; + +const tableByType = { + admin: core_admin_permissions, + moderator: core_moderators_permissions, +} as const; + +export const getUserRoleIds = async ( + c: Context, + user: { id: number; roleId: number }, +): Promise => { + const secondary = await c + .get("db") + .select({ roleId: core_users_secondary_roles.roleId }) + .from(core_users_secondary_roles) + .where(eq(core_users_secondary_roles.userId, user.id)); + + return [...new Set([user.roleId, ...secondary.map(row => row.roleId)])]; +}; + +export const resolveStaffPermissions = async ( + c: Context, + { + type, + user, + }: { type: PermissionStaffType; user: { id: number; roleId: number } }, +): Promise => { + const roleIds = await getUserRoleIds(c, user); + + const rootRoles = await c + .get("db") + .select({ id: core_roles.id }) + .from(core_roles) + .where(and(inArray(core_roles.id, roleIds), eq(core_roles.root, true))) + .limit(1); + + if (rootRoles.length > 0) { + return { root: true, permissions: [] }; + } + + const table = tableByType[type]; + const entries = await c + .get("db") + .select({ data: table.data }) + .from(table) + .where(or(eq(table.userId, user.id), inArray(table.roleId, roleIds))); + + if (entries.some(entry => entry.data?.unrestricted)) { + return { root: true, permissions: [] }; + } + + const seen = new Set(); + const permissions: PermissionsStaffArgs[] = []; + for (const entry of entries) { + for (const permission of entry.data?.permissions ?? []) { + const key = staffPermissionKey(permission); + if (seen.has(key)) continue; + seen.add(key); + permissions.push(permission); + } + } + + return { root: false, permissions }; +}; + +export const checkStaffPermission = async ( + c: Context, + { type, ...args }: PermissionsStaffArgs & { type: PermissionStaffType }, +): Promise => { + const user = type === "admin" ? c.get("admin")?.user : c.get("user"); + if (!user) return false; + + const set = await resolveStaffPermissions(c, { type, user }); + + return hasStaffPermission(set, args); +}; + +export const assertStaffPermission = async ( + c: Context, + args: PermissionsStaffArgs & { type: PermissionStaffType }, +): Promise => { + const allowed = await checkStaffPermission(c, args); + if (!allowed) { + throw new HTTPException(403, { message: "Forbidden" }); + } +}; diff --git a/packages/vitnode/src/api/lib/permission-staff.ts b/packages/vitnode/src/api/lib/permission-staff.ts new file mode 100644 index 000000000..09a082719 --- /dev/null +++ b/packages/vitnode/src/api/lib/permission-staff.ts @@ -0,0 +1,103 @@ +/** + * Plugin-based staff permission catalog. + * + * A plugin declares, in its API config, which permissions exist for moderators + * and for admins. Permissions are grouped by module string, mirroring the way a + * plugin is split into modules elsewhere in the API. + * + * The i18n label for each permission lives in the plugin's locale file under the + * flat top-level key `{pluginId}:{module}:{permission}` (e.g. + * `@vitnode/blog:posts:can_delete`). + * + * A permission may be declared as a plain string, or as an object that lists the + * other permissions (in the same module) it `dependsOn`. The staff form only + * shows a permission once every permission it depends on is enabled — e.g. + * `{ permission: "can_create", dependsOn: ["can_view"] }` stays hidden until + * `can_view` is on. + */ +export type PermissionStaffEntryInput = + string | { dependsOn?: string[]; permission: string }; + +/** + * Author-facing module map — what a plugin declares in its API config. + */ +export type PermissionStaffModulesInput = Record< + string, + PermissionStaffEntryInput[] +>; + +/** + * A permission entry after normalization — the shape carried on the request + * context and returned by the permission catalog. `dependsOn` is always an + * array (empty when the permission has no prerequisites). + */ +export interface PermissionStaffEntry { + dependsOn: string[]; + permission: string; +} + +export type PermissionStaffModules = Record; + +export interface PermissionStaffConfig { + admin?: PermissionStaffModulesInput; + moderator?: PermissionStaffModulesInput; +} + +export type PermissionStaffType = "admin" | "moderator"; + +/** + * Normalizes the author-facing module map into the canonical + * `{ permission, dependsOn }` shape, so every downstream reader deals with a + * single shape regardless of how the plugin declared it. + */ +export const normalizePermissionStaffModules = ( + modules: PermissionStaffModulesInput = {}, +): PermissionStaffModules => + Object.fromEntries( + Object.entries(modules).map(([module, entries]) => [ + module, + entries.map(entry => + typeof entry === "string" + ? { permission: entry, dependsOn: [] } + : { permission: entry.permission, dependsOn: entry.dependsOn ?? [] }, + ), + ]), + ); + +/** + * A single granted permission stored against a staff entry. + */ +export interface PermissionsStaffArgs { + module: string; + permission: string; + plugin: string; +} + +/** + * The value persisted in a staff entry's `data` jsonb column. + * + * `unrestricted` grants every permission for the staff type (and any added in + * the future); when `false`, only the explicitly listed `permissions` apply. + */ +export interface StaffPermissionsData { + permissions: PermissionsStaffArgs[]; + unrestricted: boolean; +} + +/** + * The catalog as aggregated onto the request context (`c.get("core").permissionStaff`). + */ +export interface PermissionStaffCatalogEntry { + admin: PermissionStaffModules; + moderator: PermissionStaffModules; + pluginId: string; +} + +/** + * The resolved effective permissions for a user. `root` short-circuits every + * check to `true`. + */ +export interface StaffPermissionSet { + permissions: PermissionsStaffArgs[]; + root: boolean; +} diff --git a/packages/vitnode/src/api/lib/plugin.ts b/packages/vitnode/src/api/lib/plugin.ts index 3f256cb5f..eac1d7bc4 100644 --- a/packages/vitnode/src/api/lib/plugin.ts +++ b/packages/vitnode/src/api/lib/plugin.ts @@ -2,6 +2,7 @@ import { OpenAPIHono } from "@hono/zod-openapi"; import type { CronJobConfig } from "./cron"; import type { BuildModuleReturn } from "./module"; +import type { PermissionStaffConfig } from "./permission-staff"; import type { WebSocketConfig } from "./websocket"; import { checkPluginId } from "./check-plugin-id"; @@ -9,6 +10,7 @@ import { checkPluginId } from "./check-plugin-id"; export interface BuildPluginApiReturn { cronJobs?: Omit[]; hono: OpenAPIHono; + permissionStaff?: PermissionStaffConfig; pluginId: string; webSockets?: Omit[]; } @@ -16,8 +18,10 @@ export interface BuildPluginApiReturn { export function buildApiPlugin

({ pluginId, modules = [], + permissionStaff, }: { modules?: BuildModuleReturn[]; + permissionStaff?: PermissionStaffConfig; pluginId: P; }): BuildPluginApiReturn { // Run for checking if the plugin is valid @@ -43,5 +47,6 @@ export function buildApiPlugin

({ hono, cronJobs, webSockets, + permissionStaff, }; } diff --git a/packages/vitnode/src/api/lib/route.ts b/packages/vitnode/src/api/lib/route.ts index a080d7522..ba06ce27f 100644 --- a/packages/vitnode/src/api/lib/route.ts +++ b/packages/vitnode/src/api/lib/route.ts @@ -1,4 +1,5 @@ import type { RouteConfig, RouteHandler } from "@hono/zod-openapi"; +import type { MiddlewareHandler } from "hono"; import { createRoute as createRouteHono } from "@hono/zod-openapi"; @@ -7,6 +8,13 @@ import { type EnvVitNode, pluginMiddleware, } from "../middlewares/global.middleware"; +import { assertStaffPermission } from "./check-staff-permission"; + +export interface AdminStaffPermission { + module: string; + permission: string; + plugin?: string; +} export const buildRoute = < Plugin extends string, @@ -19,7 +27,9 @@ export const buildRoute = < route, handler, pluginId, + adminStaffPermission, }: { + adminStaffPermission?: AdminStaffPermission; handler: RouteHandler; pluginId: Plugin; route: R; @@ -31,18 +41,35 @@ export const buildRoute = < const tags = [pluginTag, ...(route.tags ?? [])]; + const middleware: MiddlewareHandler[] = [pluginMiddleware(pluginId)]; + + if (adminStaffPermission) { + const { plugin, module, permission } = adminStaffPermission; + middleware.push(async (c, next) => { + await assertStaffPermission(c, { + type: "admin", + plugin: plugin ?? pluginId, + module, + permission, + }); + await next(); + }); + } + + if (route.withCaptcha) { + middleware.push(captchaMiddleware()); + } + + if (Array.isArray(route.middleware)) { + middleware.push(...route.middleware); + } else if (route.middleware) { + middleware.push(route.middleware); + } + return { route: createRouteHono({ tags, - middleware: [ - pluginMiddleware(pluginId), - ...(route.withCaptcha ? [captchaMiddleware()] : []), - ...(Array.isArray(route.middleware) - ? route.middleware - : route.middleware - ? [route.middleware] - : []), - ], + middleware, ...route, }), handler: handler as Route["handler"], diff --git a/packages/vitnode/src/api/lib/staff-permission.ts b/packages/vitnode/src/api/lib/staff-permission.ts new file mode 100644 index 000000000..85db5b927 --- /dev/null +++ b/packages/vitnode/src/api/lib/staff-permission.ts @@ -0,0 +1,24 @@ +import type { + PermissionsStaffArgs, + StaffPermissionSet, +} from "./permission-staff"; + +export const hasStaffPermission = ( + set: StaffPermissionSet, + args: PermissionsStaffArgs, +): boolean => { + if (set.root) return true; + + return set.permissions.some( + permission => + permission.plugin === args.plugin && + permission.module === args.module && + permission.permission === args.permission, + ); +}; + +export const staffPermissionKey = ({ + plugin, + module, + permission, +}: PermissionsStaffArgs): string => `${plugin}:${module}:${permission}`; diff --git a/packages/vitnode/src/api/middlewares/global.middleware.ts b/packages/vitnode/src/api/middlewares/global.middleware.ts index 229c70ff3..7c7a1532e 100644 --- a/packages/vitnode/src/api/middlewares/global.middleware.ts +++ b/packages/vitnode/src/api/middlewares/global.middleware.ts @@ -12,6 +12,7 @@ import { CONFIG } from "@/lib/config"; import { realtime } from "@/ws/registry"; import type { BuildCronReturn } from "../lib/cron"; +import type { PermissionStaffCatalogEntry } from "../lib/permission-staff"; import type { WebSocketConfig } from "../lib/websocket"; import type { SSOApiPlugin } from "../models/sso"; @@ -19,6 +20,7 @@ import { loggerMiddleware, type LoggerMiddlewareType, } from "../lib/logger-middleware"; +import { normalizePermissionStaffModules } from "../lib/permission-staff"; declare module "hono" { // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -64,6 +66,7 @@ export interface EnvVariablesVitNode { title: string; }; pathToMessages: (path: string) => Promise<{ default: object }>; + permissionStaff: PermissionStaffCatalogEntry[]; plugins: { id: string }[]; webSockets: WebSocketConfig[]; }; @@ -131,6 +134,16 @@ export const globalMiddleware = ({ })), ); + const permissionStaffMetadata: PermissionStaffCatalogEntry[] = plugins.map( + plugin => ({ + pluginId: plugin.pluginId, + admin: normalizePermissionStaffModules(plugin.permissionStaff?.admin), + moderator: normalizePermissionStaffModules( + plugin.permissionStaff?.moderator, + ), + }), + ); + const ipHeaderKeys = [ "x-forwarded-for", "x-real-ip", @@ -190,6 +203,7 @@ export const globalMiddleware = ({ plugins: pluginsMetadata, cron: cronMetadata, webSockets: webSocketsMetadata, + permissionStaff: permissionStaffMetadata, }); const user = await new SessionModel(c).getUser(); diff --git a/packages/vitnode/src/api/modules/admin/routes/session.route.ts b/packages/vitnode/src/api/modules/admin/routes/session.route.ts index 0a48349d3..6ab9ec763 100644 --- a/packages/vitnode/src/api/modules/admin/routes/session.route.ts +++ b/packages/vitnode/src/api/modules/admin/routes/session.route.ts @@ -1,6 +1,7 @@ import { HTTPException } from "hono/http-exception"; import { z } from "zod"; +import { resolveStaffPermissions } from "@/api/lib/check-staff-permission"; import { buildRoute } from "@/api/lib/route"; import { CONFIG_PLUGIN } from "@/config"; @@ -27,6 +28,16 @@ export const sessionAdminRoute = buildRoute({ roleId: z.number(), birthday: z.date().nullable(), }), + permissions: z.object({ + root: z.boolean(), + permissions: z.array( + z.object({ + plugin: z.string(), + module: z.string(), + permission: z.string(), + }), + ), + }), vitnode_version: z.string(), }), }, @@ -38,12 +49,18 @@ export const sessionAdminRoute = buildRoute({ }, }, }, - handler: c => { + handler: async c => { const user = c.get("admin")?.user; if (!user) throw new HTTPException(403); + const permissions = await resolveStaffPermissions(c, { + type: "admin", + user, + }); + return c.json({ user, + permissions, vitnode_version: CONFIG_PLUGIN.version, }); }, diff --git a/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts b/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts index 6c60873d7..c7eb9eb65 100644 --- a/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts +++ b/packages/vitnode/src/api/modules/admin/staff/lib/resolve-staff-edges.ts @@ -2,19 +2,27 @@ import type { Context } from "hono"; import { eq, inArray } from "drizzle-orm"; +import { getUserRoleIds } from "@/api/lib/check-staff-permission"; import { resolveRoleNames } from "@/api/lib/resolve-role-names"; import { core_roles } from "@/database/roles"; import { core_users } from "@/database/users"; interface RawStaffEdge { createdAt: Date; + data?: null | { unrestricted: boolean }; id: number; + protected: boolean; roleId: null | number; updatedAt: Date; userId: null | number; } export const resolveStaffEdges = async (c: Context, edges: RawStaffEdge[]) => { + const currentUser = c.get("admin")?.user; + const currentUserRoleIds = currentUser + ? new Set(await getUserRoleIds(c, currentUser)) + : new Set(); + const entryRoleIds = edges .map(edge => edge.roleId) .filter((id): id is number => id != null); @@ -55,10 +63,18 @@ export const resolveStaffEdges = async (c: Context, edges: RawStaffEdge[]) => { const entryRole = entryRoles.find(role => role.id === edge.roleId); const user = users.find(item => item.id === edge.userId); + const self = + currentUser != null && + ((edge.userId != null && edge.userId === currentUser.id) || + (edge.roleId != null && currentUserRoleIds.has(edge.roleId))); + return { id: edge.id, createdAt: edge.createdAt, updatedAt: edge.updatedAt, + unrestricted: edge.data?.unrestricted ?? false, + protected: edge.protected, + self, role: entryRole ? { id: entryRole.id, diff --git a/packages/vitnode/src/api/modules/admin/staff/lib/schema.ts b/packages/vitnode/src/api/modules/admin/staff/lib/schema.ts index 6c2a5ff20..df1bc164a 100644 --- a/packages/vitnode/src/api/modules/admin/staff/lib/schema.ts +++ b/packages/vitnode/src/api/modules/admin/staff/lib/schema.ts @@ -14,7 +14,7 @@ export const staffListAdminQuery = zodPaginationQuery.extend({ // A role reference resolved for the `RoleFormat` component (it picks the active // locale from the translated names on the frontend). -const staffRoleSchema = z.object({ +export const staffRoleSchema = z.object({ id: z.number(), color: z.string().nullable(), name: z.array( @@ -25,25 +25,42 @@ const staffRoleSchema = z.object({ ), }); -export const staffListAdminSchema = z.object({ - edges: z.array( - z.object({ +// A single resolved staff entry — the shape returned by `resolveStaffEdges`. +export const staffEntrySchema = z.object({ + id: z.number(), + createdAt: z.date(), + updatedAt: z.date(), + unrestricted: z.boolean(), + protected: z.boolean(), + self: z.boolean(), + role: staffRoleSchema.nullable(), + user: z + .object({ id: z.number(), - createdAt: z.date(), - updatedAt: z.date(), - // A staff entry grants permissions to a whole role... - role: staffRoleSchema.nullable(), - // ...or to a single user (rendered with their own role formatting). - user: z - .object({ - id: z.number(), - name: z.string(), - nameCode: z.string(), - avatarColor: z.string(), - role: staffRoleSchema, - }) - .nullable(), - }), - ), + name: z.string(), + nameCode: z.string(), + avatarColor: z.string(), + role: staffRoleSchema, + }) + .nullable(), +}); + +export const staffListAdminSchema = z.object({ + edges: z.array(staffEntrySchema), pageInfo: zodPaginationPageInfo, }); + +export const permissionsStaffArgsSchema = z.object({ + plugin: z.string(), + module: z.string(), + permission: z.string(), +}); + +export const staffTypeSchema = z.enum(["admin", "moderator"]); + +// Maps a staff entry type to the admin permission-catalog module that gates +// managing it, so moderator and administrator staff can be governed separately. +export const staffPermissionModuleByType = { + admin: "staff_admins", + moderator: "staff_moderators", +} as const; diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts index cac5e4797..d98cb5184 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/admins.route.ts @@ -8,6 +8,7 @@ import { staffListAdminQuery, staffListAdminSchema } from "../lib/schema"; export const listAdminsStaffAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "staff_admins", permission: "can_view" }, route: { method: "get", description: "Get list of administrators staff (Admin only)", @@ -46,6 +47,8 @@ export const listAdminsStaffAdminRoute = buildRoute({ userId: core_admin_permissions.userId, createdAt: core_admin_permissions.createdAt, updatedAt: core_admin_permissions.updatedAt, + data: core_admin_permissions.data, + protected: core_admin_permissions.protected, }) .from(core_admin_permissions) .where(where) diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts new file mode 100644 index 000000000..e1a5b82fb --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/staff/routes/create.route.ts @@ -0,0 +1,107 @@ +import { z } from "@hono/zod-openapi"; +import { eq, or } from "drizzle-orm"; + +import { assertStaffPermission } from "@/api/lib/check-staff-permission"; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_admin_permissions } from "@/database/admins"; +import { core_moderators_permissions } from "@/database/moderators"; + +import { staffPermissionModuleByType, staffTypeSchema } from "../lib/schema"; + +const tableByType = { + admin: core_admin_permissions, + moderator: core_moderators_permissions, +} as const; + +export const createStaffAdminRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "post", + description: "Create a staff entry for a role or a user (Admin only)", + path: "/entry/{type}", + request: { + params: z.object({ + type: staffTypeSchema, + }), + body: { + content: { + "application/json": { + schema: z + .object({ + roleId: z.number().nullable().optional(), + userId: z.number().nullable().optional(), + }) + // Exactly one of role/user must be provided. + .refine( + value => Boolean(value.roleId) !== Boolean(value.userId), + { message: "Provide exactly one of roleId or userId" }, + ), + }, + }, + }, + }, + responses: { + 201: { + content: { + "application/json": { + schema: z.object({ id: z.number() }), + }, + }, + description: "Staff entry created", + }, + 403: { + description: "Access Denied", + }, + 409: { + content: { + "application/json": { + schema: z.object({ error: z.string() }), + }, + }, + description: "Staff entry already exists", + }, + }, + }, + handler: async c => { + const { type } = c.req.valid("param"); + await assertStaffPermission(c, { + type: "admin", + plugin: CONFIG_PLUGIN.pluginId, + module: staffPermissionModuleByType[type], + permission: "can_create", + }); + + const { roleId, userId } = c.req.valid("json"); + const table = tableByType[type]; + + // Prevent assigning the same role/user twice. + const [existing] = await c + .get("db") + .select({ id: table.id }) + .from(table) + .where( + or( + roleId ? eq(table.roleId, roleId) : undefined, + userId ? eq(table.userId, userId) : undefined, + ), + ) + .limit(1); + + if (existing) { + return c.json({ error: "Staff entry already exists" }, 409); + } + + const [created] = await c + .get("db") + .insert(table) + .values({ + roleId: roleId ?? null, + userId: userId ?? null, + data: { unrestricted: false, permissions: [] }, + }) + .returning({ id: table.id }); + + return c.json({ id: created.id }, 201); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts new file mode 100644 index 000000000..4f394f6a9 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/staff/routes/delete.route.ts @@ -0,0 +1,104 @@ +import { z } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; +import { HTTPException } from "hono/http-exception"; + +import { + assertStaffPermission, + getUserRoleIds, +} from "@/api/lib/check-staff-permission"; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_admin_permissions } from "@/database/admins"; +import { core_moderators_permissions } from "@/database/moderators"; + +import { staffPermissionModuleByType, staffTypeSchema } from "../lib/schema"; + +const tableByType = { + admin: core_admin_permissions, + moderator: core_moderators_permissions, +} as const; + +export const deleteStaffAdminRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "delete", + description: "Remove a staff entry (Admin only)", + path: "/entry/{type}/{id}", + request: { + params: z.object({ + type: staffTypeSchema, + id: z.string().openapi({ example: "1" }), + }), + }, + responses: { + 200: { + description: "Staff entry removed", + }, + 403: { + description: "Access Denied", + }, + 404: { + content: { + "application/json": { + schema: z.object({ error: z.string() }), + }, + }, + description: "Staff entry not found", + }, + }, + }, + handler: async c => { + const { type, id } = c.req.valid("param"); + await assertStaffPermission(c, { + type: "admin", + plugin: CONFIG_PLUGIN.pluginId, + module: staffPermissionModuleByType[type], + permission: "can_delete", + }); + + const entryId = Number(id); + if (!Number.isInteger(entryId)) { + return c.json({ error: "Staff entry not found" }, 404); + } + + const table = tableByType[type]; + const [entry] = await c + .get("db") + .select({ + protected: table.protected, + userId: table.userId, + roleId: table.roleId, + }) + .from(table) + .where(eq(table.id, entryId)) + .limit(1); + + if (!entry) { + return c.json({ error: "Staff entry not found" }, 404); + } + // Protected entries are managed by the system and cannot be removed. + if (entry.protected) { + throw new HTTPException(403, { message: "Forbidden" }); + } + + // An admin cannot remove the entry that governs their own access — their own + // user entry or an entry for any role they belong to (primary or secondary). + const currentUser = c.get("admin")?.user; + const currentUserRoleIds = currentUser + ? await getUserRoleIds(c, currentUser) + : []; + const isSelf = + currentUser != null && + ((entry.userId != null && entry.userId === currentUser.id) || + (entry.roleId != null && currentUserRoleIds.includes(entry.roleId))); + if (isSelf) { + throw new HTTPException(403, { + message: "You cannot remove your own staff permissions.", + }); + } + + await c.get("db").delete(table).where(eq(table.id, entryId)); + + return c.body(null, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts index 455053ff1..c6a908ea9 100644 --- a/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts +++ b/packages/vitnode/src/api/modules/admin/staff/routes/moderators.route.ts @@ -8,6 +8,7 @@ import { staffListAdminQuery, staffListAdminSchema } from "../lib/schema"; export const listModeratorsStaffAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "staff_moderators", permission: "can_view" }, route: { method: "get", description: "Get list of moderators staff (Admin only)", @@ -46,6 +47,8 @@ export const listModeratorsStaffAdminRoute = buildRoute({ userId: core_moderators_permissions.userId, createdAt: core_moderators_permissions.createdAt, updatedAt: core_moderators_permissions.updatedAt, + data: core_moderators_permissions.data, + protected: core_moderators_permissions.protected, }) .from(core_moderators_permissions) .where(where) diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts new file mode 100644 index 000000000..98e84721f --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/staff/routes/permission-catalog.route.ts @@ -0,0 +1,46 @@ +import { z } from "@hono/zod-openapi"; + +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; + +const permissionStaffModulesSchema = z.record( + z.string(), + z.array( + z.object({ + permission: z.string(), + dependsOn: z.array(z.string()), + }), + ), +); + +export const permissionCatalogStaffAdminRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "get", + description: + "Get the staff permission catalog declared by every plugin (Admin only)", + path: "/permission-catalog", + responses: { + 200: { + content: { + "application/json": { + schema: z.array( + z.object({ + pluginId: z.string(), + admin: permissionStaffModulesSchema, + moderator: permissionStaffModulesSchema, + }), + ), + }, + }, + description: "Staff permission catalog", + }, + 403: { + description: "Access Denied", + }, + }, + }, + handler: c => { + return c.json(c.get("core").permissionStaff, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts new file mode 100644 index 000000000..5a484b514 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/staff/routes/show-permissions.route.ts @@ -0,0 +1,104 @@ +import { z } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; + +import { assertStaffPermission } from "@/api/lib/check-staff-permission"; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_admin_permissions } from "@/database/admins"; +import { core_moderators_permissions } from "@/database/moderators"; + +import { resolveStaffEdges } from "../lib/resolve-staff-edges"; +import { + permissionsStaffArgsSchema, + staffEntrySchema, + staffPermissionModuleByType, + staffTypeSchema, +} from "../lib/schema"; + +const tableByType = { + admin: core_admin_permissions, + moderator: core_moderators_permissions, +} as const; + +export const showPermissionsStaffAdminRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "get", + description: + "Get a single staff entry with its granted permissions (Admin only)", + path: "/entry/{type}/{id}", + request: { + params: z.object({ + type: staffTypeSchema, + id: z.string().openapi({ example: "1" }), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: staffEntrySchema.extend({ + permissions: z.array(permissionsStaffArgsSchema), + }), + }, + }, + description: "Staff entry", + }, + 403: { + description: "Access Denied", + }, + 404: { + content: { + "application/json": { + schema: z.object({ error: z.string() }), + }, + }, + description: "Staff entry not found", + }, + }, + }, + handler: async c => { + const { type, id } = c.req.valid("param"); + await assertStaffPermission(c, { + type: "admin", + plugin: CONFIG_PLUGIN.pluginId, + module: staffPermissionModuleByType[type], + permission: "can_edit", + }); + + const entryId = Number(id); + if (!Number.isInteger(entryId)) { + return c.json({ error: "Staff entry not found" }, 404); + } + + const table = tableByType[type]; + const [entry] = await c + .get("db") + .select({ + id: table.id, + roleId: table.roleId, + userId: table.userId, + createdAt: table.createdAt, + updatedAt: table.updatedAt, + data: table.data, + protected: table.protected, + }) + .from(table) + .where(eq(table.id, entryId)) + .limit(1); + + if (!entry) { + return c.json({ error: "Staff entry not found" }, 404); + } + + const [resolved] = await resolveStaffEdges(c, [entry]); + + return c.json( + { + ...resolved, + permissions: entry.data?.permissions ?? [], + }, + 200, + ); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts b/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts new file mode 100644 index 000000000..002515b4d --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/staff/routes/update-permissions.route.ts @@ -0,0 +1,199 @@ +import { z } from "@hono/zod-openapi"; +import { eq } from "drizzle-orm"; +import { HTTPException } from "hono/http-exception"; + +import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; + +import { + assertStaffPermission, + getUserRoleIds, +} from "@/api/lib/check-staff-permission"; +import { buildRoute } from "@/api/lib/route"; +import { staffPermissionKey } from "@/api/lib/staff-permission"; +import { CONFIG_PLUGIN } from "@/config"; +import { core_admin_permissions } from "@/database/admins"; +import { core_moderators_permissions } from "@/database/moderators"; + +import { + permissionsStaffArgsSchema, + staffPermissionModuleByType, + staffTypeSchema, +} from "../lib/schema"; + +const tableByType = { + admin: core_admin_permissions, + moderator: core_moderators_permissions, +} as const; + +export const updatePermissionsStaffAdminRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "patch", + description: "Update the granted permissions of a staff entry (Admin only)", + path: "/entry/{type}/{id}", + request: { + params: z.object({ + type: staffTypeSchema, + id: z.string().openapi({ example: "1" }), + }), + body: { + content: { + "application/json": { + schema: z.object({ + unrestricted: z.boolean(), + permissions: z.array(permissionsStaffArgsSchema), + }), + }, + }, + }, + }, + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + unrestricted: z.boolean(), + permissions: z.array(permissionsStaffArgsSchema), + }), + }, + }, + description: "Updated permissions", + }, + 403: { + description: "Access Denied", + }, + 404: { + content: { + "application/json": { + schema: z.object({ error: z.string() }), + }, + }, + description: "Staff entry not found", + }, + }, + }, + handler: async c => { + const { type, id } = c.req.valid("param"); + await assertStaffPermission(c, { + type: "admin", + plugin: CONFIG_PLUGIN.pluginId, + module: staffPermissionModuleByType[type], + permission: "can_edit", + }); + + const { unrestricted, permissions } = c.req.valid("json"); + + const entryId = Number(id); + if (!Number.isInteger(entryId)) { + return c.json({ error: "Staff entry not found" }, 404); + } + + const table = tableByType[type]; + + const [entry] = await c + .get("db") + .select({ + protected: table.protected, + userId: table.userId, + roleId: table.roleId, + }) + .from(table) + .where(eq(table.id, entryId)) + .limit(1); + + if (!entry) { + return c.json({ error: "Staff entry not found" }, 404); + } + // Protected entries are managed by the system and cannot be edited. + if (entry.protected) { + throw new HTTPException(403, { message: "Forbidden" }); + } + + // An admin cannot edit the entry that governs their own access — their own + // user entry or an entry for any role they belong to (primary or + // secondary). This stops them from escalating their own permissions. + const currentUser = c.get("admin")?.user; + const currentUserRoleIds = currentUser + ? await getUserRoleIds(c, currentUser) + : []; + const isSelf = + currentUser != null && + ((entry.userId != null && entry.userId === currentUser.id) || + (entry.roleId != null && currentUserRoleIds.includes(entry.roleId))); + if (isSelf) { + throw new HTTPException(403, { + message: "You cannot edit your own staff permissions.", + }); + } + + // Only persist permissions that actually exist in the catalog for this + // staff type — silently drops anything unknown/forged. Also record each + // permission's dependencies (the keys of the permissions it `dependsOn` + // within the same module) so we can drop grants whose gate is missing. + const allowed = new Set(); + const dependencies = new Map(); + for (const plugin of c.get("core").permissionStaff) { + for (const [module, modulePermissions] of Object.entries(plugin[type])) { + for (const entry of modulePermissions) { + const key = staffPermissionKey({ + plugin: plugin.pluginId, + module, + permission: entry.permission, + }); + allowed.add(key); + dependencies.set( + key, + entry.dependsOn.map(dependency => + staffPermissionKey({ + plugin: plugin.pluginId, + module, + permission: dependency, + }), + ), + ); + } + } + } + + const seen = new Set(); + const granted = new Map(); + // When unrestricted, the explicit list is irrelevant — store none. + if (!unrestricted) { + for (const permission of permissions) { + const key = staffPermissionKey(permission); + if (!allowed.has(key) || seen.has(key)) continue; + seen.add(key); + granted.set(key, permission); + } + + // Drop any granted permission whose dependencies aren't all granted too, + // repeating until stable so a broken chain (a → b → c) collapses fully. + let changed = true; + while (changed) { + changed = false; + for (const key of granted.keys()) { + const deps = dependencies.get(key) ?? []; + if (deps.some(dependency => !granted.has(dependency))) { + granted.delete(key); + changed = true; + } + } + } + } + + const sanitized: PermissionsStaffArgs[] = [...granted.values()]; + + const [updated] = await c + .get("db") + .update(table) + .set({ data: { unrestricted, permissions: sanitized } }) + .where(eq(table.id, entryId)) + .returning({ id: table.id }); + + if (!updated) { + return c.json({ error: "Staff entry not found" }, 404); + } + + return c.json({ unrestricted, permissions: sanitized }, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts b/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts index fb58b6309..7b66659c2 100644 --- a/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts +++ b/packages/vitnode/src/api/modules/admin/staff/staff.admin.module.ts @@ -2,10 +2,23 @@ import { buildModule } from "@/api/lib/module"; import { CONFIG_PLUGIN } from "@/config"; import { listAdminsStaffAdminRoute } from "./routes/admins.route"; +import { createStaffAdminRoute } from "./routes/create.route"; +import { deleteStaffAdminRoute } from "./routes/delete.route"; import { listModeratorsStaffAdminRoute } from "./routes/moderators.route"; +import { permissionCatalogStaffAdminRoute } from "./routes/permission-catalog.route"; +import { showPermissionsStaffAdminRoute } from "./routes/show-permissions.route"; +import { updatePermissionsStaffAdminRoute } from "./routes/update-permissions.route"; export const staffAdminModule = buildModule({ pluginId: CONFIG_PLUGIN.pluginId, name: "staff", - routes: [listModeratorsStaffAdminRoute, listAdminsStaffAdminRoute], + routes: [ + listModeratorsStaffAdminRoute, + listAdminsStaffAdminRoute, + permissionCatalogStaffAdminRoute, + showPermissionsStaffAdminRoute, + updatePermissionsStaffAdminRoute, + createStaffAdminRoute, + deleteStaffAdminRoute, + ], }); diff --git a/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts b/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts new file mode 100644 index 000000000..301342c07 --- /dev/null +++ b/packages/vitnode/src/api/modules/admin/users/lib/assert-edit-user-permission.ts @@ -0,0 +1,22 @@ +import type { Context } from "hono"; + +import { assertStaffPermission } from "@/api/lib/check-staff-permission"; +import { SessionAdminModel } from "@/api/models/session-admin"; +import { CONFIG_PLUGIN } from "@/config"; + +export const assertCanEditAdminTarget = async ( + c: Context, + userId: number, +): Promise => { + const isTargetAdmin = await new SessionAdminModel(c).checkIfUserIsAdmin( + userId, + ); + if (!isTargetAdmin) return; + + await assertStaffPermission(c, { + type: "admin", + plugin: CONFIG_PLUGIN.pluginId, + module: "users", + permission: "can_edit_admin", + }); +}; diff --git a/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts index ecb8041d7..0ea5110b7 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/create.route.ts @@ -25,6 +25,7 @@ export const zodCreateUserAdminSchema = z.object({ export const createUserAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "users", permission: "can_create" }, route: { method: "post", description: "Create a new user (Admin only)", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts index 0557e5da5..2bde26cce 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/list.route.ts @@ -14,6 +14,7 @@ import { core_users, core_users_secondary_roles } from "@/database/users"; export const listUsersAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", description: "Get list of all users", diff --git a/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts index 43b4a47e0..62cc90dd1 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/show.route.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { resolveRoleNames } from "@/api/lib/resolve-role-names"; import { buildRoute } from "@/api/lib/route"; +import { SessionAdminModel } from "@/api/models/session-admin"; import { UserModel } from "@/api/models/user"; import { CONFIG_PLUGIN } from "@/config"; import { core_roles } from "@/database/roles"; @@ -21,6 +22,7 @@ const roleSchema = z.object({ export const showUserAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "users", permission: "can_view" }, route: { method: "get", description: "Get a single user by id (Admin only)", @@ -48,6 +50,7 @@ export const showUserAdminRoute = buildRoute({ secondaryRoles: z.array(roleSchema), birthday: z.date().nullable(), language: z.string(), + isAdmin: z.boolean(), }), }, }, @@ -108,9 +111,15 @@ export const showUserAdminRoute = buildRoute({ ...secondaryRoleRows.map(role => role.id), ]); + // Whether the listed user is themselves an administrator. The frontend uses + // this to require the elevated `can_edit_admin` permission before showing + // edit controls (the backend enforces the same rule on write). + const isAdmin = await new SessionAdminModel(c).checkIfUserIsAdmin(user.id); + return c.json( { ...user, + isAdmin, role: { id: user.roleId, color: primaryRole?.color ?? null, diff --git a/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts index 4a2c36366..29eeb5793 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/update.route.ts @@ -6,6 +6,8 @@ import { CONFIG_PLUGIN } from "@/config"; import { core_roles } from "@/database/roles"; import { core_users, core_users_secondary_roles } from "@/database/users"; +import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; + const nameRegex = /^(?!.* {2})[\p{L}\p{N}._@ -]*$/u; export const zodUpdateUserAdminSchema = z @@ -38,6 +40,7 @@ export const zodUpdateUserAdminSchema = z export const updateUserAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "patch", description: "Update a user's name or email by id (Admin only)", @@ -118,6 +121,8 @@ export const updateUserAdminRoute = buildRoute({ return c.json({ error: "User not found" }, 404); } + await assertCanEditAdminTarget(c, userId); + const values: Partial = {}; if (body.email !== undefined) { diff --git a/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts b/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts index 5e73a915b..a7f9b63cf 100644 --- a/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts +++ b/packages/vitnode/src/api/modules/admin/users/routes/verify-email.route.ts @@ -5,8 +5,11 @@ import { buildRoute } from "@/api/lib/route"; import { CONFIG_PLUGIN } from "@/config"; import { core_users } from "@/database/users"; +import { assertCanEditAdminTarget } from "../lib/assert-edit-user-permission"; + export const verifyEmailUserAdminRoute = buildRoute({ pluginId: CONFIG_PLUGIN.pluginId, + adminStaffPermission: { module: "users", permission: "can_edit" }, route: { method: "post", description: "Verify a user's email by id (Admin only)", @@ -50,20 +53,29 @@ export const verifyEmailUserAdminRoute = buildRoute({ return c.json({ error: "User not found" }, 404); } - const [updated] = await c - .get("db") + const db = c.get("db"); + + const [user] = await db + .select({ id: core_users.id }) + .from(core_users) + .where(eq(core_users.id, userId)) + .limit(1); + + if (!user) { + return c.json({ error: "User not found" }, 404); + } + + await assertCanEditAdminTarget(c, userId); + + const [updated] = await db .update(core_users) .set({ emailVerified: true }) - .where(eq(core_users.id, userId)) + .where(eq(core_users.id, user.id)) .returning({ name: core_users.name, emailVerified: core_users.emailVerified, }); - if (!updated) { - return c.json({ error: "User not found" }, 404); - } - return c.json( { name: updated.name, emailVerified: updated.emailVerified }, 200, diff --git a/packages/vitnode/src/api/modules/users/routes/permissions.route.ts b/packages/vitnode/src/api/modules/users/routes/permissions.route.ts new file mode 100644 index 000000000..213d475df --- /dev/null +++ b/packages/vitnode/src/api/modules/users/routes/permissions.route.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +import { resolveStaffPermissions } from "@/api/lib/check-staff-permission"; +import { buildRoute } from "@/api/lib/route"; +import { CONFIG_PLUGIN } from "@/config"; + +export const permissionsRoute = buildRoute({ + pluginId: CONFIG_PLUGIN.pluginId, + route: { + method: "get", + description: + "Get the current user's effective moderator permissions (public site)", + path: "/permissions", + responses: { + 200: { + content: { + "application/json": { + schema: z.object({ + root: z.boolean(), + permissions: z.array( + z.object({ + plugin: z.string(), + module: z.string(), + permission: z.string(), + }), + ), + }), + }, + }, + description: "Effective moderator permissions", + }, + }, + }, + handler: async c => { + const user = c.get("user"); + if (!user) { + return c.json({ root: false, permissions: [] }, 200); + } + + const permissions = await resolveStaffPermissions(c, { + type: "moderator", + user, + }); + + return c.json(permissions, 200); + }, +}); diff --git a/packages/vitnode/src/api/modules/users/users.module.ts b/packages/vitnode/src/api/modules/users/users.module.ts index 9c1fc6ec6..0af6965ac 100644 --- a/packages/vitnode/src/api/modules/users/users.module.ts +++ b/packages/vitnode/src/api/modules/users/users.module.ts @@ -2,6 +2,7 @@ import { buildModule } from "@/api/lib/module"; import { CONFIG_PLUGIN } from "@/config"; import { changePasswordRoute } from "./routes/change-password.route"; +import { permissionsRoute } from "./routes/permissions.route"; import { resetPasswordRoute } from "./routes/reset-passowrd.route"; import { sessionRoute } from "./routes/session.route"; import { signInRoute } from "./routes/sign-in.route"; @@ -21,6 +22,7 @@ export const usersModule = buildModule({ testRoute, resetPasswordRoute, changePasswordRoute, + permissionsRoute, ], modules: [ssoUserModule], }); diff --git a/packages/vitnode/src/api/plugin.ts b/packages/vitnode/src/api/plugin.ts index 12c04d132..de772a6da 100644 --- a/packages/vitnode/src/api/plugin.ts +++ b/packages/vitnode/src/api/plugin.ts @@ -9,4 +9,34 @@ import { usersModule } from "./modules/users/users.module"; export const newBuildPluginApiCore = buildApiPlugin({ pluginId: CONFIG_PLUGIN.pluginId, modules: [middlewareModule, usersModule, adminModule, cronModule], + permissionStaff: { + moderator: { + users: ["can_edit"], + }, + admin: { + users: [ + "can_view", + { permission: "can_create", dependsOn: ["can_view"] }, + { permission: "can_edit", dependsOn: ["can_view"] }, + { permission: "can_edit_admin", dependsOn: ["can_view"] }, + ], + roles: ["can_manage"], + debug: [ + "can_view", + { permission: "can_clear_cache", dependsOn: ["can_view"] }, + ], + staff_moderators: [ + "can_view", + { permission: "can_create", dependsOn: ["can_view"] }, + { permission: "can_edit", dependsOn: ["can_view"] }, + { permission: "can_delete", dependsOn: ["can_view"] }, + ], + staff_admins: [ + "can_view", + { permission: "can_create", dependsOn: ["can_view"] }, + { permission: "can_edit", dependsOn: ["can_view"] }, + { permission: "can_delete", dependsOn: ["can_view"] }, + ], + }, + }, }); diff --git a/packages/vitnode/src/components/staff-permission/provider.tsx b/packages/vitnode/src/components/staff-permission/provider.tsx new file mode 100644 index 000000000..95055184a --- /dev/null +++ b/packages/vitnode/src/components/staff-permission/provider.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; + +import type { + PermissionsStaffArgs, + StaffPermissionSet, +} from "@/api/lib/permission-staff"; + +import { hasStaffPermission } from "@/api/lib/staff-permission"; + +const AdminStaffPermissionContext = React.createContext({ + root: false, + permissions: [], +}); + +/** + * Makes the current **admin's** effective permissions available to client + * components. Rendered once near the top of the admin layout. + */ +export const AdminStaffPermissionProvider = ({ + value, + children, +}: { + children: React.ReactNode; + value: StaffPermissionSet; +}) => ( + + {children} + +); + +/** Returns the current admin's raw effective permission set. */ +export const useAdminStaffPermissions = (): StaffPermissionSet => + React.use(AdminStaffPermissionContext); + +/** Returns whether the current admin holds a given permission. */ +export const useAdminStaffPermission = ( + args: PermissionsStaffArgs, +): boolean => { + const set = useAdminStaffPermissions(); + + return hasStaffPermission(set, args); +}; + +/** + * Renders `children` only when the current admin holds the given permission, + * otherwise `fallback` (defaults to nothing). + */ +export const AdminStaffPermissionGate = ({ + plugin, + module, + permission, + children, + fallback = null, +}: PermissionsStaffArgs & { + children: React.ReactNode; + fallback?: React.ReactNode; +}) => { + const allowed = useAdminStaffPermission({ plugin, module, permission }); + + return <>{allowed ? children : fallback}; +}; diff --git a/packages/vitnode/src/components/theme-provider.tsx b/packages/vitnode/src/components/theme-provider.tsx index 94356b1e8..b1a3986c9 100644 --- a/packages/vitnode/src/components/theme-provider.tsx +++ b/packages/vitnode/src/components/theme-provider.tsx @@ -1,7 +1,7 @@ "use client"; import { useServerInsertedHTML } from "next/navigation"; -import * as React from "react"; +import React from "react"; const MEDIA = "(prefers-color-scheme: dark)"; const colorSchemes = ["light", "dark"]; diff --git a/packages/vitnode/src/components/ui/accordion.tsx b/packages/vitnode/src/components/ui/accordion.tsx index 2753fc0aa..c360dbcb9 100644 --- a/packages/vitnode/src/components/ui/accordion.tsx +++ b/packages/vitnode/src/components/ui/accordion.tsx @@ -2,7 +2,7 @@ import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import * as React from "react"; +import React from "react"; import { cn } from "@/lib/utils"; diff --git a/packages/vitnode/src/components/ui/alert-dialog.tsx b/packages/vitnode/src/components/ui/alert-dialog.tsx index bd653acf3..d80c4b729 100644 --- a/packages/vitnode/src/components/ui/alert-dialog.tsx +++ b/packages/vitnode/src/components/ui/alert-dialog.tsx @@ -67,7 +67,7 @@ function AlertDialogOverlay({ return ( new Date()), protected: t.boolean().notNull().default(false), - // data: t.jsonb().$type<{ permissions: PermissionsStaffArgs[] }>().default({ - // permissions: [], - // }), + data: t + .jsonb() + .$type() + .notNull() + .default({ unrestricted: false, permissions: [] }), }), t => [ index("core_admin_permissions_role_id_idx").on(t.roleId), diff --git a/packages/vitnode/src/database/moderators.ts b/packages/vitnode/src/database/moderators.ts index 9e2f22e35..cc85ef3cb 100644 --- a/packages/vitnode/src/database/moderators.ts +++ b/packages/vitnode/src/database/moderators.ts @@ -1,6 +1,8 @@ import { relations } from "drizzle-orm"; import { index, pgTable } from "drizzle-orm/pg-core"; +import type { StaffPermissionsData } from "@/api/lib/permission-staff"; + import { core_roles } from "./roles"; import { core_users } from "./users"; @@ -20,6 +22,11 @@ export const core_moderators_permissions = pgTable( .notNull() .$onUpdate(() => new Date()), protected: t.boolean().notNull().default(false), + data: t + .jsonb() + .$type() + .notNull() + .default({ unrestricted: false, permissions: [] }), }), t => [ index("core_moderators_permissions_role_id_idx").on(t.roleId), diff --git a/packages/vitnode/src/hooks/use-mobile.ts b/packages/vitnode/src/hooks/use-mobile.ts index 50c341613..90940c989 100644 --- a/packages/vitnode/src/hooks/use-mobile.ts +++ b/packages/vitnode/src/hooks/use-mobile.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import React from "react"; const MOBILE_BREAKPOINT = 768; diff --git a/packages/vitnode/src/lib/api/get-moderator-permissions-api.ts b/packages/vitnode/src/lib/api/get-moderator-permissions-api.ts new file mode 100644 index 000000000..ca84a06e2 --- /dev/null +++ b/packages/vitnode/src/lib/api/get-moderator-permissions-api.ts @@ -0,0 +1,31 @@ +import type { + PermissionsStaffArgs, + StaffPermissionSet, +} from "@/api/lib/permission-staff"; + +import { hasStaffPermission } from "@/api/lib/staff-permission"; +import { usersModule } from "@/api/modules/users/users.module"; +import { fetcher } from "@/lib/fetcher"; + +export const getModeratorPermissionsApi = + async (): Promise => { + const res = await fetcher(usersModule, { + path: "/permissions", + method: "get", + module: "users", + }); + + if (res.status !== 200) { + return { root: false, permissions: [] }; + } + + return await res.json(); + }; + +export const checkModeratorPermissionApi = async ( + args: PermissionsStaffArgs, +): Promise => { + const set = await getModeratorPermissionsApi(); + + return hasStaffPermission(set, args); +}; diff --git a/packages/vitnode/src/lib/api/get-session-admin-api.ts b/packages/vitnode/src/lib/api/get-session-admin-api.ts index cf294e5eb..14f2c0a08 100644 --- a/packages/vitnode/src/lib/api/get-session-admin-api.ts +++ b/packages/vitnode/src/lib/api/get-session-admin-api.ts @@ -1,4 +1,8 @@ +import type { PermissionsStaffArgs } from "@/api/lib/permission-staff"; + +import { hasStaffPermission } from "@/api/lib/staff-permission"; import { adminModule } from "@/api/modules/admin/admin.module"; +import { CONFIG_PLUGIN } from "@/config"; import { fetcher } from "@/lib/fetcher"; import { redirect } from "../navigation"; @@ -23,3 +27,20 @@ export const getSessionAdminApi = async () => { return data; }; + +export const checkAdminPermissionApi = async ({ + plugin = CONFIG_PLUGIN.pluginId, + module, + permission, +}: Omit & { + plugin?: string; +}): Promise => { + const session = await getSessionAdminApi(); + if (!session) return false; + + return hasStaffPermission(session.permissions, { + plugin, + module, + permission, + }); +}; diff --git a/packages/vitnode/src/lib/plugin.ts b/packages/vitnode/src/lib/plugin.ts index 68c728e25..3cc12efed 100644 --- a/packages/vitnode/src/lib/plugin.ts +++ b/packages/vitnode/src/lib/plugin.ts @@ -1,10 +1,19 @@ +import type { PermissionsStaffArgs } from "../api/lib/permission-staff"; import type { ItemNavAdmin } from "../views/admin/layouts/sidebar/nav/item"; +/** + * A staff permission a nav item is gated by, scoped to the declaring plugin + * (the `plugin` is filled in automatically from the plugin's id). When set, the + * item is hidden from the admin sidebar unless the current admin holds it. + */ +export type AdminNavPermission = Omit; + interface AdminNavItem extends Pick< React.ComponentProps, "href" | "icon" | "isOpenInNewTab" > { id: string; + permission?: AdminNavPermission; } export interface BuildPluginReturn

{ diff --git a/packages/vitnode/src/locales/en.json b/packages/vitnode/src/locales/en.json index f19e5c2da..037363e32 100644 --- a/packages/vitnode/src/locales/en.json +++ b/packages/vitnode/src/locales/en.json @@ -1,4 +1,27 @@ { + "@vitnode/core": { + "title": "Core" + }, + "@vitnode/core:users": "Users", + "@vitnode/core:users:can_view": "View users list", + "@vitnode/core:users:can_create": "Create users", + "@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:debug": "Debug Panel", + "@vitnode/core:debug:can_view": "View debug panel", + "@vitnode/core:debug:can_clear_cache": "Clear cache", + "@vitnode/core:staff_moderators": "Staff: Moderators", + "@vitnode/core:staff_moderators:can_view": "View moderators list", + "@vitnode/core:staff_moderators:can_create": "Create moderators", + "@vitnode/core:staff_moderators:can_edit": "Edit moderator permissions", + "@vitnode/core:staff_moderators:can_delete": "Remove moderators", + "@vitnode/core:staff_admins": "Staff: Administrators", + "@vitnode/core:staff_admins:can_view": "View administrators list", + "@vitnode/core:staff_admins:can_create": "Create administrators", + "@vitnode/core:staff_admins:can_edit": "Edit administrator permissions", + "@vitnode/core:staff_admins:can_delete": "Remove administrators", "core": { "global": { "close": "Close", @@ -230,8 +253,12 @@ "users": { "title": "Users", "list": "User List", - "roles": "Roles", - "staff": "Staff" + "roles": "Roles" + }, + "staff": { + "title": "Staff", + "moderators": "Moderators", + "admins": "Administrators" }, "user_bar": { "home_page": "Home Page", @@ -362,6 +389,14 @@ "staff": { "title": "Staff", "desc": "Manage the staff of your application.", + "protected": "Protected", + "self": "You cannot edit your own permissions", + "delete": { + "title": "Remove staff member?", + "desc": "This revokes the assigned staff access. This action cannot be undone.", + "confirm": "Yes, remove", + "success": "Staff member removed." + }, "tabs": { "moderators": "Moderators", "admins": "Administrators" @@ -369,15 +404,73 @@ "table": { "role": "Role", "user": "User", - "updatedAt": "Updated At" + "permissions": "Permissions", + "unrestricted": "Unrestricted", + "restricted": "Restricted", + "updatedAt": "Updated At", + "edit": "Edit permissions" + }, + "edit": { + "title": "Edit permissions", + "subject": "For", + "back": "Back", + "save": "Save changes", + "success": "Permissions updated successfully.", + "error": "Failed to update permissions.", + "protected": "This entry is protected and its permissions cannot be edited.", + "self": "You cannot edit your own staff permissions, including the entry for your main role.", + "no_permissions": "No plugins have declared staff permissions yet.", + "select_all": "Enable all", + "clear_all": "Disable all", + "search_plugins": "Search plugins", + "search_empty": "No plugins match your search.", + "granted": "{granted}/{total} granted", + "requires": "Requires {permission}", + "mode": { + "label": "Access level", + "unrestricted": { + "label": "Unrestricted", + "desc": "Grant every permission, including ones added later." + }, + "restricted": { + "label": "Restricted", + "desc": "Choose exactly which permissions apply." + } + } + }, + "create": { + "admins": "Add administrator", + "moderators": "Add moderator", + "desc": "Grant staff access to a role or a specific user.", + "button": "Add member", + "back": "Back", + "assign_to": "Assign to", + "tabs": { + "role": "Role", + "role_desc": "Grant staff access to everyone with a role.", + "user": "User", + "user_desc": "Grant staff access to a single person." + }, + "select_role": "Select a role", + "search_user": "Search by name or email", + "submit": "Add member", + "success": "Staff member added.", + "error": "Failed to add staff member.", + "already_exists": "This role or user is already a staff member." }, "moderators": { + "title": "Moderators", + "desc": "Manage the moderators of your application.", + "create": "Add Moderator", "noResults": { "title": "No moderators found", "description": "Assign a role or user to grant moderator permissions." } }, "admins": { + "title": "Administrators", + "desc": "Manage the administrators of your application.", + "create": "Add Administrator", "noResults": { "title": "No administrators found", "description": "Assign a role or user to grant administrator permissions." diff --git a/packages/vitnode/src/routes/admin/core/debug/page.tsx b/packages/vitnode/src/routes/admin/core/debug/page.tsx index bec9ba007..881d4cc39 100644 --- a/packages/vitnode/src/routes/admin/core/debug/page.tsx +++ b/packages/vitnode/src/routes/admin/core/debug/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"; import { ClearCacheAction } from "@/views/admin/views/core/debug/actions/clear-cache/clear-cache"; const SystemLogsView = dynamic(async () => @@ -27,13 +29,21 @@ export const generateMetadata = async () => { export default async function Page( props: React.ComponentProps, ) { - const t = await getTranslations("admin.debug"); + const [t, canView, canClearCache] = await Promise.all([ + getTranslations("admin.debug"), + checkAdminPermissionApi({ module: "debug", permission: "can_view" }), + checkAdminPermissionApi({ module: "debug", permission: "can_clear_cache" }), + ]); + + if (!canView) { + notFound(); + } return (

- + {canClearCache && } diff --git a/packages/vitnode/src/routes/admin/core/staff/admins/create/page.tsx b/packages/vitnode/src/routes/admin/core/staff/admins/create/page.tsx new file mode 100644 index 000000000..dffd7d64b --- /dev/null +++ b/packages/vitnode/src/routes/admin/core/staff/admins/create/page.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import { I18nProvider } from "@/components/i18n-provider"; +import { Loader } from "@/components/ui/loader"; +import { CreateStaffPermissionsView } from "@/views/admin/views/core/staff/create/create-staff-permissions-view"; + +export default function Page() { + return ( + +
+ }> + + +
+
+ ); +} diff --git a/packages/vitnode/src/routes/admin/core/staff/admins/edit/[id]/page.tsx b/packages/vitnode/src/routes/admin/core/staff/admins/edit/[id]/page.tsx new file mode 100644 index 000000000..8398da931 --- /dev/null +++ b/packages/vitnode/src/routes/admin/core/staff/admins/edit/[id]/page.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { I18nProvider } from "@/components/i18n-provider"; +import { Loader } from "@/components/ui/loader"; +import { EditStaffPermissionsView } from "@/views/admin/views/core/staff/edit/edit-staff-permissions-view"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + +
+ }> + + +
+
+ ); +} diff --git a/packages/vitnode/src/routes/admin/core/users/staff/admins/page.tsx b/packages/vitnode/src/routes/admin/core/staff/admins/page.tsx similarity index 56% rename from packages/vitnode/src/routes/admin/core/users/staff/admins/page.tsx rename to packages/vitnode/src/routes/admin/core/staff/admins/page.tsx index ea0dd8ff1..724e77d6f 100644 --- a/packages/vitnode/src/routes/admin/core/users/staff/admins/page.tsx +++ b/packages/vitnode/src/routes/admin/core/staff/admins/page.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { DataTableSkeleton } from "@/components/table/data-table"; -import { AdminsStaffAdminView } from "@/views/admin/views/core/staff/admins/admins-staff-view"; +import { I18nProvider } from "@/components/i18n-provider"; +import { AdminsStaffAdminView } from "@/views/admin/views/core/staff/views/admins/admins-staff-view"; export default function Page( props: React.ComponentProps, ) { return ( - }> + - + ); } diff --git a/packages/vitnode/src/routes/admin/core/staff/moderators/create/page.tsx b/packages/vitnode/src/routes/admin/core/staff/moderators/create/page.tsx new file mode 100644 index 000000000..8b5dfa8a3 --- /dev/null +++ b/packages/vitnode/src/routes/admin/core/staff/moderators/create/page.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +import { I18nProvider } from "@/components/i18n-provider"; +import { Loader } from "@/components/ui/loader"; +import { CreateStaffPermissionsView } from "@/views/admin/views/core/staff/create/create-staff-permissions-view"; + +export default function Page() { + return ( + +
+ }> + + +
+
+ ); +} diff --git a/packages/vitnode/src/routes/admin/core/staff/moderators/edit/[id]/page.tsx b/packages/vitnode/src/routes/admin/core/staff/moderators/edit/[id]/page.tsx new file mode 100644 index 000000000..4aec66910 --- /dev/null +++ b/packages/vitnode/src/routes/admin/core/staff/moderators/edit/[id]/page.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { I18nProvider } from "@/components/i18n-provider"; +import { Loader } from "@/components/ui/loader"; +import { EditStaffPermissionsView } from "@/views/admin/views/core/staff/edit/edit-staff-permissions-view"; + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ( + +
+ }> + + +
+
+ ); +} diff --git a/packages/vitnode/src/routes/admin/core/users/staff/moderators/page.tsx b/packages/vitnode/src/routes/admin/core/staff/moderators/page.tsx similarity index 56% rename from packages/vitnode/src/routes/admin/core/users/staff/moderators/page.tsx rename to packages/vitnode/src/routes/admin/core/staff/moderators/page.tsx index f98b7bb3f..8a80c7e87 100644 --- a/packages/vitnode/src/routes/admin/core/users/staff/moderators/page.tsx +++ b/packages/vitnode/src/routes/admin/core/staff/moderators/page.tsx @@ -1,14 +1,14 @@ import React from "react"; -import { DataTableSkeleton } from "@/components/table/data-table"; -import { ModeratorsStaffAdminView } from "@/views/admin/views/core/staff/moderators/moderators-staff-view"; +import { I18nProvider } from "@/components/i18n-provider"; +import { ModeratorsStaffAdminView } from "@/views/admin/views/core/staff/views/moderators/moderators-staff-view"; export default function Page( props: React.ComponentProps, ) { return ( - }> + - + ); } diff --git a/packages/vitnode/src/routes/admin/core/users/page.tsx b/packages/vitnode/src/routes/admin/core/users/page.tsx index 0d9a094e2..37d5801ee 100644 --- a/packages/vitnode/src/routes/admin/core/users/page.tsx +++ b/packages/vitnode/src/routes/admin/core/users/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 { CreateUserAdmin } from "@/views/admin/views/core/users/actions/create/create"; const UsersAdminView = dynamic(async () => @@ -26,16 +28,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.user.list"), getTranslations("admin.global.nav.users"), + checkAdminPermissionApi({ module: "users", permission: "can_view" }), + checkAdminPermissionApi({ module: "users", permission: "can_create" }), ]); + if (!canView) { + notFound(); + } + return (
- + {canCreate && } }> diff --git a/packages/vitnode/src/routes/admin/core/users/staff/layout.tsx b/packages/vitnode/src/routes/admin/core/users/staff/layout.tsx deleted file mode 100644 index dacc297c7..000000000 --- a/packages/vitnode/src/routes/admin/core/users/staff/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { LayoutStaffAdmin } from "@/views/admin/views/core/staff/layout"; - -export default function Layout( - props: React.ComponentProps, -) { - return ; -} diff --git a/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/create/page.tsx b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/create/page.tsx new file mode 100644 index 000000000..21f67c0e4 --- /dev/null +++ b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/create/page.tsx @@ -0,0 +1,5 @@ +import { BreadcrumbStaffCreateAdmin } from "@/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin"; + +export default function BreadcrumbSlot() { + return ; +} diff --git a/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/edit/[id]/page.tsx b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/edit/[id]/page.tsx new file mode 100644 index 000000000..6624b1395 --- /dev/null +++ b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/edit/[id]/page.tsx @@ -0,0 +1,11 @@ +import { BreadcrumbStaffEditAdmin } from "@/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin"; + +export default async function BreadcrumbSlot({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ; +} diff --git a/packages/vitnode/src/routes/breadcrumb/admin/core/users/staff/admins/page.tsx b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/page.tsx similarity index 100% rename from packages/vitnode/src/routes/breadcrumb/admin/core/users/staff/admins/page.tsx rename to packages/vitnode/src/routes/breadcrumb/admin/core/staff/admins/page.tsx diff --git a/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/create/page.tsx b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/create/page.tsx new file mode 100644 index 000000000..05c04a02b --- /dev/null +++ b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/create/page.tsx @@ -0,0 +1,5 @@ +import { BreadcrumbStaffCreateAdmin } from "@/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin"; + +export default function BreadcrumbSlot() { + return ; +} diff --git a/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/edit/[id]/page.tsx b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/edit/[id]/page.tsx new file mode 100644 index 000000000..8101841a7 --- /dev/null +++ b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/edit/[id]/page.tsx @@ -0,0 +1,11 @@ +import { BreadcrumbStaffEditAdmin } from "@/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin"; + +export default async function BreadcrumbSlot({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + + return ; +} diff --git a/packages/vitnode/src/routes/breadcrumb/admin/core/users/staff/moderators/page.tsx b/packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/page.tsx similarity index 100% rename from packages/vitnode/src/routes/breadcrumb/admin/core/users/staff/moderators/page.tsx rename to packages/vitnode/src/routes/breadcrumb/admin/core/staff/moderators/page.tsx diff --git a/packages/vitnode/src/views/admin/layouts/admin-layout.tsx b/packages/vitnode/src/views/admin/layouts/admin-layout.tsx index 9f35d987a..4a702535f 100644 --- a/packages/vitnode/src/views/admin/layouts/admin-layout.tsx +++ b/packages/vitnode/src/views/admin/layouts/admin-layout.tsx @@ -1,5 +1,6 @@ import { cookies } from "next/headers"; +import { AdminStaffPermissionProvider } from "@/components/staff-permission/provider"; import { ThemeSwitcher } from "@/components/switchers/themes/theme-switcher"; import { Separator } from "@/components/ui/separator"; import { @@ -38,29 +39,31 @@ export const AdminLayout = async ({ return ( - - - -
- - {breadcrumb != null && ( - <> - - {breadcrumb} - - )} - -
- {vitNodeConfig.i18n.locales.length > 1 && ( - + + + + +
+ + {breadcrumb != null && ( + <> + + {breadcrumb} + )} - - -
-
- {children} -
-
+ +
+ {vitNodeConfig.i18n.locales.length > 1 && ( + + )} + + +
+ + {children} + + +
); }; diff --git a/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-admin.tsx b/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-admin.tsx index a7214216e..6a1952bc0 100644 --- a/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-admin.tsx +++ b/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-admin.tsx @@ -16,10 +16,10 @@ export const BreadcrumbStaffAdmin = async ({ return ( ); diff --git a/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin.tsx b/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin.tsx new file mode 100644 index 000000000..dadf35347 --- /dev/null +++ b/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-create-admin.tsx @@ -0,0 +1,28 @@ +import { getTranslations } from "next-intl/server"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { BreadcrumbAdmin } from "./breadcrumb-admin"; + +export const BreadcrumbStaffCreateAdmin = async ({ + type, +}: { + type: PermissionStaffType; +}) => { + const [t, tCreate] = await Promise.all([ + getTranslations("admin.staff"), + getTranslations("admin.staff.create"), + ]); + const tab = type === "admin" ? "admins" : "moderators"; + + return ( + + ); +}; diff --git a/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin.tsx b/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin.tsx new file mode 100644 index 000000000..d40175891 --- /dev/null +++ b/packages/vitnode/src/views/admin/layouts/breadcrumb/breadcrumb-staff-edit-admin.tsx @@ -0,0 +1,27 @@ +import { getTranslations } from "next-intl/server"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { BreadcrumbAdmin } from "./breadcrumb-admin"; + +export const BreadcrumbStaffEditAdmin = async ({ + type, + id, +}: { + id: string; + type: PermissionStaffType; +}) => { + const t = await getTranslations("admin.staff"); + const tab = type === "admin" ? "admins" : "moderators"; + + return ( + + ); +}; diff --git a/packages/vitnode/src/views/admin/layouts/breadcrumb/resolve-breadcrumb.ts b/packages/vitnode/src/views/admin/layouts/breadcrumb/resolve-breadcrumb.ts index be1fe0f70..651ab82b3 100644 --- a/packages/vitnode/src/views/admin/layouts/breadcrumb/resolve-breadcrumb.ts +++ b/packages/vitnode/src/views/admin/layouts/breadcrumb/resolve-breadcrumb.ts @@ -4,15 +4,9 @@ import type { NavAdminParent } from "../sidebar/nav/get-admin-nav"; export type { BreadcrumbCrumb }; -// Mirror the trailing-slash normalization used by the sidebar nav so hrefs like -// "/admin/core/" and "/admin/core" resolve to the same key. const normalizeUrl = (url: string): string => url.endsWith("/") && url.length > 1 ? url.slice(0, -1) : url; -// Flatten the nav tree into a `normalizedHref -> translated title` lookup, -// including nested sub-items. First writer wins, so a parent item keeps its -// label when a sub-item points at the same href (e.g. "Users" vs "User List" -// both at /admin/core/users) — better breadcrumb hierarchy. const flattenNav = (nav: NavAdminParent[]): Map => { const labels = new Map(); const setIfAbsent = (href: null | string | undefined, title: string) => { @@ -23,6 +17,8 @@ const flattenNav = (nav: NavAdminParent[]): Map => { }; for (const parent of nav) { + setIfAbsent(`/admin/${parent.id}`, parent.title); + for (const item of parent.items) { setIfAbsent(item.href, item.title); @@ -35,11 +31,6 @@ const flattenNav = (nav: NavAdminParent[]): Map => { return labels; }; -/** - * Turns the path segments after `/admin` into breadcrumb crumbs, resolving each - * cumulative path against the already-translated admin nav. Segments without a - * nav match fall back to a humanized label and render as plain text. - */ export const resolveBreadcrumb = ( nav: NavAdminParent[], segments: string[], 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 ee80be756..f8c360aac 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 @@ -1,8 +1,20 @@ -import { LayoutDashboardIcon, UsersRoundIcon, WrenchIcon } from "lucide-react"; +import { + LayoutDashboardIcon, + ShieldUserIcon, + UsersRoundIcon, + WrenchIcon, +} from "lucide-react"; import { getTranslations } from "next-intl/server"; +import type { + PermissionsStaffArgs, + StaffPermissionSet, +} from "@/api/lib/permission-staff"; import type { VitNodeConfig } from "@/vitnode.config"; +import { hasStaffPermission } from "@/api/lib/staff-permission"; +import { CONFIG_PLUGIN } from "@/config"; +import { getSessionAdminApi } from "@/lib/api/get-session-admin-api"; import { getVitNodeConfig } from "@/vitnode.config"; import type { ItemNavAdmin } from "./item"; @@ -13,14 +25,85 @@ export interface NavAdminParent { title: string; } +interface NavSubItemConfig { + href: string; + isOpenInNewTab?: boolean; + permission?: PermissionsStaffArgs; + title: string; +} + +interface NavItemConfig { + href: string; + icon?: React.ReactNode; + isOpenInNewTab?: boolean; + items?: NavSubItemConfig[]; + permission?: PermissionsStaffArgs; + title: string; +} + +interface NavGroupConfig { + id: string; + items: NavItemConfig[]; + title: string; +} + +const isAllowed = ( + permission: PermissionsStaffArgs | undefined, + set: StaffPermissionSet, +): boolean => !permission || hasStaffPermission(set, permission); + +const filterNavItems = ( + items: NavItemConfig[], + set: StaffPermissionSet, +): React.ComponentProps[] => { + const result: React.ComponentProps[] = []; + + for (const item of items) { + if (!isAllowed(item.permission, set)) continue; + + if (item.items && item.items.length > 0) { + const visibleSubItems = item.items.filter(subItem => + isAllowed(subItem.permission, set), + ); + if (visibleSubItems.length === 0) continue; + + result.push({ + href: item.href, + icon: item.icon, + isOpenInNewTab: item.isOpenInNewTab, + title: item.title, + items: visibleSubItems.map(subItem => ({ + href: subItem.href, + isOpenInNewTab: subItem.isOpenInNewTab, + title: subItem.title, + })), + }); + } else { + result.push({ + href: item.href, + icon: item.icon, + isOpenInNewTab: item.isOpenInNewTab, + title: item.title, + }); + } + } + + return result; +}; + export const getAdminNav = async ({ vitNodeConfig = getVitNodeConfig(), }: { vitNodeConfig?: VitNodeConfig; } = {}): Promise => { const t = await getTranslations(); + const session = await getSessionAdminApi(); + const permissions: StaffPermissionSet = session?.permissions ?? { + root: false, + permissions: [], + }; - const core: NavAdminParent = { + const core: NavGroupConfig = { id: "core", title: t("admin.global.nav.core"), items: [ @@ -42,14 +125,40 @@ export const getAdminNav = async ({ { title: t("admin.global.nav.users.list"), href: "/admin/core/users", + permission: { + plugin: CONFIG_PLUGIN.pluginId, + module: "users", + permission: "can_view", + }, }, { title: t("admin.global.nav.users.roles"), href: "/admin/core/users/roles", }, + ], + }, + { + href: "/admin/core/staff", + title: t("admin.global.nav.staff.title"), + icon: , + items: [ + { + title: t("admin.global.nav.staff.moderators"), + href: "/admin/core/staff/moderators", + permission: { + plugin: CONFIG_PLUGIN.pluginId, + module: "staff_moderators", + permission: "can_view", + }, + }, { - title: t("admin.global.nav.users.staff"), - href: "/admin/core/users/staff/moderators", + title: t("admin.global.nav.staff.admins"), + href: "/admin/core/staff/admins", + permission: { + plugin: CONFIG_PLUGIN.pluginId, + module: "staff_admins", + permission: "can_view", + }, }, ], }, @@ -67,7 +176,7 @@ export const getAdminNav = async ({ ], }; - const pluginNav: NavAdminParent[] = vitNodeConfig.plugins + const pluginNav: NavGroupConfig[] = vitNodeConfig.plugins .filter(plugin => plugin.admin?.nav) .map(plugin => ({ id: plugin.pluginId, @@ -75,19 +184,34 @@ export const getAdminNav = async ({ // @ts-expect-error title: t(`${plugin.pluginId}.title`), items: (plugin.admin?.nav ?? []).map(item => ({ - ...item, + href: item.href, + icon: item.icon, + isOpenInNewTab: item.isOpenInNewTab, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error title: t(`${plugin.pluginId}.admin.nav.${item.id}`), + permission: item.permission + ? { plugin: plugin.pluginId, ...item.permission } + : undefined, items: item.items?.map(subItem => ({ - ...subItem, + href: subItem.href, + isOpenInNewTab: subItem.isOpenInNewTab, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error title: t(`${plugin.pluginId}.admin.nav.${item.id}.${subItem.id}`), + permission: subItem.permission + ? { plugin: plugin.pluginId, ...subItem.permission } + : undefined, })) ?? [], })), })); - return [core, ...pluginNav]; + return [core, ...pluginNav] + .map(group => ({ + id: group.id, + title: group.title, + items: filterNavItems(group.items, permissions), + })) + .filter(group => group.items.length > 0); }; diff --git a/packages/vitnode/src/views/admin/layouts/sidebar/nav/item.tsx b/packages/vitnode/src/views/admin/layouts/sidebar/nav/item.tsx index 7481af2c6..3e1354d26 100644 --- a/packages/vitnode/src/views/admin/layouts/sidebar/nav/item.tsx +++ b/packages/vitnode/src/views/admin/layouts/sidebar/nav/item.tsx @@ -45,13 +45,34 @@ export const ItemNavAdmin = ({ return url.endsWith("/") && url.length > 1 ? url.slice(0, -1) : url; }; + // True when the pathname is the href itself or lives under it as a path + // segment (e.g. an edit/create sub-page), matching whole segments only. + const isPathnameUnderHref = (candidate: string) => { + const normalizedPathname = normalizeUrl(pathname); + const normalizedHref = normalizeUrl(candidate); + + return ( + normalizedPathname === normalizedHref || + normalizedPathname.startsWith(`${normalizedHref}/`) + ); + }; + // Check if current path matches href (with normalization) const isActive = normalizeUrl(pathname) === normalizeUrl(href); + // Only the most specific (longest) matching child is active, so nested + // siblings like `/users` and `/users/roles` don't both highlight. + const activeChildHref = items.reduce((best, item) => { + if (!isPathnameUnderHref(item.href)) return best; + if (best === null) return item.href; + + return normalizeUrl(item.href).length > normalizeUrl(best).length + ? item.href + : best; + }, null); + // Check if any child item is active - const hasActiveChild = items.some( - item => normalizeUrl(pathname) === normalizeUrl(item.href), - ); + const hasActiveChild = activeChildHref !== null; // Open collapsible by default if has active child, and keep it open when // navigating into one of its children. Controlled to avoid Base UI warning @@ -124,8 +145,7 @@ export const ItemNavAdmin = ({ > {items.map(item => { - const isChildActive = - normalizeUrl(pathname) === normalizeUrl(item.href); + const isChildActive = item.href === activeChildHref; return ( diff --git a/packages/vitnode/src/views/admin/layouts/user-bar/client.tsx b/packages/vitnode/src/views/admin/layouts/user-bar/client.tsx index 9fb7a2d45..a101b609f 100644 --- a/packages/vitnode/src/views/admin/layouts/user-bar/client.tsx +++ b/packages/vitnode/src/views/admin/layouts/user-bar/client.tsx @@ -3,12 +3,14 @@ import { BugIcon, HomeIcon, LogOut } from "lucide-react"; import { useTranslations } from "next-intl"; +import { useAdminStaffPermission } from "@/components/staff-permission/provider"; import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; +import { CONFIG_PLUGIN } from "@/config"; import { Link } from "@/lib/navigation"; import { logOutMutationApi } from "@/views/layouts/theme/header/user/auth/log-out-mutation-api"; @@ -21,6 +23,11 @@ export const ClientUserBarAdmin = ({ }; }) => { const t = useTranslations("admin.global.nav.user_bar"); + const canViewDebug = useAdminStaffPermission({ + plugin: CONFIG_PLUGIN.pluginId, + module: "debug", + permission: "can_view", + }); return ( <> @@ -36,10 +43,12 @@ export const ClientUserBarAdmin = ({ {t("home_page")} - }> - - {t("debug")} - + {canViewDebug && ( + }> + + {t("debug")} + + )} { + const canClearCache = await checkAdminPermissionApi({ + module: "debug", + permission: "can_clear_cache", + }); + + if (!canClearCache) { + throw new Error("Forbidden"); + } + await Promise.resolve(revalidatePath("/", "layout")); }; diff --git a/packages/vitnode/src/views/admin/views/core/staff/admins/admins-staff-view.tsx b/packages/vitnode/src/views/admin/views/core/staff/admins/admins-staff-view.tsx deleted file mode 100644 index 025f3a9be..000000000 --- a/packages/vitnode/src/views/admin/views/core/staff/admins/admins-staff-view.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { StaffTableAdmin } from "../staff-table"; - -export const AdminsStaffAdminView = ( - props: Pick, "searchParams">, -) => { - return ; -}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/create/create-staff-permissions-view.tsx b/packages/vitnode/src/views/admin/views/core/staff/create/create-staff-permissions-view.tsx new file mode 100644 index 000000000..8c6d5f766 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/create/create-staff-permissions-view.tsx @@ -0,0 +1,56 @@ +import { ArrowLeftIcon } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import { notFound } from "next/navigation"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { staffPermissionModuleByType } from "@/api/modules/admin/staff/lib/schema"; +import { Button } from "@/components/ui/button"; +import { HeaderContent } from "@/components/ui/header-content"; +import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api"; +import { Link } from "@/lib/navigation"; + +import { CreateStaffPermissionsForm } from "./form"; + +export const CreateStaffPermissionsView = async ({ + type, +}: { + type: PermissionStaffType; +}) => { + const [t, canCreate] = await Promise.all([ + getTranslations("admin.staff.create"), + checkAdminPermissionApi({ + module: staffPermissionModuleByType[type], + permission: "can_create", + }), + ]); + + if (!canCreate) { + notFound(); + } + + const backHref = + type === "admin" + ? "/admin/core/staff/admins" + : "/admin/core/staff/moderators"; + + return ( + <> + + + + + + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/create/form.tsx b/packages/vitnode/src/views/admin/views/core/staff/create/form.tsx new file mode 100644 index 000000000..a12076585 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/create/form.tsx @@ -0,0 +1,283 @@ +"use client"; + +import { ChevronsUpDownIcon, ShieldIcon, UserIcon } from "lucide-react"; +import { useLocale, useTranslations } from "next-intl"; +import React from "react"; +import { toast } from "sonner"; +import { useDebouncedCallback } from "use-debounce"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { Avatar } from "@/components/avatar"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Spinner } from "@/components/ui/spinner"; +import { useRouter } from "@/lib/navigation"; + +import type { Role } from "../../users/show/roles/search-roles.action"; +import type { StaffUserOption } from "./search.action"; + +import { searchRolesForUser } from "../../users/show/roles/search-roles.action"; +import { SelectableCard } from "../selectable-card"; +import { createStaffEntry } from "./mutation-api"; +import { searchUsersForStaff } from "./search.action"; + +const roleName = (role: Role, locale: string) => + role.name.find(item => item.languageCode === locale)?.name ?? + role.name[0]?.name ?? + ""; + +function EntityPicker({ + search, + getKey, + renderItem, + placeholder, + onSelect, + trigger, +}: { + getKey: (item: T) => string; + onSelect: (item: T) => void; + placeholder: string; + renderItem: (item: T) => React.ReactNode; + search: (value: string) => Promise; + trigger: React.ReactNode; +}) { + const t = useTranslations("core.global"); + const [open, setOpen] = React.useState(false); + const [options, setOptions] = React.useState([]); + const [isSearching, setIsSearching] = React.useState(false); + + const runSearch = React.useCallback( + async (value: string) => { + setIsSearching(true); + try { + setOptions(await search(value)); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + setOptions([]); + } finally { + setIsSearching(false); + } + }, + [search], + ); + const debouncedSearch = useDebouncedCallback(runSearch, 400); + + return ( + { + setOpen(next); + if (next) { + setOptions([]); + void runSearch(""); + } + }} + open={open} + > + + } + > + {trigger} + + + + + + + {isSearching && options.length === 0 ? ( +
+ +
+ ) : ( + <> + {t("results_not_found")} + + {options.map(item => ( + { + onSelect(item); + setOpen(false); + }} + value={getKey(item)} + > + {renderItem(item)} + + ))} + + + )} +
+
+
+
+ ); +} + +export const CreateStaffPermissionsForm = ({ + type, +}: { + type: PermissionStaffType; +}) => { + const t = useTranslations("admin.staff.create"); + const locale = useLocale(); + const router = useRouter(); + const [target, setTarget] = React.useState<"role" | "user">("role"); + const [role, setRole] = React.useState(null); + const [user, setUser] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + + const onSubmit = async () => { + let payload: null | { roleId: number } | { userId: number } = null; + if (target === "role") { + if (role) payload = { roleId: role.id }; + } else { + if (user) payload = { userId: user.id }; + } + if (!payload) return; + + setIsLoading(true); + const result = await createStaffEntry({ type, ...payload }); + + if (result.error) { + setIsLoading(false); + toast.error( + result.error.status === 409 ? t("already_exists") : t("error"), + ); + + return; + } + + toast.success(t("success")); + router.push( + `/admin/core/staff/${ + type === "admin" ? "admins" : "moderators" + }/edit/${result.data?.id}`, + ); + }; + + const canSubmit = target === "role" ? Boolean(role) : Boolean(user); + + return ( +
+
+

+ {t("assign_to")} +

+ +
+ + + + } + onSelect={() => setTarget("role")} + selected={target === "role"} + title={t("tabs.role")} + /> + + + + } + onSelect={() => setTarget("user")} + selected={target === "user"} + title={t("tabs.user")} + /> +
+
+ +
+ {target === "role" ? ( + + getKey={item => String(item.id)} + onSelect={setRole} + placeholder={t("select_role")} + renderItem={item => ( + + {roleName(item, locale)} + + )} + search={searchRolesForUser} + trigger={ + role ? ( + + {roleName(role, locale)} + + ) : ( + + {t("select_role")} + + ) + } + /> + ) : ( + + getKey={item => String(item.id)} + onSelect={setUser} + placeholder={t("search_user")} + renderItem={item => ( +
+ + {item.name} + + @{item.nameCode} + +
+ )} + search={searchUsersForStaff} + trigger={ + user ? ( + + + {user.name} + + ) : ( + + {t("search_user")} + + ) + } + /> + )} +
+ +
+ +
+
+ ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/create/mutation-api.ts b/packages/vitnode/src/views/admin/views/core/staff/create/mutation-api.ts new file mode 100644 index 000000000..66dbd8dbf --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/create/mutation-api.ts @@ -0,0 +1,36 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { adminModule } from "@/api/modules/admin/admin.module"; +import { fetcher } from "@/lib/fetcher"; + +export const createStaffEntry = async ({ + type, + roleId, + userId, +}: { + roleId?: number; + type: PermissionStaffType; + userId?: number; +}): Promise<{ data?: { id: number }; error?: { status: number } }> => { + const res = await fetcher(adminModule, { + path: "/entry/{type}", + method: "post", + module: "admin/staff", + args: { + params: { type }, + body: { roleId, userId }, + }, + }); + + if (res.status !== 201) { + return { error: { status: res.status } }; + } + + revalidatePath("/[locale]/admin", "layout"); + + return { data: await res.json() }; +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/create/search.action.ts b/packages/vitnode/src/views/admin/views/core/staff/create/search.action.ts new file mode 100644 index 000000000..7257ce7db --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/create/search.action.ts @@ -0,0 +1,38 @@ +"use server"; + +import { adminModule } from "@/api/modules/admin/admin.module"; +import { fetcher } from "@/lib/fetcher"; + +export interface StaffUserOption { + avatarColor: string; + id: number; + name: string; + nameCode: string; +} + +export const searchUsersForStaff = async ( + search: string, +): Promise => { + const res = await fetcher(adminModule, { + path: "/list", + method: "get", + module: "admin/users", + args: { + query: { search, first: "20" }, + }, + withPagination: true, + }); + + if (res.status !== 200) { + return []; + } + + const data = await res.json(); + + return data.edges.map(user => ({ + id: user.id, + name: user.name, + nameCode: user.nameCode, + avatarColor: user.avatarColor, + })); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/edit/edit-staff-permissions-view.tsx b/packages/vitnode/src/views/admin/views/core/staff/edit/edit-staff-permissions-view.tsx new file mode 100644 index 000000000..55fa58b0b --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/edit/edit-staff-permissions-view.tsx @@ -0,0 +1,161 @@ +import { ArrowLeftIcon } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import { notFound } from "next/navigation"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { staffPermissionKey } from "@/api/lib/staff-permission"; +import { adminModule } from "@/api/modules/admin/admin.module"; +import { staffPermissionModuleByType } from "@/api/modules/admin/staff/lib/schema"; +import { RoleFormat } from "@/components/role-format"; +import { Button } from "@/components/ui/button"; +import { HeaderContent } from "@/components/ui/header-content"; +import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api"; +import { fetcher } from "@/lib/fetcher"; +import { Link } from "@/lib/navigation"; + +import { StaffUserFormat } from "../table/staff-user-format"; +import { EditStaffPermissionsForm } from "./form"; + +interface EditStaffPermissionsViewProps { + id: string; + type: PermissionStaffType; +} + +export const EditStaffPermissionsView = async ({ + type, + id, +}: EditStaffPermissionsViewProps) => { + const canEdit = await checkAdminPermissionApi({ + module: staffPermissionModuleByType[type], + permission: "can_edit", + }); + + if (!canEdit) { + notFound(); + } + + const t = await getTranslations("admin.staff.edit"); + const tRoot = (await getTranslations()) as unknown as (( + key: string, + ) => string) & { + has: (key: string) => boolean; + }; + + const [catalogRes, entryRes] = await Promise.all([ + fetcher(adminModule, { + path: "/permission-catalog", + method: "get", + module: "admin/staff", + }), + fetcher(adminModule, { + path: "/entry/{type}/{id}", + method: "get", + module: "admin/staff", + args: { params: { type, id } }, + }), + ]); + + if (entryRes.status !== 200 || catalogRes.status !== 200) { + notFound(); + } + + const catalog = await catalogRes.json(); + const entry = await entryRes.json(); + + const granted = new Set( + entry.permissions.map(permission => staffPermissionKey(permission)), + ); + + // Resolve every label on the server so the client form only deals with + // ready-to-render strings. + const plugins = catalog + .map(plugin => { + const modules = Object.entries(plugin[type]) + .map(([module, permissions]) => ({ + module, + label: tRoot.has(`${plugin.pluginId}:${module}`) + ? tRoot(`${plugin.pluginId}:${module}`) + : module, + permissions: permissions.map(entry => { + const args = { + plugin: plugin.pluginId, + module, + permission: entry.permission, + }; + const key = staffPermissionKey(args); + + return { + ...args, + key, + checked: granted.has(key), + label: tRoot.has(key) ? tRoot(key) : entry.permission, + // The keys of the permissions this one depends on — the form + // keeps it hidden until every one of them is enabled. + dependsOn: entry.dependsOn.map(dependency => + staffPermissionKey({ + plugin: plugin.pluginId, + module, + permission: dependency, + }), + ), + }; + }), + })) + .filter(module => module.permissions.length > 0); + + return { + pluginId: plugin.pluginId, + label: tRoot.has(`${plugin.pluginId}.title`) + ? tRoot(`${plugin.pluginId}.title`) + : plugin.pluginId, + modules, + }; + }) + .filter(plugin => plugin.modules.length > 0); + + const backHref = + type === "admin" + ? "/admin/core/staff/admins" + : "/admin/core/staff/moderators"; + + return ( + <> + + {t("subject")} + {entry.role ? ( + + ) : entry.user ? ( + + ) : null} +
+ } + h1={t("title")} + > + +
+ + {entry.protected ? ( +

{t("protected")}

+ ) : entry.self ? ( +

{t("self")}

+ ) : ( + + )} + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/edit/form.tsx b/packages/vitnode/src/views/admin/views/core/staff/edit/form.tsx new file mode 100644 index 000000000..a87a6b2f4 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/edit/form.tsx @@ -0,0 +1,568 @@ +"use client"; + +import { + CheckIcon, + ChevronRightIcon, + LockIcon, + SearchIcon, + ShieldCheckIcon, + SparklesIcon, +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import React from "react"; +import { toast } from "sonner"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { useRouter } from "@/lib/navigation"; +import { cn } from "@/lib/utils"; + +import { SelectableCard } from "../selectable-card"; +import { updateStaffPermissions } from "./mutation-api"; + +const listHref = (type: PermissionStaffType) => + type === "admin" + ? "/admin/core/staff/admins" + : "/admin/core/staff/moderators"; + +interface PermissionItem { + checked: boolean; + // Keys of the permissions this one depends on. The row stays locked until + // every dependency is enabled (e.g. `can_create` depends on `can_view`). + dependsOn: string[]; + key: string; + label: string; + module: string; + permission: string; + plugin: string; +} + +interface ModuleGroup { + label: string; + module: string; + permissions: PermissionItem[]; +} + +export interface PluginGroup { + label: string; + modules: ModuleGroup[]; + pluginId: string; +} + +type Translate = ReturnType>; + +const countGranted = (permissions: PermissionItem[], checked: Set) => + permissions.reduce( + (total, item) => total + (checked.has(item.key) ? 1 : 0), + 0, + ); + +const CountBadge = ({ granted, total }: { granted: number; total: number }) => { + const isFull = total > 0 && granted === total; + + return ( + + {granted}/{total} + + ); +}; + +const PermissionRow = ({ + permission, + checked, + labelByKey, + onToggle, + t, +}: { + checked: Set; + labelByKey: Map; + onToggle: (key: string, value: boolean) => void; + permission: PermissionItem; + t: Translate; +}) => { + const isChecked = checked.has(permission.key); + const locked = !permission.dependsOn.every(dependency => + checked.has(dependency), + ); + const requires = permission.dependsOn + .map(dependency => labelByKey.get(dependency) ?? dependency) + .join(", "); + + return ( + + ); +}; + +const ModuleSection = ({ + module, + checked, + labelByKey, + open, + onOpenChange, + onToggle, + onToggleModule, + t, +}: { + checked: Set; + labelByKey: Map; + module: ModuleGroup; + onOpenChange: (value: boolean) => void; + onToggle: (key: string, value: boolean) => void; + onToggleModule: (module: ModuleGroup, value: boolean) => void; + open: boolean; + t: Translate; +}) => { + const granted = countGranted(module.permissions, checked); + const total = module.permissions.length; + const isFull = total > 0 && granted === total; + + return ( +
+
+ + + + onToggleModule(module, value)} + /> +
+ + {open ? ( +
+ {module.permissions.map(permission => ( + + ))} +
+ ) : null} +
+ ); +}; + +export const EditStaffPermissionsForm = ({ + type, + id, + plugins, + unrestricted: defaultUnrestricted, +}: { + id: string; + plugins: PluginGroup[]; + type: PermissionStaffType; + unrestricted: boolean; +}) => { + const t = useTranslations("admin.staff.edit"); + const router = useRouter(); + const [mode, setMode] = React.useState<"restricted" | "unrestricted">( + defaultUnrestricted ? "unrestricted" : "restricted", + ); + const [checked, setChecked] = React.useState>( + () => + new Set( + plugins.flatMap(plugin => + plugin.modules.flatMap(module => + module.permissions + .filter(permission => permission.checked) + .map(permission => permission.key), + ), + ), + ), + ); + const [isLoading, setIsLoading] = React.useState(false); + const [query, setQuery] = React.useState(""); + const [selectedPluginId, setSelectedPluginId] = React.useState( + () => plugins[0]?.pluginId ?? "", + ); + const [openModules, setOpenModules] = React.useState>( + () => new Set(), + ); + + const allItems = React.useMemo( + () => + plugins.flatMap(plugin => + plugin.modules.flatMap(module => module.permissions), + ), + [plugins], + ); + + // Resolve every permission key to its label so dependency hints ("Requires + // …") can name the gates they wait on. + const labelByKey = React.useMemo(() => { + const map = new Map(); + for (const item of allItems) map.set(item.key, item.label); + + return map; + }, [allItems]); + + // Reverse index: permission key -> keys of the permissions that depend on it, + // so turning a gate off can cascade to everything it unlocks. + const dependentsByKey = React.useMemo(() => { + const map = new Map(); + for (const item of allItems) { + for (const dependency of item.dependsOn) { + map.set(dependency, [...(map.get(dependency) ?? []), item.key]); + } + } + + return map; + }, [allItems]); + + const filteredPlugins = React.useMemo(() => { + const value = query.trim().toLowerCase(); + if (!value) return plugins; + + return plugins.filter( + plugin => + plugin.label.toLowerCase().includes(value) || + plugin.pluginId.toLowerCase().includes(value), + ); + }, [plugins, query]); + + const selectedPlugin = + filteredPlugins.find(plugin => plugin.pluginId === selectedPluginId) ?? + filteredPlugins[0]; + + const toggle = (key: string, value: boolean) => { + setChecked(prev => { + const next = new Set(prev); + if (value) { + next.add(key); + + return next; + } + + // Removing a permission also removes anything that (transitively) depends + // on it, so a hidden switch can never stay granted behind the scenes. + const stack = [key]; + while (stack.length > 0) { + const current = stack.pop(); + if (current === undefined) break; + next.delete(current); + for (const dependent of dependentsByKey.get(current) ?? []) { + if (next.has(dependent)) stack.push(dependent); + } + } + + return next; + }); + }; + + const setKeysChecked = (keys: string[], value: boolean) => { + setChecked(prev => { + const next = new Set(prev); + for (const key of keys) { + if (value) { + next.add(key); + } else { + next.delete(key); + } + } + + return next; + }); + }; + + const setPluginChecked = (plugin: PluginGroup, value: boolean) => { + setKeysChecked( + plugin.modules.flatMap(module => + module.permissions.map(permission => permission.key), + ), + value, + ); + }; + + const setModuleChecked = (module: ModuleGroup, value: boolean) => { + setKeysChecked( + module.permissions.map(permission => permission.key), + value, + ); + }; + + const onSubmit = async () => { + const unrestricted = mode === "unrestricted"; + setIsLoading(true); + + const permissions = unrestricted + ? [] + : allItems + .filter( + item => + checked.has(item.key) && + item.dependsOn.every(dependency => checked.has(dependency)), + ) + .map(({ plugin, module, permission }) => ({ + plugin, + module, + permission, + })); + + const result = await updateStaffPermissions({ + type, + id, + unrestricted, + permissions, + }); + + if (result.error) { + setIsLoading(false); + toast.error(t("error")); + + return; + } + + toast.success(t("success")); + router.push(listHref(type)); + }; + + return ( +
+
+

+ {t("mode.label")} +

+ +
+ + + + } + onSelect={() => setMode("unrestricted")} + selected={mode === "unrestricted"} + title={t("mode.unrestricted.label")} + /> + + + + } + onSelect={() => setMode("restricted")} + selected={mode === "restricted"} + title={t("mode.restricted.label")} + /> +
+
+ + {mode === "restricted" && + (plugins.length === 0 ? ( +

{t("no_permissions")}

+ ) : ( +
+ + +
+ {selectedPlugin ? ( + <> +
+
+

+ {selectedPlugin.label} +

+

+ {selectedPlugin.pluginId} +

+
+ +
+ + · + +
+
+ + {selectedPlugin.modules.map(module => { + const moduleKey = `${selectedPlugin.pluginId}:${module.module}`; + + return ( + + setOpenModules(prev => { + const next = new Set(prev); + if (value) { + next.add(moduleKey); + } else { + next.delete(moduleKey); + } + + return next; + }) + } + onToggle={toggle} + onToggleModule={setModuleChecked} + open={openModules.has(moduleKey)} + t={t} + /> + ); + })} + + ) : ( +

+ {t("search_empty")} +

+ )} +
+
+ ))} + +
+ +
+
+ ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/edit/mutation-api.ts b/packages/vitnode/src/views/admin/views/core/staff/edit/mutation-api.ts new file mode 100644 index 000000000..85cc336ec --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/edit/mutation-api.ts @@ -0,0 +1,41 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import type { + PermissionsStaffArgs, + PermissionStaffType, +} from "@/api/lib/permission-staff"; + +import { adminModule } from "@/api/modules/admin/admin.module"; +import { fetcher } from "@/lib/fetcher"; + +export const updateStaffPermissions = async ({ + type, + id, + unrestricted, + permissions, +}: { + id: string; + permissions: PermissionsStaffArgs[]; + type: PermissionStaffType; + unrestricted: boolean; +}): Promise<{ data?: true; error?: { status: number } }> => { + const res = await fetcher(adminModule, { + path: "/entry/{type}/{id}", + method: "patch", + module: "admin/staff", + args: { + params: { type, id }, + body: { unrestricted, permissions }, + }, + }); + + if (res.status !== 200) { + return { error: { status: res.status } }; + } + + revalidatePath("/[locale]/admin", "layout"); + + return { data: true }; +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/layout.tsx b/packages/vitnode/src/views/admin/views/core/staff/layout.tsx deleted file mode 100644 index 045b07128..000000000 --- a/packages/vitnode/src/views/admin/views/core/staff/layout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { getTranslations } from "next-intl/server"; - -import { I18nProvider } from "@/components/i18n-provider"; -import { HeaderContent } from "@/components/ui/header-content"; - -import { StaffTabsAdmin } from "./staff-tabs"; - -export const LayoutStaffAdmin = async ({ - children, -}: { - children: React.ReactNode; -}) => { - const t = await getTranslations("admin.staff"); - - return ( - -
- - - - - {children} -
-
- ); -}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/moderators/moderators-staff-view.tsx b/packages/vitnode/src/views/admin/views/core/staff/moderators/moderators-staff-view.tsx deleted file mode 100644 index 3de59a4b2..000000000 --- a/packages/vitnode/src/views/admin/views/core/staff/moderators/moderators-staff-view.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { StaffTableAdmin } from "../staff-table"; - -export const ModeratorsStaffAdminView = ( - props: Pick, "searchParams">, -) => { - return ; -}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/selectable-card.tsx b/packages/vitnode/src/views/admin/views/core/staff/selectable-card.tsx new file mode 100644 index 000000000..48e6c3e89 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/selectable-card.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { CheckIcon } from "lucide-react"; +import React from "react"; + +import { cn } from "@/lib/utils"; + +interface SelectableCardProps { + description: React.ReactNode; + icon: React.ReactNode; + onSelect: () => void; + selected: boolean; + title: React.ReactNode; +} + +/** + * A large, tappable choice card with a radio-style indicator in the corner. + * Shared by the access-level switch (edit) and the assignee switch (create) so + * both flows speak the same visual language. + */ +export const SelectableCard = ({ + selected, + onSelect, + icon, + title, + description, +}: SelectableCardProps) => { + return ( + + ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/staff-tabs.tsx b/packages/vitnode/src/views/admin/views/core/staff/staff-tabs.tsx deleted file mode 100644 index 2874f177a..000000000 --- a/packages/vitnode/src/views/admin/views/core/staff/staff-tabs.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; - -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Link, usePathname } from "@/lib/navigation"; - -export const StaffTabsAdmin = () => { - const t = useTranslations("admin.staff.tabs"); - const pathname = usePathname(); - const value = pathname.includes("/staff/admins") ? "admins" : "moderators"; - - return ( - - - } - value="moderators" - > - {t("moderators")} - - } - value="admins" - > - {t("admins")} - - - - ); -}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/table/actions/delete-action.ts b/packages/vitnode/src/views/admin/views/core/staff/table/actions/delete-action.ts new file mode 100644 index 000000000..732c96e36 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/table/actions/delete-action.ts @@ -0,0 +1,33 @@ +"use server"; + +import { revalidatePath } from "next/cache"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { adminModule } from "@/api/modules/admin/admin.module"; +import { fetcher } from "@/lib/fetcher"; + +export const deleteStaffEntry = async ({ + type, + id, +}: { + id: number; + type: PermissionStaffType; +}): Promise<{ data?: true; error?: { status: number } }> => { + const res = await fetcher(adminModule, { + path: "/entry/{type}/{id}", + method: "delete", + module: "admin/staff", + args: { + params: { type, id: String(id) }, + }, + }); + + if (res.status !== 200) { + return { error: { status: res.status } }; + } + + revalidatePath("/[locale]/admin", "layout"); + + return { data: true }; +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/table/actions/staff-row-actions.tsx b/packages/vitnode/src/views/admin/views/core/staff/table/actions/staff-row-actions.tsx new file mode 100644 index 000000000..fbee83512 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/table/actions/staff-row-actions.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { LockIcon, PencilIcon, Trash2Icon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { toast } from "sonner"; + +import type { PermissionStaffType } from "@/api/lib/permission-staff"; + +import { ConfirmActionAlertDialog } from "@/components/confirm-action/confirm-action-alert-dialog"; +import { Button } from "@/components/ui/button"; +import { TooltipWithContent } from "@/components/ui/tooltip"; +import { Link } from "@/lib/navigation"; + +import { deleteStaffEntry } from "./delete-action"; + +export const StaffRowActions = ({ + type, + id, + protected: isProtected, + self, + canEdit, + canDelete, +}: { + canDelete: boolean; + canEdit: boolean; + id: number; + protected: boolean; + self: boolean; + type: PermissionStaffType; +}) => { + const t = useTranslations("admin.staff"); + const tGlobal = useTranslations("core.global"); + + // Protected entries are managed by the system, and an admin cannot manage the + // entry that governs their own access — neither can be edited or removed. + if (isProtected || self) { + return ( +
+ + + + + +
+ ); + } + + // No actionable permissions — render an empty cell. + if (!canEdit && !canDelete) { + return null; + } + + const editHref = `/admin/core/staff/${ + type === "admin" ? "admins" : "moderators" + }/edit/${id}`; + + return ( +
+ {canEdit && ( + + )} + + {canDelete && ( + { + const result = await deleteStaffEntry({ type, id }); + if (result.error) { + toast.error(tGlobal("errors.title"), { + description: tGlobal("errors.internal_server_error"), + }); + + return; + } + + toast.success(t("delete.success")); + onClose(); + }} + textSubmit={t("delete.confirm")} + title={t("delete.title")} + > + + + )} +
+ ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/staff-table.tsx b/packages/vitnode/src/views/admin/views/core/staff/table/staff-table.tsx similarity index 65% rename from packages/vitnode/src/views/admin/views/core/staff/staff-table.tsx rename to packages/vitnode/src/views/admin/views/core/staff/table/staff-table.tsx index 4ca18c5c7..c7d4324e3 100644 --- a/packages/vitnode/src/views/admin/views/core/staff/staff-table.tsx +++ b/packages/vitnode/src/views/admin/views/core/staff/table/staff-table.tsx @@ -3,11 +3,15 @@ import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; import { adminModule } from "@/api/modules/admin/admin.module"; +import { staffPermissionModuleByType } from "@/api/modules/admin/staff/lib/schema"; import { DateFormat } from "@/components/date-format"; import { RoleFormat } from "@/components/role-format"; import { DataTable } from "@/components/table/data-table"; +import { Badge } from "@/components/ui/badge"; +import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api"; import { fetcher } from "@/lib/fetcher"; +import { StaffRowActions } from "./actions/staff-row-actions"; import { StaffUserFormat } from "./staff-user-format"; interface StaffTableAdminProps { @@ -19,9 +23,19 @@ export const StaffTableAdmin = async ({ type, searchParams, }: StaffTableAdminProps) => { - const [t, tType] = await Promise.all([ + const staffType = type === "admins" ? "admin" : "moderator"; + const permissionModule = staffPermissionModuleByType[staffType]; + const [t, tType, canEdit, canDelete] = await Promise.all([ getTranslations("admin.staff.table"), getTranslations(`admin.staff.${type}`), + checkAdminPermissionApi({ + module: permissionModule, + permission: "can_edit", + }), + checkAdminPermissionApi({ + module: permissionModule, + permission: "can_delete", + }), ]); const query = await searchParams; const res = @@ -70,11 +84,36 @@ export const StaffTableAdmin = async ({ ), }, + { + id: "unrestricted", + label: t("permissions"), + cell: ({ row }) => + row.unrestricted ? ( + {t("unrestricted")} + ) : ( + {t("restricted")} + ), + }, { id: "updatedAt", label: t("updatedAt"), cell: ({ row }) => , }, + { + id: "actions", + label: "", + className: "w-10", + cell: ({ row }) => ( + + ), + }, ]} customNoResults={{ title: tType("noResults.title"), diff --git a/packages/vitnode/src/views/admin/views/core/staff/staff-user-format.tsx b/packages/vitnode/src/views/admin/views/core/staff/table/staff-user-format.tsx similarity index 100% rename from packages/vitnode/src/views/admin/views/core/staff/staff-user-format.tsx rename to packages/vitnode/src/views/admin/views/core/staff/table/staff-user-format.tsx diff --git a/packages/vitnode/src/views/admin/views/core/staff/views/admins/admins-staff-view.tsx b/packages/vitnode/src/views/admin/views/core/staff/views/admins/admins-staff-view.tsx new file mode 100644 index 000000000..f32366b35 --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/views/admins/admins-staff-view.tsx @@ -0,0 +1,49 @@ +import { PlusIcon } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import { notFound } from "next/navigation"; +import React from "react"; + +import { DataTableSkeleton } from "@/components/table/data-table"; +import { Button } from "@/components/ui/button"; +import { HeaderContent } from "@/components/ui/header-content"; +import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api"; +import { Link } from "@/lib/navigation"; + +import { StaffTableAdmin } from "../../table/staff-table"; + +export const AdminsStaffAdminView = async ( + props: Pick, "searchParams">, +) => { + const [t, canView, canCreate] = await Promise.all([ + getTranslations("admin.staff.admins"), + checkAdminPermissionApi({ module: "staff_admins", permission: "can_view" }), + checkAdminPermissionApi({ + module: "staff_admins", + permission: "can_create", + }), + ]); + + if (!canView) { + notFound(); + } + + return ( +
+ + {canCreate && ( + + )} + + + }> + + +
+ ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/staff/views/moderators/moderators-staff-view.tsx b/packages/vitnode/src/views/admin/views/core/staff/views/moderators/moderators-staff-view.tsx new file mode 100644 index 000000000..ec96dcb7b --- /dev/null +++ b/packages/vitnode/src/views/admin/views/core/staff/views/moderators/moderators-staff-view.tsx @@ -0,0 +1,51 @@ +import { PlusIcon } from "lucide-react"; +import { getTranslations } from "next-intl/server"; +import { notFound } from "next/navigation"; +import React from "react"; + +import { DataTableSkeleton } from "@/components/table/data-table"; +import { Button } from "@/components/ui/button"; +import { HeaderContent } from "@/components/ui/header-content"; +import { checkAdminPermissionApi } from "@/lib/api/get-session-admin-api"; +import { Link } from "@/lib/navigation"; + +import { StaffTableAdmin } from "../../table/staff-table"; + +export const ModeratorsStaffAdminView = async ( + props: Pick, "searchParams">, +) => { + const [t, canView, canCreate] = await Promise.all([ + getTranslations("admin.staff.moderators"), + checkAdminPermissionApi({ + module: "staff_moderators", + permission: "can_view", + }), + checkAdminPermissionApi({ + module: "staff_moderators", + permission: "can_create", + }), + ]); + + if (!canView) { + notFound(); + } + + return ( +
+ + {canCreate && ( + + )} + + }> + + +
+ ); +}; diff --git a/packages/vitnode/src/views/admin/views/core/users/actions.tsx b/packages/vitnode/src/views/admin/views/core/users/actions.tsx index 9776ae187..3c9c3b3fd 100644 --- a/packages/vitnode/src/views/admin/views/core/users/actions.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/actions.tsx @@ -3,8 +3,10 @@ import { PenIcon } from "lucide-react"; import { useTranslations } from "next-intl"; +import { useAdminStaffPermission } from "@/components/staff-permission/provider"; import { buttonVariants } from "@/components/ui/button"; import { TooltipWithContent } from "@/components/ui/tooltip"; +import { CONFIG_PLUGIN } from "@/config"; import { Link } from "@/lib/navigation"; import { VerifyEmailUserAdmin } from "./actions/verify-email/verify-email"; @@ -17,6 +19,13 @@ export const UsersAdminActions = ({ id: number; }) => { const t = useTranslations("admin.user.list"); + const canEdit = useAdminStaffPermission({ + plugin: CONFIG_PLUGIN.pluginId, + module: "users", + permission: "can_edit", + }); + + if (!canEdit) return null; return ( <> diff --git a/packages/vitnode/src/views/admin/views/core/users/show/edit-field.tsx b/packages/vitnode/src/views/admin/views/core/users/show/edit-field.tsx index 24a55d2ef..b4e947112 100644 --- a/packages/vitnode/src/views/admin/views/core/users/show/edit-field.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/show/edit-field.tsx @@ -2,7 +2,7 @@ import { CheckIcon, MailIcon, PencilIcon, XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; -import * as React from "react"; +import React from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -20,8 +20,10 @@ export const EditUserField = ({ as: Tag = "span", valueClassName, showUnverified = false, + canEdit = true, }: { as?: "h2" | "span"; + canEdit?: boolean; field: "email" | "name"; id: number; label: string; @@ -136,14 +138,16 @@ export const EditUserField = ({ )}
- + {canEdit && ( + + )}
); }; diff --git a/packages/vitnode/src/views/admin/views/core/users/show/roles/edit-roles.tsx b/packages/vitnode/src/views/admin/views/core/users/show/roles/edit-roles.tsx index 83f0fc60c..ffc9dd785 100644 --- a/packages/vitnode/src/views/admin/views/core/users/show/roles/edit-roles.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/show/roles/edit-roles.tsx @@ -9,7 +9,7 @@ import { XIcon, } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; -import * as React from "react"; +import React from "react"; import { toast } from "sonner"; import { useDebouncedCallback } from "use-debounce"; diff --git a/packages/vitnode/src/views/admin/views/core/users/show/roles/roles.tsx b/packages/vitnode/src/views/admin/views/core/users/show/roles/roles.tsx index aa61e4c8b..4fed7f2ba 100644 --- a/packages/vitnode/src/views/admin/views/core/users/show/roles/roles.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/show/roles/roles.tsx @@ -18,7 +18,9 @@ export const RolesUserAdmin = async ({ id, role, secondaryRoles, + canEdit = true, }: { + canEdit?: boolean; id: number; role: Role; secondaryRoles: Role[]; @@ -32,13 +34,15 @@ export const RolesUserAdmin = async ({ {t("rolesTitle")} - - - + {canEdit && ( + + + + )} diff --git a/packages/vitnode/src/views/admin/views/core/users/show/show-user-admin-view.tsx b/packages/vitnode/src/views/admin/views/core/users/show/show-user-admin-view.tsx index 4e17cebb2..42744653c 100644 --- a/packages/vitnode/src/views/admin/views/core/users/show/show-user-admin-view.tsx +++ b/packages/vitnode/src/views/admin/views/core/users/show/show-user-admin-view.tsx @@ -2,11 +2,14 @@ import { ExternalLinkIcon } from "lucide-react"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; +import { hasStaffPermission } from "@/api/lib/staff-permission"; import { adminModule } from "@/api/modules/admin/admin.module"; import { Avatar } from "@/components/avatar"; import { DateFormat } from "@/components/date-format"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { CONFIG_PLUGIN } from "@/config"; +import { getSessionAdminApi } from "@/lib/api/get-session-admin-api"; import { fetcher } from "@/lib/fetcher"; import { Link } from "@/lib/navigation"; @@ -32,15 +35,36 @@ export const ShowUserAdminView = async ({ id }: { id: string }) => { const user = await res.json(); + // Editing any user needs `can_edit`; editing an administrator additionally + // needs the elevated `can_edit_admin` permission. The backend enforces the + // same rule — this only hides the edit controls. + const session = await getSessionAdminApi(); + const canEdit = + !!session && + hasStaffPermission(session.permissions, { + plugin: CONFIG_PLUGIN.pluginId, + module: "users", + permission: "can_edit", + }) && + (user.isAdmin + ? hasStaffPermission(session.permissions, { + plugin: CONFIG_PLUGIN.pluginId, + module: "users", + permission: "can_edit_admin", + }) + : true); + return (
{/* Cover placeholder */}
{t("coverPlaceholder")} -
- -
+ {canEdit && ( +
+ +
+ )}
@@ -52,15 +76,18 @@ export const ShowUserAdminView = async ({ id }: { id: string }) => { size={128} user={user} /> -
- -
+ {canEdit && ( +
+ +
+ )}
{/* Username */} { @{user.nameCode} - + {canEdit && }
{/* Email */}
{ { return buildApiPlugin({ pluginId: CONFIG_PLUGIN.pluginId, modules: [adminModule, categoriesModule, postsModule], + permissionStaff: { + moderator: { + posts: ["can_edit", "can_delete"], + }, + admin: { + posts: [ + "can_view", + { + permission: "can_create", + dependsOn: ["can_view"], + }, + "can_edit", + "can_delete", + ], + categories: ["can_view", "can_create", "can_edit", "can_delete"], + }, + }, }); }; diff --git a/plugins/blog/src/config.tsx b/plugins/blog/src/config.tsx index c0316143c..5aae2b245 100644 --- a/plugins/blog/src/config.tsx +++ b/plugins/blog/src/config.tsx @@ -12,11 +12,13 @@ export const blogPlugin = () => { id: "posts", href: "/admin/blog/posts", icon: , + permission: { module: "posts", permission: "can_view" }, }, { id: "categories", href: "/admin/blog/categories", icon: , + permission: { module: "categories", permission: "can_view" }, }, ], }, diff --git a/plugins/blog/src/locales/en.json b/plugins/blog/src/locales/en.json index 65f92dfff..cef3d9ac1 100644 --- a/plugins/blog/src/locales/en.json +++ b/plugins/blog/src/locales/en.json @@ -66,5 +66,15 @@ } } } - } + }, + "@vitnode/blog:posts": "Posts", + "@vitnode/blog:posts:can_view": "View posts list", + "@vitnode/blog:posts:can_create": "Create posts", + "@vitnode/blog:posts:can_edit": "Edit posts", + "@vitnode/blog:posts:can_delete": "Delete posts", + "@vitnode/blog:categories": "Categories", + "@vitnode/blog:categories:can_view": "View categories list", + "@vitnode/blog:categories:can_create": "Create categories", + "@vitnode/blog:categories:can_edit": "Edit categories", + "@vitnode/blog:categories:can_delete": "Delete categories" } diff --git a/plugins/blog/src/routes/admin/blog/categories/page.tsx b/plugins/blog/src/routes/admin/blog/categories/page.tsx index 33e5012fd..36aa27716 100644 --- a/plugins/blog/src/routes/admin/blog/categories/page.tsx +++ b/plugins/blog/src/routes/admin/blog/categories/page.tsx @@ -3,10 +3,13 @@ import type { Metadata } from "next"; import { I18nProvider } from "@vitnode/core/components/i18n-provider"; import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; import { getTranslations } from "next-intl/server"; import dynamic from "next/dynamic"; +import { notFound } from "next/navigation"; import React from "react"; +import { CONFIG_PLUGIN } from "@/const"; import { ActionsCategoriesAdmin } from "@/views/admin/categories/actions/actions"; const CategoriesAdminView = dynamic(async () => @@ -26,16 +29,30 @@ export const generateMetadata = async (): Promise => { export default async function CategoriesPage( params: React.ComponentProps, ) { - const [t, tNav] = await Promise.all([ + const [t, tNav, canView, canCreate] = await Promise.all([ getTranslations("@vitnode/blog.admin.categories"), getTranslations("@vitnode/blog.admin.nav"), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "categories", + permission: "can_view", + }), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "categories", + permission: "can_create", + }), ]); + if (!canView) { + notFound(); + } + return (
- + {canCreate && } }> diff --git a/plugins/blog/src/routes/admin/blog/posts/page.tsx b/plugins/blog/src/routes/admin/blog/posts/page.tsx index b7d009d98..53ee8699e 100644 --- a/plugins/blog/src/routes/admin/blog/posts/page.tsx +++ b/plugins/blog/src/routes/admin/blog/posts/page.tsx @@ -3,10 +3,13 @@ import type { Metadata } from "next"; import { I18nProvider } from "@vitnode/core/components/i18n-provider"; import { DataTableSkeleton } from "@vitnode/core/components/table/data-table"; import { HeaderContent } from "@vitnode/core/components/ui/header-content"; +import { checkAdminPermissionApi } from "@vitnode/core/lib/api/get-session-admin-api"; import { getTranslations } from "next-intl/server"; import dynamic from "next/dynamic"; +import { notFound } from "next/navigation"; import React from "react"; +import { CONFIG_PLUGIN } from "@/const"; import { ActionsPostsAdmin } from "@/views/admin/posts/actions/actions"; const PostsAdminView = dynamic(async () => @@ -26,16 +29,30 @@ export const generateMetadata = async (): Promise => { export default async function PostsPage( params: React.ComponentProps, ) { - const [t, tNav] = await Promise.all([ + const [t, tNav, canView, canCreate] = await Promise.all([ getTranslations("@vitnode/blog.admin.posts"), getTranslations("@vitnode/blog.admin.nav"), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "posts", + permission: "can_view", + }), + checkAdminPermissionApi({ + plugin: CONFIG_PLUGIN.pluginId, + module: "posts", + permission: "can_create", + }), ]); + if (!canView) { + notFound(); + } + return (
- + {canCreate && } }> diff --git a/plugins/blog/src/views/admin/categories/table/actions/delete/delete-action.tsx b/plugins/blog/src/views/admin/categories/table/actions/delete/delete-action.tsx index dc5325d2f..33ef3a4d7 100644 --- a/plugins/blog/src/views/admin/categories/table/actions/delete/delete-action.tsx +++ b/plugins/blog/src/views/admin/categories/table/actions/delete/delete-action.tsx @@ -1,6 +1,7 @@ "use client"; import { ConfirmActionAlertDialog } from "@vitnode/core/components/confirm-action/confirm-action-alert-dialog"; +import { useAdminStaffPermission } from "@vitnode/core/components/staff-permission/provider"; import { Button } from "@vitnode/core/components/ui/button"; import { Tooltip, @@ -12,11 +13,20 @@ import { Trash2Icon } from "lucide-react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; +import { CONFIG_PLUGIN } from "@/const"; + import { mutationApi } from "./mutation-api"; export const DeleteAction = ({ title, id }: { id: number; title: string }) => { const t = useTranslations("@vitnode/blog.admin.categories.delete"); const tGlobal = useTranslations("core.global"); + const canDelete = useAdminStaffPermission({ + plugin: CONFIG_PLUGIN.pluginId, + module: "categories", + permission: "can_delete", + }); + + if (!canDelete) return null; return ( diff --git a/plugins/blog/src/views/admin/categories/table/actions/edit-action.tsx b/plugins/blog/src/views/admin/categories/table/actions/edit-action.tsx index fc29e26ea..c6488f6fc 100644 --- a/plugins/blog/src/views/admin/categories/table/actions/edit-action.tsx +++ b/plugins/blog/src/views/admin/categories/table/actions/edit-action.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAdminStaffPermission } from "@vitnode/core/components/staff-permission/provider"; import { Button } from "@vitnode/core/components/ui/button"; import { Dialog, @@ -21,6 +22,8 @@ import { useTranslations } from "next-intl"; import dynamic from "next/dynamic"; import React from "react"; +import { CONFIG_PLUGIN } from "@/const"; + const CreateEditActionCategoriesAdmin = dynamic(async () => import("../..//actions/create-edit/create-edit").then(mod => ({ default: mod.CreateEditActionCategoriesAdmin, @@ -31,6 +34,13 @@ export const EditAction = ( props: Required>, ) => { const t = useTranslations("@vitnode/blog.admin.categories.edit"); + const canEdit = useAdminStaffPermission({ + plugin: CONFIG_PLUGIN.pluginId, + module: "categories", + permission: "can_edit", + }); + + if (!canEdit) return null; return ( diff --git a/plugins/blog/src/views/admin/posts/table/actions/delete/delete-action.tsx b/plugins/blog/src/views/admin/posts/table/actions/delete/delete-action.tsx index ed9a12689..340e2f894 100644 --- a/plugins/blog/src/views/admin/posts/table/actions/delete/delete-action.tsx +++ b/plugins/blog/src/views/admin/posts/table/actions/delete/delete-action.tsx @@ -1,6 +1,7 @@ "use client"; import { ConfirmActionAlertDialog } from "@vitnode/core/components/confirm-action/confirm-action-alert-dialog"; +import { useAdminStaffPermission } from "@vitnode/core/components/staff-permission/provider"; import { Button } from "@vitnode/core/components/ui/button"; import { Tooltip, @@ -12,11 +13,20 @@ import { Trash2Icon } from "lucide-react"; import { useTranslations } from "next-intl"; import { toast } from "sonner"; +import { CONFIG_PLUGIN } from "@/const"; + import { mutationApi } from "./mutation-api"; export const DeleteAction = ({ title, id }: { id: number; title: string }) => { const t = useTranslations("@vitnode/blog.admin.posts.delete"); const tGlobal = useTranslations("core.global"); + const canDelete = useAdminStaffPermission({ + plugin: CONFIG_PLUGIN.pluginId, + module: "posts", + permission: "can_delete", + }); + + if (!canDelete) return null; return ( diff --git a/plugins/blog/src/views/admin/posts/table/actions/edit-action.tsx b/plugins/blog/src/views/admin/posts/table/actions/edit-action.tsx index 15498bde7..f74c270ed 100644 --- a/plugins/blog/src/views/admin/posts/table/actions/edit-action.tsx +++ b/plugins/blog/src/views/admin/posts/table/actions/edit-action.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAdminStaffPermission } from "@vitnode/core/components/staff-permission/provider"; import { Button } from "@vitnode/core/components/ui/button"; import { Dialog, @@ -21,6 +22,8 @@ import { useTranslations } from "next-intl"; import dynamic from "next/dynamic"; import React from "react"; +import { CONFIG_PLUGIN } from "@/const"; + const CreateEditActionPostsAdmin = dynamic(async () => import("../../actions/create-edit/create-edit").then(mod => ({ default: mod.CreateEditActionPostsAdmin, @@ -31,6 +34,13 @@ export const EditAction = ( props: Required>, ) => { const t = useTranslations("@vitnode/blog.admin.posts.edit"); + const canEdit = useAdminStaffPermission({ + plugin: CONFIG_PLUGIN.pluginId, + module: "posts", + permission: "can_edit", + }); + + if (!canEdit) return null; return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c2a8ecd4..20e1f9397 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,10 +16,10 @@ importers: version: link:packages/config prettier: specifier: ^3.8.4 - version: 3.8.4 + version: 3.9.3 prettier-plugin-tailwindcss: specifier: ^0.8.0 - version: 0.8.0(prettier@3.8.4) + version: 0.8.0(prettier@3.9.3) tsx: specifier: ^4.22.4 version: 4.22.4 @@ -55,7 +55,7 @@ importers: version: 4.12.27 next-intl: specifier: ^4.13.0 - version: 4.13.0(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 4.13.0(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) react: specifier: ^19.2.7 version: 19.2.7 @@ -104,10 +104,10 @@ importers: version: 17.4.2 eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) react-email: specifier: ^6.6.4 - version: 6.6.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 6.6.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) tsc-alias: specifier: ^1.8.17 version: 1.8.17 @@ -140,28 +140,28 @@ importers: version: 0.45.2(@opentelemetry/api@1.9.1)(postgres@3.4.9) fumadocs-core: specifier: ^16.10.5 - version: 16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + version: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) fumadocs-mdx: specifier: ^15.0.12 - version: 15.0.12(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) fumadocs-ui: specifier: ^16.10.5 - version: 16.10.5(@tailwindcss/oxide@4.3.1)(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1) + version: 16.10.7(@tailwindcss/oxide@4.3.2)(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.2) hono: specifier: ^4.12.27 version: 4.12.27 lucide-react: specifier: ^1.21.0 - version: 1.21.0(react@19.2.7) + version: 1.22.0(react@19.2.7) motion: specifier: ^12.41.0 - version: 12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: - specifier: 16.3.0-preview.4 - version: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 16.3.0-preview.5 + version: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-intl: specifier: ^4.13.0 - version: 4.13.0(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 4.13.0(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) node-cron: specifier: ^4.5.0 version: 4.5.0 @@ -179,7 +179,7 @@ importers: version: 17.6.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) shadcn: specifier: ^4.11.0 - version: 4.11.0(typescript@6.0.3) + version: 4.12.0(typescript@6.0.3) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -195,7 +195,7 @@ importers: version: 1.0.12(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tailwindcss/postcss': specifier: ^4.3.1 - version: 4.3.1 + version: 4.3.2 '@types/mdx': specifier: ^2.0.14 version: 2.0.14 @@ -228,19 +228,19 @@ importers: version: 0.7.1 eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) postcss: specifier: ^8.5.15 - version: 8.5.15 + version: 8.5.16 react-email: specifier: ^6.6.4 - version: 6.6.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 6.6.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) shiki: specifier: ^4.3.0 version: 4.3.0 tailwindcss: specifier: ^4.3.1 - version: 4.3.1 + version: 4.3.2 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 @@ -258,41 +258,41 @@ importers: dependencies: '@eslint-react/eslint-plugin': specifier: ^5.9.2 - version: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + version: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.5.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.6.0(jiti@2.7.0)) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.5.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-jsx-a11y: specifier: ^6.10.2 - version: 6.10.2(eslint@10.5.0(jiti@2.7.0)) + version: 6.10.2(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-perfectionist: specifier: ^5.9.1 - version: 5.9.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + version: 5.9.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) eslint-plugin-prettier: specifier: ^5.5.6 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4) + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.7.0)))(eslint@10.6.0(jiti@2.7.0))(prettier@3.9.3) eslint-plugin-react-hooks: specifier: ^7.1.1 - version: 7.1.1(eslint@10.5.0(jiti@2.7.0)) + version: 7.1.1(eslint@10.6.0(jiti@2.7.0)) eslint-plugin-react-you-might-not-need-an-effect: specifier: ^1.0.1 - version: 1.0.1(eslint@10.5.0(jiti@2.7.0)) + version: 1.0.1(eslint@10.6.0(jiti@2.7.0)) prettier: specifier: ^3.x.x - version: 3.8.4 + version: 3.9.3 prettier-plugin-tailwindcss: specifier: ^0.8.0 - version: 0.8.0(prettier@3.8.4) + version: 0.8.0(prettier@3.9.3) typescript-eslint: specifier: ^8.62.0 - version: 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + version: 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) devDependencies: eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -329,7 +329,7 @@ importers: version: link:../config eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) typescript: specifier: ^6.0.3 version: 6.0.3 @@ -357,7 +357,7 @@ importers: version: 10.0.3 eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) tsc-alias: specifier: ^1.8.17 version: 1.8.17 @@ -391,7 +391,7 @@ importers: version: 10.0.3 eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) tsc-alias: specifier: ^1.8.17 version: 1.8.17 @@ -403,7 +403,7 @@ importers: dependencies: resend: specifier: ^6.14.0 - version: 6.14.0(@react-email/render@2.0.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) + version: 6.16.0(@react-email/render@2.0.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)) devDependencies: '@swc/cli': specifier: ^0.8.1 @@ -422,7 +422,7 @@ importers: version: 10.0.3 eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) tsc-alias: specifier: ^1.8.17 version: 1.8.17 @@ -437,7 +437,7 @@ importers: version: 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@bprogress/next': specifier: ^3.2.12 - version: 3.2.12(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.2.12(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -449,7 +449,7 @@ importers: version: 5.2.10(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/react-query': specifier: ^5.101.1 - version: 5.101.1(react@19.2.7) + version: 5.101.2(react@19.2.7) '@tiptap/extension-text-align': specifier: ^3.27.1 version: 3.27.1(@tiptap/core@3.27.1(@tiptap/pm@3.27.1)) @@ -488,7 +488,7 @@ importers: version: 1.4.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) motion: specifier: ^12.41.0 - version: 12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -506,10 +506,10 @@ importers: version: 10.0.1(@types/react@19.2.17)(react@19.2.7) react-resizable-panels: specifier: ^4.11.2 - version: 4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 4.12.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-scan: specifier: ^0.5.7 - version: 0.5.7(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(esbuild@0.27.7)(eslint@10.5.0(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + version: 0.5.7(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(esbuild@0.27.7)(eslint@10.6.0(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) recharts: specifier: 3.9.0 version: 3.9.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react-is@17.0.2)(react@19.2.7)(redux@5.0.1) @@ -518,7 +518,7 @@ importers: version: 0.0.1 shadcn: specifier: ^4.11.0 - version: 4.11.0(typescript@6.0.3) + version: 4.12.0(typescript@6.0.3) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 @@ -588,7 +588,7 @@ importers: version: 0.45.2(@opentelemetry/api@1.9.1)(postgres@3.4.9) eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) hono: specifier: ^4.12.27 version: 4.12.27 @@ -600,13 +600,13 @@ importers: version: 29.1.1 lucide-react: specifier: ^1.21.0 - version: 1.21.0(react@19.2.7) + version: 1.22.0(react@19.2.7) next: - specifier: 16.3.0-preview.4 - version: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 16.3.0-preview.5 + version: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-intl: specifier: ^4.13.0 - version: 4.13.0(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 4.13.0(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) react: specifier: ^19.2.7 version: 19.2.7 @@ -615,7 +615,7 @@ importers: version: 19.2.7(react@19.2.7) react-email: specifier: ^6.6.4 - version: 6.6.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 6.6.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-hook-form: specifier: ^7.80.0 version: 7.80.0(react@19.2.7) @@ -624,13 +624,13 @@ importers: version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) tailwindcss: specifier: ^4.3.1 - version: 4.3.1 + version: 4.3.2 tsc-alias: specifier: ^1.8.17 version: 1.8.17 tsup: specifier: ^8.5.1 - version: 8.5.1(@swc/core@1.15.43)(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) + version: 8.5.1(@swc/core@1.15.43)(jiti@2.7.0)(postcss@8.5.16)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0) tsx: specifier: ^4.22.4 version: 4.22.4 @@ -669,13 +669,13 @@ importers: version: 4.12.27 lucide-react: specifier: ^1.21.0 - version: 1.21.0(react@19.2.7) + version: 1.22.0(react@19.2.7) next: - specifier: 16.3.0-preview.4 - version: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + specifier: 16.3.0-preview.5 + version: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-intl: specifier: ^4.13.0 - version: 4.13.0(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) + version: 4.13.0(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3) react: specifier: ^19.2.7 version: 19.2.7 @@ -684,7 +684,7 @@ importers: version: 19.2.7(react@19.2.7) react-email: specifier: ^6.6.4 - version: 6.6.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 6.6.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react-hook-form: specifier: ^7.80.0 version: 7.80.0(react@19.2.7) @@ -721,7 +721,7 @@ importers: version: 10.0.3 eslint: specifier: ^10.5.0 - version: 10.5.0(jiti@2.7.0) + version: 10.6.0(jiti@2.7.0) tsc-alias: specifier: ^1.8.17 version: 1.8.17 @@ -962,8 +962,8 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} engines: {node: '>=20.19.0'} '@csstools/css-calc@3.2.1': @@ -973,8 +973,8 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.1.8': - resolution: {integrity: sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==} + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -986,8 +986,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.5': - resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -1829,50 +1829,50 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-react/ast@5.9.2': - resolution: {integrity: sha512-206StJvea00Bs9etMOEG94muuBP/gQ6NPK2Tg/m/Dbx1o3hEOpoblxKqBF1jYx91C1DIrbuzqaqI1N/XTfGYxw==} + '@eslint-react/ast@5.10.0': + resolution: {integrity: sha512-8AZj8ZkRIwLnsy9dA7A9aJJp0rjURKa18/rZU7I37akXVpRBpQAui3+Z7IfbSH5t5B3y3vIM96kpYfc5RiYXYw==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - '@eslint-react/core@5.9.2': - resolution: {integrity: sha512-GcXEaMAyjFgbIP7g1TQ+p7VohhnM4g1wtEccj4MNXb1jzTKioPcWxRWN95lrBnrCYskvZXsPCWM4ERQjMQGU2g==} + '@eslint-react/core@5.10.0': + resolution: {integrity: sha512-brqXjHlQV9WD337ksz8u9g4z70czWTeLBA74cdPvZ32IlYuIYTIPu/R5j8MXLQhaTAJmC6+H3RN+EjgrTfFvRw==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - '@eslint-react/eslint-plugin@5.9.2': - resolution: {integrity: sha512-o1rSyib/uWlWU8qPg6E1U0ME/mrS/YZYSr4ruSw7dGoQTNZBOFVs0T0KivioK5YfLksHH2ynPOCn6w4EUnH47w==} + '@eslint-react/eslint-plugin@5.10.0': + resolution: {integrity: sha512-wIlaphThT/ld61VYc1Sss8U7KmOsv2pgwV9ucSWRJSzqQ+DkXsoeMbv/TvsnVV39Lq6wfWnBt8r7F9EnmtNpqA==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - '@eslint-react/eslint@5.9.2': - resolution: {integrity: sha512-8Fr+dqE8NoB7XRlp8AQp/IE5koQYxprXYAzktCmySVtwH6/I2HDQsVy/wcNMuIcCM7EPkiAbtl9MOiFvcbTAkw==} + '@eslint-react/eslint@5.10.0': + resolution: {integrity: sha512-iHoMYyLdrzQJcZESTJ9xa56CzrKR0gmJyQB6KAz95b0zjKY9VCpKufnZ6ZlPQ33p2IGQq8A30uk3KLULYCgGNw==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - '@eslint-react/jsx@5.9.2': - resolution: {integrity: sha512-rag1x+7lZHDOTT8WfWeS22fymh5JVr11O8m2SptTyI68ao0TWjTc5+BBHtv/lvQlea+VZpRH1n4pZV3e4Hkspw==} + '@eslint-react/jsx@5.10.0': + resolution: {integrity: sha512-jrZNSItx/OFVFNyax2sU2QXjxAfDazKGOLt3bWdjpU7Vz46LkdPwW1MfgK9qB74KiGq7uZRtpjqzaTF4atzsvg==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - '@eslint-react/shared@5.9.2': - resolution: {integrity: sha512-NU3pHMA3iADBH7HEPql/KSZTgooGp1HShT6TyeG46TApA42Z+X+gBk5MFmwazF/HPd/q3T2N6KU5gLWRNqXgng==} + '@eslint-react/shared@5.10.0': + resolution: {integrity: sha512-FNaKRqoNANfVlrXi/uuFPIs9S9yO4WLgKTAFOc3V1xNa9hRtRkKkii+cQqwlYDBuHlMIw9UMQoImnyO4AWiDDg==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - '@eslint-react/var@5.9.2': - resolution: {integrity: sha512-9+J8GmsKi3diHF2Ij++vb5HVs6IO9rbLUs2i4pzCeqrGZstyrTVp3BMSSzn2GamRODCU9Zz/Shl8vhwrSkqYsQ==} + '@eslint-react/var@5.10.0': + resolution: {integrity: sha512-hApDw4o1xzc+4sDXJ/IWuUpG46FjgdX4bH15/VIT+qM4oxIta9aWuxn4WrcXDhEzkL4y75WRAVD0CugZg6Ww2g==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' @@ -1934,8 +1934,8 @@ packages: '@formatjs/fast-memoize@3.1.6': resolution: {integrity: sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==} - '@formatjs/icu-messageformat-parser@3.5.11': - resolution: {integrity: sha512-NVsuNsc2dUVG9+4HBJ/srScxtA/18LqGgwtop/tuN/OIBjVl6QA+0KhfZQddDD9sEh2LeVjLFPGVU3ixa3blcA==} + '@formatjs/icu-messageformat-parser@3.5.12': + resolution: {integrity: sha512-YyzzxVgYJ8DELmmkhn0Yr0rUj0dTJFf9Jp628K3S0ysInBWxLVDOS8i3RP91cCp4DMK4WYb4cVMhWA9i4knSJg==} '@formatjs/icu-skeleton-parser@2.1.10': resolution: {integrity: sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==} @@ -2466,8 +2466,8 @@ packages: '@next/env@16.1.7': resolution: {integrity: sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==} - '@next/env@16.3.0-preview.4': - resolution: {integrity: sha512-2qYk8SAAHfyhJSRyIz6kbpZpkcYPrkE5WiUnYs9G+E6/Cm+oF8FUbaHBOp3pNHfZlI8E14JEuLAJu5kG9YUd2Q==} + '@next/env@16.3.0-preview.5': + resolution: {integrity: sha512-XqdVR0utAWMsVc1OIyO48D32vrdmC4/uAgI3Ds088YlOO4vfGKXXVyvkGFkOZkOK0xg7bNYNfJAarX4A0tYqGg==} '@next/swc-darwin-arm64@16.1.7': resolution: {integrity: sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==} @@ -2475,8 +2475,8 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.3.0-preview.4': - resolution: {integrity: sha512-OzChI+GSoolI2F7nH+26tbb7gbVpoPr2w4gDjKF+DHhqtPzLJtZU1Mhx2lbqmx8akFok0gdHSUAzBSErUcsN3A==} + '@next/swc-darwin-arm64@16.3.0-preview.5': + resolution: {integrity: sha512-PPWAJGoIkzVpz5hOD9V/qGNdkBuWj3QXhjQU8BQ1FXlMy6xsy4+aD/3UoasKy/HYInW4h1LqdQtDhiQkLYrrMA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -2487,8 +2487,8 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.3.0-preview.4': - resolution: {integrity: sha512-oHngcIe3PEjqdmcf59Nf84ZySlC09IkfzIv20xhc2XjN4ZdiUJ5AmbuuDOcKDfV2RlFSKYerKB1vmMRSAizY8w==} + '@next/swc-darwin-x64@16.3.0-preview.5': + resolution: {integrity: sha512-UPN/RS1H+kr9fgJrbFoH7bs1b9q2/G5cFe+uUf0nP4Hlgfl8NzfTBHEJKTfLAGqi1Qemwuyd29pvRy2vwEjL5g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -2500,8 +2500,8 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-arm64-gnu@16.3.0-preview.4': - resolution: {integrity: sha512-PAENKuBD1NVHhTsa7tuxZ++DH1yxgboXOVQbUWh+as+5yq1OZGJZ28XeLtBX7gOWvN0L7bteGjwEGiY7Be2nbQ==} + '@next/swc-linux-arm64-gnu@16.3.0-preview.5': + resolution: {integrity: sha512-kh+bKgk9ZIlmxMkEPnQZXtKc7/AyUyIS9jXgbKt4hWyxXEEZVDmXhiU2bh1zZpthMr/l09wz9z6CvfXtCWUJBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2514,8 +2514,8 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-arm64-musl@16.3.0-preview.4': - resolution: {integrity: sha512-uIBRfn5rPKW1CpxK2MCB9eHqiimhqfQiUm3tFa3/r9DxxBBp2Y8WlzL/WTfQ8uTvZK7gLO3SIn8th71oGQN/8A==} + '@next/swc-linux-arm64-musl@16.3.0-preview.5': + resolution: {integrity: sha512-m09/acXFGhlp+U6m7Wn0AqsmLqars3qI9eBXDpPJm4h/XVS9HPHNzWGy2BI7F1iLoFX59Uy0tcau9ey7JVud3w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -2528,8 +2528,8 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-x64-gnu@16.3.0-preview.4': - resolution: {integrity: sha512-hgSnVd7e6WTFiI/uqkkAKxLRn88TmgbBky0SGiUiBvC9sfEgds+xEGp5Xw/u9iQJnnwQeD8Fxm28sWAEwV95jw==} + '@next/swc-linux-x64-gnu@16.3.0-preview.5': + resolution: {integrity: sha512-/EBiqRjLZJWJo6Keq9upJfhrP+tNpePy1beBfOL+tUn68inwNiJEjx+0Lgve99Zur8kSk9TgSmDmwgQxX4iM+g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2542,8 +2542,8 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-x64-musl@16.3.0-preview.4': - resolution: {integrity: sha512-8vthKq4yzshpmZV5yfKUlXBpiXaXMShxqfnKSXYAzW5Qv0k95RYCNfKx2+H4JEvZIRRhueqDN0B374sTPoickA==} + '@next/swc-linux-x64-musl@16.3.0-preview.5': + resolution: {integrity: sha512-lUCiPFoecSGkM8aeY6UAgQDiJjR3DhPsI036mznlHFg89ZLoeRdo521N4nmk6EpbPpNzRujgiboBkbuyexDgCg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -2555,8 +2555,8 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.3.0-preview.4': - resolution: {integrity: sha512-UfPDgJoqVMsiBOIRjdcAlz5Re1RjXUsD+t+gpc79CNVMuELlpPAusBoxGbBzuDY666nntyPoCOPuYpqYNBWwFw==} + '@next/swc-win32-arm64-msvc@16.3.0-preview.5': + resolution: {integrity: sha512-Nr4e3dRB86gElIgysL/L7dr9tuRLIq3looK8hLxnYDLUvLza2Tu/7Ik/X6DSRGejIrbZsYjnH3S4xYeAAf7Prw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -2567,8 +2567,8 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.0-preview.4': - resolution: {integrity: sha512-QeGYYUN2FSgXShi/dFG96lWaKmi8uJ/ej4sY+SJYOU/8lkrfurOFTm9qDpR+jhbdSspUENz1EXTCKLDRmLxtBg==} + '@next/swc-win32-x64-msvc@16.3.0-preview.5': + resolution: {integrity: sha512-Svg+VCRUbyNsBuh96hN+1ael8dNXqVQVZqOe9tqFlF4mUzIk5CQFcn5VsZPrz8GNP9HCxJfrfy3PM0cXoSXliw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4374,12 +4374,12 @@ packages: resolution: {integrity: sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g==} engines: {node: '>=14'} - '@sentry/core@10.60.0': - resolution: {integrity: sha512-szN7ccOJAEaLb1BBQzCQhABGMTJmKNUk0G2sc7rWhajeXoZoMKIbNkI9RvJrFuV69cbad/d/BKGBjbpJhySAzw==} + '@sentry/core@10.62.0': + resolution: {integrity: sha512-tV69fMg2sS5DUFmQSnS7Jd5qJAp0izxwcsvBVz2ieTM9VMRi99IfOSYW9UYr3p1yfuksk41kefN5PEbeedUE+A==} engines: {node: '>=18'} - '@sentry/node-core@10.60.0': - resolution: {integrity: sha512-aXi9ixvP+hgUZPPZCRwMNHgY2I0gkSeoAKAUuysDJhWDmrygwfGdlkbGmmtW6PQjtMYFx69Igt5btvhjEBoJTw==} + '@sentry/node-core@10.62.0': + resolution: {integrity: sha512-V7rDgbxViiHU0OpcFEDp3l41IFvWTasKHfXw8SQ6yIgtZ8VpFqmz2TR5N7X85iIOmWIvK5HV0yp0eDdsly0+rA==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -4399,20 +4399,20 @@ packages: '@opentelemetry/sdk-trace-base': optional: true - '@sentry/node@10.60.0': - resolution: {integrity: sha512-u//paUrkKaCr0oNn7r7UulGydkYMSkU1wQOIpG/P/jf7psZWnyXhgeszHzUfZXo6pCdxXG9z9viPvzGjqPQN7A==} + '@sentry/node@10.62.0': + resolution: {integrity: sha512-4hoU67bJY0o3irEDMZu2UIztAOsvEqFkLXA7EUKl1LXMA3Ba1Lb32OUVqlsTypiEInSDs/BtM+aAFKojZ3P3Fw==} engines: {node: '>=18'} - '@sentry/opentelemetry@10.60.0': - resolution: {integrity: sha512-gl+2NVH+9RmTu7pd9kV1tKif+Th+p9tmnXR1l3Sb3Wqo1ir5FaNMKrloWEKMXjnepii9EJUrEHdSC+i8NoexxQ==} + '@sentry/opentelemetry@10.62.0': + resolution: {integrity: sha512-nFwBgtjfwgY8P5lAuQFWfAsQW1MXxuQ6kR/HtBs+A6julqwGGS2QnQ65OCWMzz6IqDEL/pRgT1405/gU+OXU3A==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 '@opentelemetry/core': ^1.30.1 || ^2.1.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@sentry/server-utils@10.60.0': - resolution: {integrity: sha512-SX+MzWM3nz5ttKT48rlfktm0ERyIpDLma+b6pYeWgW2oFHKcpIu0g0qMGJrZs4lKM3MlgV7IqLa4texMqTp9kQ==} + '@sentry/server-utils@10.62.0': + resolution: {integrity: sha512-S5szsj6kKBhxw97b2HA98fYp/PpWXvSizlisEzb2rnL4IH6RAJ8wP05/fnth8pSywTH+gtUu+i6Wn8e8rX5HvA==} engines: {node: '>=18'} '@shikijs/core@4.3.0': @@ -4573,69 +4573,69 @@ packages: '@swc/types@0.1.27': resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} - '@tailwindcss/node@4.3.1': - resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + '@tailwindcss/node@4.3.2': + resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} - '@tailwindcss/oxide-android-arm64@4.3.1': - resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} + '@tailwindcss/oxide-android-arm64@4.3.2': + resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.1': - resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} + '@tailwindcss/oxide-darwin-arm64@4.3.2': + resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.1': - resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} + '@tailwindcss/oxide-darwin-x64@4.3.2': + resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.1': - resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} + '@tailwindcss/oxide-freebsd-x64@4.3.2': + resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': - resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': - resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.1': - resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.1': - resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.1': - resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.1': - resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4646,30 +4646,30 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': - resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.1': - resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.1': - resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} + '@tailwindcss/oxide@4.3.2': + resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} engines: {node: '>= 20'} - '@tailwindcss/postcss@4.3.1': - resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==} + '@tailwindcss/postcss@4.3.2': + resolution: {integrity: sha512-rjVWYCa7Ngbi5AarT6k8TkxUG3Wl1QKzHdIZVsjZSzf36Jmo2IKZt/NHRAwly8oDkbBOH0YTu+CHuf9jPxMc+g==} - '@tanstack/query-core@5.101.1': - resolution: {integrity: sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} - '@tanstack/react-query@5.101.1': - resolution: {integrity: sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==} + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} peerDependencies: react: ^18 || ^19 @@ -5179,10 +5179,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - agent-install@0.0.5: resolution: {integrity: sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ==} hasBin: true @@ -5355,8 +5351,8 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.10.38: - resolution: {integrity: sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==} + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} hasBin: true @@ -5393,8 +5389,8 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -5541,6 +5537,10 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + cnfast@0.0.8: + resolution: {integrity: sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q==} + hasBin: true + code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} @@ -5739,10 +5739,6 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -6001,8 +5997,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.378: - resolution: {integrity: sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==} + electron-to-chromium@1.5.381: + resolution: {integrity: sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==} embla-carousel-react@8.6.0: resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} @@ -6085,8 +6081,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-module-lexer@2.2.0: + resolution: {integrity: sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==} es-object-atoms@1.1.2: resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} @@ -6100,12 +6096,12 @@ packages: resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} engines: {node: '>= 0.4'} - es-to-primitive@1.3.1: - resolution: {integrity: sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==} + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} engines: {node: '>= 0.4'} - es-toolkit@1.48.1: - resolution: {integrity: sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==} + es-toolkit@1.49.0: + resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -6185,8 +6181,8 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-react-dom@5.9.2: - resolution: {integrity: sha512-9pOLfUWBSR49OLZxCIDLngyfqOozsoilPl1Tv3pxoW389AVB/Gg3So4Rf+UPpQtEjJP6840hnTZkmY+A44umng==} + eslint-plugin-react-dom@5.10.0: + resolution: {integrity: sha512-GE47tO78o0I+XqT0Et07BnTRI9x5bCRA1tRDraTc7CEjUTCu0FHyTjAYr0ZMwdrLSJib0OXjirD7GDp2MM7lYA==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' @@ -6198,36 +6194,36 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 - eslint-plugin-react-jsx@5.9.2: - resolution: {integrity: sha512-LPogjhB5FevfPp7dUdh2qrku+DbWvVuqWYwO4Kb9ty1RYKQN9DsZx1Db1WVmd8x/W2NBDLgzkupH76SJ+ukR1w==} + eslint-plugin-react-jsx@5.10.0: + resolution: {integrity: sha512-L0jKCE9zBzFqUt0zAJGga4VyUM8wEgBMenApllXCHCTxCUW7w9dqgl3p73qgYT5xa4oHJDXTDGyAzKO7p/kgeQ==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - eslint-plugin-react-naming-convention@5.9.2: - resolution: {integrity: sha512-ODgENIpcxYoE4SjvlyA7loPVTjcphpU2Y2jehdRI6LZluxmtkUgKTJc8MAk/vSCJoKfWK6+kVu7oMqGAyW/wuQ==} + eslint-plugin-react-naming-convention@5.10.0: + resolution: {integrity: sha512-RsXF/F/3nUtYylmpIRTggPEmw7qRPZItqNLt04AV84rqNY0/S5fVm0dqn39VoBbW+B8v23O1Ve8pvk0aib1N2g==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - eslint-plugin-react-rsc@5.9.2: - resolution: {integrity: sha512-jb5nqTKn7ODscQWffvdUIy4qE+QJIL2tHY+7NlPFjfduPhnw2Daphx7qW0qfX5qSe1rcxQCBMDiEkS2/H6lgIA==} + eslint-plugin-react-rsc@5.10.0: + resolution: {integrity: sha512-7dXZFg9p8u+J27cu8oKwrpw+KVEZSPASMluGsd05GfKPRE9HlN7zTA0fVb15lfeIGQ/JPp5DfZp3Nd4LlDdcJw==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - eslint-plugin-react-web-api@5.9.2: - resolution: {integrity: sha512-uw/yBHdciPPsYEiuBABLfKYOtPaCBJmcKwxybEdKqIZFTCAweVlRqqucNulKEsWE1CnA5p6e25AyzUMB8xBgMw==} + eslint-plugin-react-web-api@5.10.0: + resolution: {integrity: sha512-VCX8fS6kFTTpC+XWD6NqdMK2PKDV1YgpDcMA/jTaWn3Goypui0kZUBUDF7kApq7zsHp3zAWmt5UexvuSsOqyRQ==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' typescript: '*' - eslint-plugin-react-x@5.9.2: - resolution: {integrity: sha512-aex/DzgcGdYF46LKShrTYVmZFWEd7lrX9PD85pADbTUmFpQoovGFQOK3nO6uqPUCrMuG97g/v66RyfzPBx0g5A==} + eslint-plugin-react-x@5.10.0: + resolution: {integrity: sha512-ekvV8vYLp62dO3558ArIp+7oQLvd0jOJPvoxomzNBOJmiekFaVJifxaCxkcRB1l6yI4Rs2b9lnm5M+/YDTzoSQ==} engines: {node: '>=22.0.0'} peerDependencies: eslint: '*' @@ -6251,8 +6247,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.5.0: - resolution: {integrity: sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==} + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -6343,8 +6339,8 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} express-rate-limit@8.5.2: @@ -6403,8 +6399,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -6424,10 +6420,6 @@ packages: picomatch: optional: true - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -6486,16 +6478,12 @@ packages: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - framer-motion@12.41.0: - resolution: {integrity: sha512-OHAMNiCEON1RDBlRGuulsN5AD8ptMjvk5QWfFmYmBLPZ3zFGIJe60kQucQQf4cez1OzQmjYBWDY+dYfISkUdqg==} + framer-motion@12.42.0: + resolution: {integrity: sha512-wp7EJnfWaaEScVygKv3e20udoRz+LbtxScsuTkakAxfXmt+ReC6WyPW2nINRAGvd+hG9odwcjBLyOTPjH5pBRA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -6526,8 +6514,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.10.5: - resolution: {integrity: sha512-e/xrZnKvQo8bF/WYMwPuym8PR3OtjZzHy0S/EIOvGwjKRgVq9z6J58zaBpi4LvYtPVZxNGsxdZVlmZXCVWq4FQ==} + fumadocs-core@16.10.7: + resolution: {integrity: sha512-lR1hDOtJ8ubsLKYH2VMkp+iVZTDdOiMh4StFaWtuTvy8Wfnngz0YSHeFijmm2K+4lg2DLXLMuCEHQ6my54g2Eg==} peerDependencies: '@mdx-js/mdx': '*' '@mixedbread/sdk': 0.x.x @@ -6585,8 +6573,8 @@ packages: zod: optional: true - fumadocs-mdx@15.0.12: - resolution: {integrity: sha512-R4WenrNQxSKi+QU46Q1cscVWi+S90dj3As4jdN+vgChO2o0TVOj+FFIe3onWM7mglhPj53NxZp/upP+t/ryekQ==} + fumadocs-mdx@15.0.13: + resolution: {integrity: sha512-VsGhCiLriXXMzm3WbgrVP7t6LvOthwh1BC+IGSI1ZW63UcSo1jE4aAiuUrTIF0jv1EGQkJG8cPsy0cOnf4sejA==} hasBin: true peerDependencies: '@types/mdast': '*' @@ -6616,13 +6604,13 @@ packages: vite: optional: true - fumadocs-ui@16.10.5: - resolution: {integrity: sha512-vd69ckYx/4a1aoJTCUJ5LBkqNeOFxm3r+8SK9bVYaeHJrY/n8+4W6b0soqxVqgj1UwNmgovoAg0vlsYmSxZBgQ==} + fumadocs-ui@16.10.7: + resolution: {integrity: sha512-zE93/DKW5bhedXRKHYg3rhE/juYi+kXx1xl3ey1dArWbCiPx6lq6/4RLswYXS0lQp1W1f8NsbY1TeA8wSQuEvw==} peerDependencies: '@takumi-rs/image-response': '*' '@types/mdx': '*' '@types/react': '*' - fumadocs-core: 16.10.5 + fumadocs-core: 16.10.7 next: 16.x.x react: ^19.2.0 react-dom: ^19.2.0 @@ -6842,10 +6830,6 @@ packages: resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} engines: {node: '>=10.19.0'} - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -6923,8 +6907,8 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - intl-messageformat@11.2.8: - resolution: {integrity: sha512-l323RCl3qJDVQ8U9j74ut/hVMdg3VPsOHpVMDvFfz9qiq4dPO5ooVYFNVUzzrpgG39a+RLzcXyJb8VFgIU+tUA==} + intl-messageformat@11.2.9: + resolution: {integrity: sha512-cGzymZerpDhVXRKjKLgXKda9gI29TU2o88L7gwNMHp3WZVxA/0c5tX52udXbW9JklDApolvMXZG6Dhhdz5eirA==} ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} @@ -7184,8 +7168,12 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.2.0: - resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + js-yaml@5.2.0: + resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==} hasBin: true jsdom@29.1.1: @@ -7388,8 +7376,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.21.0: - resolution: {integrity: sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==} + lucide-react@1.22.0: + resolution: {integrity: sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -7673,14 +7661,14 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - motion-dom@12.41.0: - resolution: {integrity: sha512-Lk3J39fOGg6xNr1KRZsN6usDyBf8aP7MEbUPez1VCughHt79OrP7VGqNrPyFL0riaT7WS8t9DRw1M3BHtM/xKw==} + motion-dom@12.42.0: + resolution: {integrity: sha512-M63h4n8R+quJdNhBwuLlgxM+OLYa9+I/T2pzDRboB9fLXRdbou+Gw7Zury+SkpaCyACP1JHSjHgZ1EgTkBr30w==} motion-utils@12.39.0: resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} - motion@12.41.0: - resolution: {integrity: sha512-avEDKE22rFPJqDr3Ttk7gMQpeaOmNik60NoJ5T0tj+RBCNvz21D3ArY3l4uitoeQ7eIpDqueWaO3pPYFv8JOVA==} + motion@12.42.0: + resolution: {integrity: sha512-Qhwvu9sVl5/URSq5CNzwMCpSKK8Uhnrwb6VO977kZyj/wOCS7mWebJUnBoHx5cZU1Zv8a9BD5CSICWKAlrLJgA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -7777,8 +7765,8 @@ packages: sass: optional: true - next@16.3.0-preview.4: - resolution: {integrity: sha512-jO71gSFyjMDqbHblZH2JH2zFyTyZpbgY9svpdtZiJo5NQK2KVb1xBSZkjMgkwupNVtQfTi4i0UpsFFRM+m0PLA==} + next@16.3.0-preview.5: + resolution: {integrity: sha512-I5rVC4VcvAL1FPr6AY5WEQUSe6o1Bt0Oa/qH5hfPhci4FRMCPeAQ95tgxFOgJDk2wME1K009k0bjS17nQ0Bq1w==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7805,15 +7793,6 @@ packages: resolution: {integrity: sha512-4Trh+kjvbXokyJkwQumvD5YAgeJfgHLR/sKyu71uSmxfCR5QMO1hldpvmFZOICN5pLgNY+J5Y8+ar3XKo5/4tQ==} engines: {node: '>=20'} - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.50: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} @@ -8152,8 +8131,8 @@ packages: resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} postgres@3.4.9: @@ -8164,8 +8143,8 @@ packages: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} - preact@10.29.2: - resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + preact@10.29.3: + resolution: {integrity: sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -8230,8 +8209,8 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.8.4: - resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + prettier@3.9.3: + resolution: {integrity: sha512-HWmu+K+zvHNpaMfSnYeqdqrDbR16cuIXaPx8WoHaviQkDJh1/0BNtOZmHVQI5jc3wXv0H1yXc9wjvFdXh+n3hQ==} engines: {node: '>=14'} hasBin: true @@ -8329,8 +8308,8 @@ packages: '@types/react-dom': optional: true - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} rate-limiter-flexible@11.2.0: @@ -8360,8 +8339,8 @@ packages: peerDependencies: react: ^19.2.7 - react-email@6.6.4: - resolution: {integrity: sha512-1FFLEcPDBbUVqKzc04g5Xt6rScYlWf5D6xQlT4YT3z8L57WEmeiYXGtCbYylIgooeRCPdvSlKVt4A94uJ08/iA==} + react-email@6.6.5: + resolution: {integrity: sha512-IO2NXS17K5xEn9v8QVt28g8Nl6D4gmaKZZc61tOGiZla4X2F+veWjuSKCJC7HDIuEtZXF27chHo9sE6Mtey6tQ==} engines: {node: '>=20.0.0'} hasBin: true peerDependencies: @@ -8418,8 +8397,8 @@ packages: '@types/react': optional: true - react-resizable-panels@4.11.2: - resolution: {integrity: sha512-+kfFbDZ8mygc7g0vxOcDzCVGuwiIUOnILqPoUHo6/uP+Mmyx6HzZU+kj1aOPDlktXuobYbr6BtQekvJwHRX4Eg==} + react-resizable-panels@4.12.0: + resolution: {integrity: sha512-t/Gp57qSCxGQ52ckhz+8lM7dnuymeU95TEzl2U203qEbGkSLHrtm7US2/ANzq/zOlja3CwPTAfCDuh1unv9mfw==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -8473,8 +8452,8 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + recast@0.23.12: + resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} engines: {node: '>= 4'} recharts@3.9.0: @@ -8559,8 +8538,8 @@ packages: reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} - resend@6.14.0: - resolution: {integrity: sha512-jVdpUgOoWGLjaP64lo8KwzHT9gY4w6Dl8c36CIb2F+ayYOMLr3khqs8xrNjXM2k19b+lPoj0VWQFhVNLiToBjA==} + resend@6.16.0: + resolution: {integrity: sha512-SaKISwHtxvAxneF84Njgnzg+zdngUu1vOT/paRU1De9QF+zXQR3GnwJHSh+mpJPjUhsGD4WxYi5CfKkXMkDqwg==} engines: {node: '>=20'} peerDependencies: '@react-email/render': '*' @@ -8713,8 +8692,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@4.11.0: - resolution: {integrity: sha512-UV0cchFea9hO7poV1CuEP0wvmYjpAqcxCKdy23bndl2Du2ARtDs8A4xdzfhUjDBeOW1nNpJ6lXmsEpsply2SfQ==} + shadcn@4.12.0: + resolution: {integrity: sha512-o781ieQziCnXH2FKsEqxp1fnbHdbgAPO9inTSPeZ59hQfsZXuMGp3ul8oFSV5KQS4nbUK9b+DrDE6C7OvfKKQQ==} + engines: {node: '>=20.18.1'} hasBin: true sharp@0.34.5: @@ -8995,8 +8975,8 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwindcss@4.3.1: - resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + tailwindcss@4.3.2: + resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -9047,11 +9027,11 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@7.4.4: - resolution: {integrity: sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==} + tldts-core@7.4.5: + resolution: {integrity: sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==} - tldts@7.4.4: - resolution: {integrity: sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==} + tldts@7.4.5: + resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==} hasBin: true to-regex-range@5.0.1: @@ -9256,8 +9236,8 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin@3.2.0: - resolution: {integrity: sha512-6nGlT7EHsS+tTcTdAkYFqXIUwDrMJyJvHFNYGSr4x2/2ySIcV4f5e1RAJUeDyfOJPR8TF0auE8l+82PLhKjqsA==} + unplugin@3.3.0: + resolution: {integrity: sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@farmfe/core': '*' @@ -9481,10 +9461,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} @@ -9651,7 +9627,7 @@ snapshots: '@apm-js-collab/code-transformer-bundler-plugins@0.5.0': dependencies: '@apm-js-collab/code-transformer': 0.15.0 - es-module-lexer: 2.1.0 + es-module-lexer: 2.2.0 magic-string: 0.30.21 module-details-from-path: 1.0.4 @@ -9676,7 +9652,7 @@ snapshots: dependencies: '@asamuzakjp/generational-cache': 1.0.1 '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -9932,11 +9908,11 @@ snapshots: '@bprogress/core@1.3.4': {} - '@bprogress/next@3.2.12(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@bprogress/next@3.2.12(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@bprogress/core': 1.3.4 '@bprogress/react': 1.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - next: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -9950,16 +9926,16 @@ snapshots: dependencies: css-tree: 3.2.1 - '@csstools/color-helpers@6.0.2': {} + '@csstools/color-helpers@6.1.0': {} '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 6.0.2 + '@csstools/color-helpers': 6.1.0 '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -9968,7 +9944,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -10450,95 +10426,95 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.5.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.7.0))': dependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint-react/ast@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/ast@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.62.0 '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) string-ts: 2.3.1 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/core@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/core@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/jsx': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/var': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/eslint-plugin@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/eslint-plugin@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) - eslint-plugin-react-dom: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-react-jsx: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-react-naming-convention: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-react-rsc: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-react-web-api: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint-plugin-react-x: 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) + eslint-plugin-react-dom: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-jsx: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-naming-convention: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-rsc: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-web-api: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint-plugin-react-x: 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/eslint@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/eslint@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/jsx@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/jsx@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/var': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@eslint-react/shared@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/shared@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 zod: 4.4.3 transitivePeerDependencies: - supports-color - '@eslint-react/var@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@eslint-react/var@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: @@ -10560,9 +10536,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.5.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.6.0(jiti@2.7.0))': optionalDependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) '@eslint/object-schema@3.0.5': {} @@ -10592,7 +10568,7 @@ snapshots: '@formatjs/fast-memoize@3.1.6': {} - '@formatjs/icu-messageformat-parser@3.5.11': + '@formatjs/icu-messageformat-parser@3.5.12': dependencies: '@formatjs/icu-skeleton-parser': 2.1.10 @@ -10609,10 +10585,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - '@fumadocs/tailwind@0.0.5(@tailwindcss/oxide@4.3.1)(tailwindcss@4.3.1)': + '@fumadocs/tailwind@0.0.5(@tailwindcss/oxide@4.3.2)(tailwindcss@4.3.2)': optionalDependencies: - '@tailwindcss/oxide': 4.3.1 - tailwindcss: 4.3.1 + '@tailwindcss/oxide': 4.3.2 + tailwindcss: 4.3.2 '@hono/node-server@1.19.14(hono@4.12.27)': dependencies: @@ -11053,54 +11029,54 @@ snapshots: '@next/env@16.1.7': {} - '@next/env@16.3.0-preview.4': {} + '@next/env@16.3.0-preview.5': {} '@next/swc-darwin-arm64@16.1.7': optional: true - '@next/swc-darwin-arm64@16.3.0-preview.4': + '@next/swc-darwin-arm64@16.3.0-preview.5': optional: true '@next/swc-darwin-x64@16.1.7': optional: true - '@next/swc-darwin-x64@16.3.0-preview.4': + '@next/swc-darwin-x64@16.3.0-preview.5': optional: true '@next/swc-linux-arm64-gnu@16.1.7': optional: true - '@next/swc-linux-arm64-gnu@16.3.0-preview.4': + '@next/swc-linux-arm64-gnu@16.3.0-preview.5': optional: true '@next/swc-linux-arm64-musl@16.1.7': optional: true - '@next/swc-linux-arm64-musl@16.3.0-preview.4': + '@next/swc-linux-arm64-musl@16.3.0-preview.5': optional: true '@next/swc-linux-x64-gnu@16.1.7': optional: true - '@next/swc-linux-x64-gnu@16.3.0-preview.4': + '@next/swc-linux-x64-gnu@16.3.0-preview.5': optional: true '@next/swc-linux-x64-musl@16.1.7': optional: true - '@next/swc-linux-x64-musl@16.3.0-preview.4': + '@next/swc-linux-x64-musl@16.3.0-preview.5': optional: true '@next/swc-win32-arm64-msvc@16.1.7': optional: true - '@next/swc-win32-arm64-msvc@16.3.0-preview.4': + '@next/swc-win32-arm64-msvc@16.3.0-preview.5': optional: true '@next/swc-win32-x64-msvc@16.1.7': optional: true - '@next/swc-win32-x64-msvc@16.3.0-preview.4': + '@next/swc-win32-x64-msvc@16.3.0-preview.5': optional: true '@nodelib/fs.scandir@2.1.5': @@ -11474,10 +11450,10 @@ snapshots: '@preact/signals-core@1.14.3': {} - '@preact/signals@2.9.2(preact@10.29.2)': + '@preact/signals@2.9.2(preact@10.29.3)': dependencies: '@preact/signals-core': 1.14.3 - preact: 10.29.2 + preact: 10.29.3 '@radix-ui/number@1.1.2': {} @@ -12328,14 +12304,14 @@ snapshots: '@react-email/render@2.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: html-to-text: 9.0.5 - prettier: 3.8.4 + prettier: 3.9.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) '@react-email/render@2.0.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: html-to-text: 9.0.5 - prettier: 3.8.4 + prettier: 3.9.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -12351,7 +12327,7 @@ snapshots: dependencies: '@react-email/text': 0.1.6(react@19.2.7) react: 19.2.7 - tailwindcss: 4.3.1 + tailwindcss: 4.3.2 optionalDependencies: '@react-email/body': 0.3.0(react@19.2.7) '@react-email/button': 0.2.1(react@19.2.7) @@ -12536,13 +12512,13 @@ snapshots: '@sentry/conventions@0.12.0': {} - '@sentry/core@10.60.0': {} + '@sentry/core@10.62.0': {} - '@sentry/node-core@10.60.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))': + '@sentry/node-core@10.62.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))': dependencies: '@sentry/conventions': 0.12.0 - '@sentry/core': 10.60.0 - '@sentry/opentelemetry': 10.60.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) + '@sentry/core': 10.62.0 + '@sentry/opentelemetry': 10.62.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) import-in-the-middle: 3.2.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -12550,37 +12526,37 @@ snapshots: '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) - '@sentry/node@10.60.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))': + '@sentry/node@10.62.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.41.1 - '@sentry/core': 10.60.0 - '@sentry/node-core': 10.60.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) - '@sentry/opentelemetry': 10.60.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) - '@sentry/server-utils': 10.60.0 + '@sentry/core': 10.62.0 + '@sentry/node-core': 10.62.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.62.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.62.0 import-in-the-middle: 3.2.0 transitivePeerDependencies: - '@opentelemetry/core' - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/opentelemetry@10.60.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))': + '@sentry/opentelemetry@10.62.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.12.0 - '@sentry/core': 10.60.0 + '@sentry/core': 10.62.0 - '@sentry/server-utils@10.60.0': + '@sentry/server-utils@10.62.0': dependencies: '@apm-js-collab/code-transformer': 0.15.0 '@apm-js-collab/code-transformer-bundler-plugins': 0.5.0 '@apm-js-collab/tracing-hooks': 0.10.0 '@sentry/conventions': 0.12.0 - '@sentry/core': 10.60.0 + '@sentry/core': 10.62.0 magic-string: 0.30.21 transitivePeerDependencies: - supports-color @@ -12720,7 +12696,7 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tailwindcss/node@4.3.1': + '@tailwindcss/node@4.3.2': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.21.6 @@ -12728,72 +12704,72 @@ snapshots: lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.1 + tailwindcss: 4.3.2 - '@tailwindcss/oxide-android-arm64@4.3.1': + '@tailwindcss/oxide-android-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.1': + '@tailwindcss/oxide-darwin-arm64@4.3.2': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.1': + '@tailwindcss/oxide-darwin-x64@4.3.2': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.1': + '@tailwindcss/oxide-freebsd-x64@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.1': + '@tailwindcss/oxide-linux-x64-musl@4.3.2': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.1': + '@tailwindcss/oxide-wasm32-wasi@4.3.2': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': optional: true - '@tailwindcss/oxide@4.3.1': + '@tailwindcss/oxide@4.3.2': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.1 - '@tailwindcss/oxide-darwin-arm64': 4.3.1 - '@tailwindcss/oxide-darwin-x64': 4.3.1 - '@tailwindcss/oxide-freebsd-x64': 4.3.1 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 - '@tailwindcss/oxide-linux-x64-musl': 4.3.1 - '@tailwindcss/oxide-wasm32-wasi': 4.3.1 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 - - '@tailwindcss/postcss@4.3.1': + '@tailwindcss/oxide-android-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-x64': 4.3.2 + '@tailwindcss/oxide-freebsd-x64': 4.3.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-x64-musl': 4.3.2 + '@tailwindcss/oxide-wasm32-wasi': 4.3.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + + '@tailwindcss/postcss@4.3.2': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.3.1 - '@tailwindcss/oxide': 4.3.1 - postcss: 8.5.15 - tailwindcss: 4.3.1 + '@tailwindcss/node': 4.3.2 + '@tailwindcss/oxide': 4.3.2 + postcss: 8.5.16 + tailwindcss: 4.3.2 - '@tanstack/query-core@5.101.1': {} + '@tanstack/query-core@5.101.2': {} - '@tanstack/react-query@5.101.1(react@19.2.7)': + '@tanstack/react-query@5.101.2(react@19.2.7)': dependencies: - '@tanstack/query-core': 5.101.1 + '@tanstack/query-core': 5.101.2 react: 19.2.7 '@testing-library/dom@10.4.1': @@ -13135,15 +13111,15 @@ snapshots: dependencies: '@types/node': 26.0.1 - '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.62.0 - '@typescript-eslint/type-utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.62.0 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -13151,14 +13127,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.62.0 debug: 4.4.3 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -13181,13 +13157,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.62.0 '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -13210,13 +13186,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3)': + '@typescript-eslint/utils@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.62.0 '@typescript-eslint/types': 8.62.0 '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -13409,8 +13385,6 @@ snapshots: acorn@8.17.0: {} - agent-base@7.1.4: {} - agent-install@0.0.5: dependencies: '@iarna/toml': 2.2.5 @@ -13447,7 +13421,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -13573,7 +13547,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.10.38: {} + baseline-browser-mapping@2.10.40: {} bidi-js@1.0.3: dependencies: @@ -13621,7 +13595,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -13631,9 +13605,9 @@ snapshots: browserslist@4.28.4: dependencies: - baseline-browser-mapping: 2.10.38 + baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 - electron-to-chromium: 1.5.378 + electron-to-chromium: 1.5.381 node-releases: 2.0.50 update-browserslist-db: 1.2.3(browserslist@4.28.4) @@ -13768,6 +13742,8 @@ snapshots: - '@types/react' - '@types/react-dom' + cnfast@0.0.8: {} + code-block-writer@13.0.3: {} collapse-white-space@2.1.0: {} @@ -13867,7 +13843,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.2.0 + js-yaml: 4.3.0 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -13940,8 +13916,6 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-uri-to-buffer@4.0.1: {} - data-urls@7.0.0: dependencies: whatwg-mimetype: 5.0.0 @@ -14105,7 +14079,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.378: {} + electron-to-chromium@1.5.381: {} embla-carousel-react@8.6.0(react@19.2.7): dependencies: @@ -14193,7 +14167,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.1 + es-to-primitive: 1.3.4 function.prototype.name: 1.2.0 get-intrinsic: 1.3.0 get-proto: 1.0.1 @@ -14240,7 +14214,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.1.0: {} + es-module-lexer@2.2.0: {} es-object-atoms@1.1.2: dependencies: @@ -14257,15 +14231,16 @@ snapshots: dependencies: hasown: 2.0.4 - es-to-primitive@1.3.1: + es-to-primitive@1.3.4: dependencies: es-abstract-get: 1.0.0 + es-define-property: 1.0.1 es-errors: 1.3.0 is-callable: 1.2.7 is-date-object: 1.1.0 is-symbol: 1.1.1 - es-toolkit@1.48.1: {} + es-toolkit@1.49.0: {} esast-util-from-estree@2.0.0: dependencies: @@ -14430,11 +14405,11 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) - eslint-plugin-jsx-a11y@6.10.2(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.6.0(jiti@2.7.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -14444,7 +14419,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -14453,122 +14428,122 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-perfectionist@5.9.1(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-perfectionist@5.9.1(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.7.0)))(eslint@10.6.0(jiti@2.7.0))(prettier@3.9.3): dependencies: - eslint: 10.5.0(jiti@2.7.0) - prettier: 3.8.4 + eslint: 10.6.0(jiti@2.7.0) + prettier: 3.9.3 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.5.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@2.7.0)) - eslint-plugin-react-dom@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-react-dom@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/jsx': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) compare-versions: 6.1.1 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.1.1(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-react-hooks@7.1.1(eslint@10.6.0(jiti@2.7.0)): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.7 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react-jsx@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-react-jsx@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/core': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/jsx': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-naming-convention@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-react-naming-convention@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/core': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/var': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-rsc@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-react-rsc@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/core': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/var': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-web-api@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-react-web-api@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/core': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/var': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) birecord: 0.1.1 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) ts-pattern: 5.9.0 typescript: 6.0.3 transitivePeerDependencies: - supports-color - eslint-plugin-react-x@5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + eslint-plugin-react-x@5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@eslint-react/ast': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/core': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/eslint': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/jsx': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/shared': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@eslint-react/var': 5.9.2(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/ast': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/core': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/eslint': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/jsx': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/shared': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@eslint-react/var': 5.10.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.62.0 - '@typescript-eslint/type-utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/types': 8.62.0 '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) compare-versions: 6.1.1 - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) string-ts: 2.3.1 ts-api-utils: 2.5.0(typescript@6.0.3) ts-pattern: 5.9.0 @@ -14576,9 +14551,9 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-you-might-not-need-an-effect@1.0.1(eslint@10.5.0(jiti@2.7.0)): + eslint-plugin-react-you-might-not-need-an-effect@1.0.1(eslint@10.6.0(jiti@2.7.0)): dependencies: - eslint: 10.5.0(jiti@2.7.0) + eslint: 10.6.0(jiti@2.7.0) globals: 16.5.0 eslint-scope@9.1.2: @@ -14592,9 +14567,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.5.0(jiti@2.7.0): + eslint@10.6.0(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.6.0 @@ -14743,7 +14718,7 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - expect-type@1.3.0: {} + expect-type@1.4.0: {} express-rate-limit@8.5.2(express@5.2.1): dependencies: @@ -14773,7 +14748,7 @@ snapshots: parseurl: 1.3.3 proxy-addr: 2.0.7 qs: 6.15.3 - range-parser: 1.2.1 + range-parser: 1.3.0 router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 @@ -14824,7 +14799,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.2: {} + fast-uri@3.1.3: {} fast-wrap-ansi@0.2.2: dependencies: @@ -14840,11 +14815,6 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -14916,15 +14886,11 @@ snapshots: form-data-encoder@4.1.0: {} - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded@0.2.0: {} - framer-motion@12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + framer-motion@12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - motion-dom: 12.41.0 + motion-dom: 12.42.0 motion-utils: 12.39.0 tslib: 2.8.1 optionalDependencies: @@ -14945,14 +14911,14 @@ snapshots: fsevents@2.3.3: optional: true - fumadocs-core@16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): + fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3): dependencies: '@orama/orama': 3.1.18 estree-util-value-to-estree: 3.5.0 github-slugger: 2.0.0 hast-util-to-estree: 3.1.3 hast-util-to-jsx-runtime: 2.3.6 - js-yaml: 4.2.0 + js-yaml: 5.2.0 mdast-util-mdx: 3.0.0 mdast-util-to-markdown: 2.1.2 remark: 15.0.1 @@ -14970,23 +14936,23 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 '@types/react': 19.2.17 - lucide-react: 1.21.0(react@19.2.7) - next: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + lucide-react: 1.22.0(react@19.2.7) + next: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) zod: 4.4.3 transitivePeerDependencies: - supports-color - fumadocs-mdx@15.0.12(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + fumadocs-mdx@15.0.13(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.1 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) - js-yaml: 4.2.0 + fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + js-yaml: 5.2.0 mdast-util-mdx: 3.0.0 picocolors: 1.1.1 picomatch: 4.0.4 @@ -15001,17 +14967,17 @@ snapshots: '@types/mdast': 4.0.4 '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 rolldown: 1.1.3 vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) transitivePeerDependencies: - supports-color - fumadocs-ui@16.10.5(@tailwindcss/oxide@4.3.1)(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1): + fumadocs-ui@16.10.7(@tailwindcss/oxide@4.3.2)(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.2): dependencies: '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@fumadocs/tailwind': 0.0.5(@tailwindcss/oxide@4.3.1)(tailwindcss@4.3.1) + '@fumadocs/tailwind': 0.0.5(@tailwindcss/oxide@4.3.2)(tailwindcss@4.3.2) '@radix-ui/react-accordion': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-collapsible': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -15023,9 +14989,10 @@ snapshots: '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) '@radix-ui/react-tabs': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) class-variance-authority: 0.7.1 - fumadocs-core: 16.10.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) - lucide-react: 1.21.0(react@19.2.7) - motion: 12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + cnfast: 0.0.8 + fumadocs-core: 16.10.7(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.22.0(react@19.2.7))(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.4.3) + lucide-react: 1.22.0(react@19.2.7) + motion: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-themes: 0.4.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -15033,12 +15000,11 @@ snapshots: rehype-raw: 7.0.0 scroll-into-view-if-needed: 3.1.0 shiki: 4.3.0 - tailwind-merge: 3.6.0 unist-util-visit: 5.1.0 optionalDependencies: '@types/mdx': 2.0.14 '@types/react': 19.2.17 - next: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) transitivePeerDependencies: - '@emotion/is-prop-valid' - '@tailwindcss/oxide' @@ -15349,13 +15315,6 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -15370,7 +15329,7 @@ snapshots: icu-minify@4.13.0: dependencies: - '@formatjs/icu-messageformat-parser': 3.5.11 + '@formatjs/icu-messageformat-parser': 3.5.12 ieee754@1.2.1: {} @@ -15421,10 +15380,10 @@ snapshots: internmap@2.0.3: {} - intl-messageformat@11.2.8: + intl-messageformat@11.2.9: dependencies: '@formatjs/fast-memoize': 3.1.6 - '@formatjs/icu-messageformat-parser': 3.5.11 + '@formatjs/icu-messageformat-parser': 3.5.12 ip-address@10.2.0: {} @@ -15636,7 +15595,11 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.2.0: + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + js-yaml@5.2.0: dependencies: argparse: 2.0.1 @@ -15645,7 +15608,7 @@ snapshots: '@asamuzakjp/css-color': 5.1.11 '@asamuzakjp/dom-selector': 7.1.1 '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) '@exodus/bytes': 1.15.1 css-tree: 3.2.1 data-urls: 7.0.0 @@ -15812,7 +15775,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@1.21.0(react@19.2.7): + lucide-react@1.22.0(react@19.2.7): dependencies: react: 19.2.7 @@ -16318,7 +16281,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minimatch@3.1.5: dependencies: @@ -16341,15 +16304,15 @@ snapshots: module-details-from-path@1.0.4: {} - motion-dom@12.41.0: + motion-dom@12.42.0: dependencies: motion-utils: 12.39.0 motion-utils@12.39.0: {} - motion@12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + motion@12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - framer-motion: 12.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + framer-motion: 12.42.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) tslib: 2.8.1 optionalDependencies: react: 19.2.7 @@ -16394,14 +16357,14 @@ snapshots: next-intl-swc-plugin-extractor@4.13.0: {} - next-intl@4.13.0(next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3): + next-intl@4.13.0(next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(typescript@6.0.3): dependencies: '@formatjs/intl-localematcher': 0.8.10 '@parcel/watcher': 2.5.6 '@swc/core': 1.15.43 icu-minify: 4.13.0 negotiator: 1.0.0 - next: 16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next-intl-swc-plugin-extractor: 4.13.0 po-parser: 2.1.1 react: 19.2.7 @@ -16420,7 +16383,7 @@ snapshots: dependencies: '@next/env': 16.1.7 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.38 + baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 postcss: 8.4.31 react: 19.2.7 @@ -16443,25 +16406,25 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@16.3.0-preview.4(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@16.3.0-preview.5(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - '@next/env': 16.3.0-preview.4 + '@next/env': 16.3.0-preview.5 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.38 + baseline-browser-mapping: 2.10.40 caniuse-lite: 1.0.30001799 postcss: 8.5.10 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.7) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.0-preview.4 - '@next/swc-darwin-x64': 16.3.0-preview.4 - '@next/swc-linux-arm64-gnu': 16.3.0-preview.4 - '@next/swc-linux-arm64-musl': 16.3.0-preview.4 - '@next/swc-linux-x64-gnu': 16.3.0-preview.4 - '@next/swc-linux-x64-musl': 16.3.0-preview.4 - '@next/swc-win32-arm64-msvc': 16.3.0-preview.4 - '@next/swc-win32-x64-msvc': 16.3.0-preview.4 + '@next/swc-darwin-arm64': 16.3.0-preview.5 + '@next/swc-darwin-x64': 16.3.0-preview.5 + '@next/swc-linux-arm64-gnu': 16.3.0-preview.5 + '@next/swc-linux-arm64-musl': 16.3.0-preview.5 + '@next/swc-linux-x64-gnu': 16.3.0-preview.5 + '@next/swc-linux-x64-musl': 16.3.0-preview.5 + '@next/swc-win32-arm64-msvc': 16.3.0-preview.5 + '@next/swc-win32-x64-msvc': 16.3.0-preview.5 '@opentelemetry/api': 1.9.1 '@playwright/test': 1.61.1 babel-plugin-react-compiler: 1.0.0 @@ -16474,14 +16437,6 @@ snapshots: node-cron@4.5.0: {} - node-domexception@1.0.0: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-releases@2.0.50: {} nodemailer@9.0.1: {} @@ -16867,12 +16822,12 @@ snapshots: postal-mime@2.7.4: {} - postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.16)(tsx@4.22.4)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.7.0 - postcss: 8.5.15 + postcss: 8.5.16 tsx: 4.22.4 yaml: 2.9.0 @@ -16893,7 +16848,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.15: + postcss@8.5.16: dependencies: nanoid: 3.3.15 picocolors: 1.1.1 @@ -16903,7 +16858,7 @@ snapshots: powershell-utils@0.1.0: {} - preact@10.29.2: {} + preact@10.29.3: {} prelude-ls@1.2.1: {} @@ -16911,11 +16866,11 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-tailwindcss@0.8.0(prettier@3.8.4): + prettier-plugin-tailwindcss@0.8.0(prettier@3.9.3): dependencies: - prettier: 3.8.4 + prettier: 3.9.3 - prettier@3.8.4: {} + prettier@3.9.3: {} pretty-format@27.5.1: dependencies: @@ -17091,7 +17046,7 @@ snapshots: '@types/react': 19.2.17 '@types/react-dom': 19.2.3(@types/react@19.2.17) - range-parser@1.2.1: {} + range-parser@1.3.0: {} rate-limiter-flexible@11.2.0: {} @@ -17110,15 +17065,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - react-doctor@0.5.8(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(eslint@10.5.0(jiti@2.7.0)): + react-doctor@0.5.8(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(eslint@10.6.0(jiti@2.7.0)): dependencies: '@babel/code-frame': 7.29.7 - '@sentry/node': 10.60.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1)) + '@sentry/node': 10.62.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1)) agent-install: 0.0.5 conf: 15.1.0 confbox: 0.2.4 deslop-js: 0.5.8 - eslint-plugin-react-hooks: 7.1.1(eslint@10.5.0(jiti@2.7.0)) + eslint-plugin-react-hooks: 7.1.1(eslint@10.6.0(jiti@2.7.0)) jiti: 2.7.0 magicast: 0.5.3 oxlint: 1.66.0 @@ -17140,7 +17095,7 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 - react-email@6.6.4(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-email@6.6.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@babel/parser': 7.27.0 '@babel/traverse': 7.27.0 @@ -17164,7 +17119,7 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) socket.io: 4.8.3 - tailwindcss: 4.3.1 + tailwindcss: 4.3.2 tsconfig-paths: 4.2.0 transitivePeerDependencies: - bufferutil @@ -17212,29 +17167,29 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - react-resizable-panels@4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + react-resizable-panels@4.12.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - react-scan@0.5.7(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(esbuild@0.27.7)(eslint@10.5.0(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + react-scan@0.5.7(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(esbuild@0.27.7)(eslint@10.6.0(jiti@2.7.0))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.7 '@babel/types': 7.29.7 - '@preact/signals': 2.9.2(preact@10.29.2) + '@preact/signals': 2.9.2(preact@10.29.3) '@rollup/pluginutils': 5.4.0(rollup@4.62.2) bippy: 0.5.42(react@19.2.7) commander: 14.0.3 picocolors: 1.1.1 - preact: 10.29.2 + preact: 10.29.3 prompts: 2.4.2 react: 19.2.7 - react-doctor: 0.5.8(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(eslint@10.5.0(jiti@2.7.0)) + react-doctor: 0.5.8(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(eslint@10.6.0(jiti@2.7.0)) react-dom: 19.2.7(react@19.2.7) react-grab: 0.1.47(react@19.2.7) optionalDependencies: esbuild: 0.27.7 - unplugin: 3.2.0(esbuild@0.27.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + unplugin: 3.3.0(esbuild@0.27.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) transitivePeerDependencies: - '@farmfe/core' - '@opentelemetry/core' @@ -17292,7 +17247,7 @@ snapshots: readdirp@5.0.0: {} - recast@0.23.11: + recast@0.23.12: dependencies: ast-types: 0.16.1 esprima: 4.0.1 @@ -17305,7 +17260,7 @@ snapshots: '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.17)(react@19.2.7)(redux@5.0.1))(react@19.2.7) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.48.1 + es-toolkit: 1.49.0 eventemitter3: 5.0.4 immer: 10.2.0 react: 19.2.7 @@ -17460,7 +17415,7 @@ snapshots: reselect@5.2.0: {} - resend@6.14.0(@react-email/render@2.0.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)): + resend@6.16.0(@react-email/render@2.0.9(react-dom@19.2.7(react@19.2.7))(react@19.2.7)): dependencies: postal-mime: 2.7.4 standardwebhooks: 1.0.0 @@ -17630,7 +17585,7 @@ snapshots: mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 - range-parser: 1.2.1 + range-parser: 1.3.0 statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -17672,7 +17627,7 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@4.11.0(typescript@6.0.3): + shadcn@4.12.0(typescript@6.0.3): dependencies: '@babel/core': 7.29.7 '@babel/parser': 7.29.7 @@ -17691,19 +17646,18 @@ snapshots: fast-glob: 3.3.3 fs-extra: 11.3.5 fuzzysort: 3.1.0 - https-proxy-agent: 7.0.6 kleur: 4.1.5 - node-fetch: 3.3.2 open: 11.0.0 ora: 8.2.0 - postcss: 8.5.15 + postcss: 8.5.16 postcss-selector-parser: 7.1.4 prompts: 2.4.2 - recast: 0.23.11 + recast: 0.23.12 stringify-object: 5.0.0 tailwind-merge: 3.6.0 ts-morph: 26.0.0 tsconfig-paths: 4.2.0 + undici: 7.28.0 validate-npm-package-name: 7.0.2 zod: 3.25.76 zod-to-json-schema: 3.25.2(zod@3.25.76) @@ -18050,7 +18004,7 @@ snapshots: tailwind-merge@3.6.0: {} - tailwindcss@4.3.1: {} + tailwindcss@4.3.2: {} tapable@2.3.3: {} @@ -18100,11 +18054,11 @@ snapshots: tinyrainbow@3.1.0: {} - tldts-core@7.4.4: {} + tldts-core@7.4.5: {} - tldts@7.4.4: + tldts@7.4.5: dependencies: - tldts-core: 7.4.4 + tldts-core: 7.4.5 to-regex-range@5.0.1: dependencies: @@ -18124,7 +18078,7 @@ snapshots: tough-cookie@6.0.1: dependencies: - tldts: 7.4.4 + tldts: 7.4.5 tr46@6.0.0: dependencies: @@ -18169,7 +18123,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(@swc/core@1.15.43)(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0): + tsup@8.5.1(@swc/core@1.15.43)(jiti@2.7.0)(postcss@8.5.16)(tsx@4.22.4)(typescript@6.0.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -18180,7 +18134,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(yaml@2.9.0) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.16)(tsx@4.22.4)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.62.2 source-map: 0.7.6 @@ -18190,7 +18144,7 @@ snapshots: tree-kill: 1.2.2 optionalDependencies: '@swc/core': 1.15.43 - postcss: 8.5.15 + postcss: 8.5.16 typescript: 6.0.3 transitivePeerDependencies: - jiti @@ -18264,13 +18218,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3): + typescript-eslint@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - '@typescript-eslint/parser': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/typescript-estree': 8.62.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.0(eslint@10.5.0(jiti@2.7.0))(typescript@6.0.3) - eslint: 10.5.0(jiti@2.7.0) + '@typescript-eslint/utils': 8.62.0(eslint@10.6.0(jiti@2.7.0))(typescript@6.0.3) + eslint: 10.6.0(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -18347,7 +18301,7 @@ snapshots: unpipe@1.0.0: {} - unplugin@3.2.0(esbuild@0.27.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + unplugin@3.3.0(esbuild@0.27.7)(rolldown@1.1.3)(rollup@4.62.2)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.27.7)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.4 @@ -18385,7 +18339,7 @@ snapshots: '@formatjs/fast-memoize': 3.1.6 '@schummar/icu-type-parser': 1.21.5 icu-minify: 4.13.0 - intl-messageformat: 11.2.8 + intl-messageformat: 11.2.9 react: 19.2.7 use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): @@ -18453,7 +18407,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.16 rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: @@ -18468,7 +18422,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.16 rolldown: 1.1.3 tinyglobby: 0.2.17 optionalDependencies: @@ -18489,8 +18443,8 @@ snapshots: '@vitest/snapshot': 4.1.9 '@vitest/spy': 4.1.9 '@vitest/utils': 4.1.9 - es-module-lexer: 2.1.0 - expect-type: 1.3.0 + es-module-lexer: 2.2.0 + expect-type: 1.4.0 magic-string: 0.30.21 obug: 2.1.3 pathe: 2.0.3 @@ -18535,8 +18489,6 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} - web-worker@1.5.0: {} webidl-conversions@8.0.1: {}