Add privilege management system with related migrations and guards

- Introduced a new `PrivilegesModule` to manage user privileges and access control.
- Added `RequirePrivilege` decorator to enforce privilege checks on controller handlers.
- Implemented `PrivilegesGuard` to handle authorization based on user privileges.
- Created database migrations for `privileges`, `privilege_keys`, and `privilege_details` tables.
- Updated user model to include `is_superadmin` field for enhanced access control.
- Added unit tests for the new privileges functionality and guards to ensure correct behavior.
This commit is contained in:
shancheas
2026-08-24 11:31:13 +07:00
parent 0550cbe764
commit 07550b3167
51 changed files with 3731 additions and 27 deletions
@@ -0,0 +1,40 @@
CREATE TABLE "privilege_details" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"privilege_id" uuid NOT NULL,
"privilege_key_id" uuid NOT NULL,
"action" text NOT NULL,
"value" boolean NOT NULL
);
--> statement-breakpoint
CREATE TABLE "privilege_keys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"code" text NOT NULL,
"label" text NOT NULL,
"sort_order" integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE "privileges" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"code" text NOT NULL,
"status" text DEFAULT 'draft' NOT NULL,
"created_at" bigint NOT NULL,
"updated_at" bigint NOT NULL,
"created_by" uuid NOT NULL,
"updated_by" uuid NOT NULL
);
--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "privilege_id" uuid;--> statement-breakpoint
ALTER TABLE "privilege_details" ADD CONSTRAINT "privilege_details_privilege_id_privileges_id_fk" FOREIGN KEY ("privilege_id") REFERENCES "public"."privileges"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "privilege_details" ADD CONSTRAINT "privilege_details_privilege_key_id_privilege_keys_id_fk" FOREIGN KEY ("privilege_key_id") REFERENCES "public"."privilege_keys"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "privileges" ADD CONSTRAINT "privileges_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "privileges" ADD CONSTRAINT "privileges_updated_by_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "privilege_details_privilege_key_action_unique" ON "privilege_details" USING btree ("privilege_id","privilege_key_id","action");--> statement-breakpoint
CREATE INDEX "privilege_details_privilege_id_idx" ON "privilege_details" USING btree ("privilege_id");--> statement-breakpoint
CREATE UNIQUE INDEX "privilege_keys_code_unique" ON "privilege_keys" USING btree ("code");--> statement-breakpoint
CREATE UNIQUE INDEX "privileges_code_unique" ON "privileges" USING btree ("code");--> statement-breakpoint
CREATE INDEX "users_privilege_id_idx" ON "users" USING btree ("privilege_id");--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_privilege_id_privileges_id_fk" FOREIGN KEY ("privilege_id") REFERENCES "public"."privileges"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
INSERT INTO "privilege_keys" ("code", "label", "sort_order") VALUES
('PRIVILEGES', 'Privileges', 1),
('USERS', 'Users', 2);
@@ -0,0 +1 @@
ALTER TABLE "users" ADD COLUMN "is_superadmin" boolean DEFAULT false NOT NULL;
+506
View File
@@ -0,0 +1,506 @@
{
"id": "687eb28d-2560-40f6-8390-06555bd39836",
"prevId": "55ce9d09-18dc-4cf8-8fa0-6b96b229820f",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.privilege_details": {
"name": "privilege_details",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"privilege_id": {
"name": "privilege_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"privilege_key_id": {
"name": "privilege_key_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"privilege_details_privilege_key_action_unique": {
"name": "privilege_details_privilege_key_action_unique",
"columns": [
{
"expression": "privilege_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "privilege_key_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "action",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"privilege_details_privilege_id_idx": {
"name": "privilege_details_privilege_id_idx",
"columns": [
{
"expression": "privilege_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"privilege_details_privilege_id_privileges_id_fk": {
"name": "privilege_details_privilege_id_privileges_id_fk",
"tableFrom": "privilege_details",
"tableTo": "privileges",
"columnsFrom": [
"privilege_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"privilege_details_privilege_key_id_privilege_keys_id_fk": {
"name": "privilege_details_privilege_key_id_privilege_keys_id_fk",
"tableFrom": "privilege_details",
"tableTo": "privilege_keys",
"columnsFrom": [
"privilege_key_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.privilege_keys": {
"name": "privilege_keys",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"label": {
"name": "label",
"type": "text",
"primaryKey": false,
"notNull": true
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"privilege_keys_code_unique": {
"name": "privilege_keys_code_unique",
"columns": [
{
"expression": "code",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.privileges": {
"name": "privileges",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'draft'"
},
"created_at": {
"name": "created_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"created_by": {
"name": "created_by",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"updated_by": {
"name": "updated_by",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"privileges_code_unique": {
"name": "privileges_code_unique",
"columns": [
{
"expression": "code",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"privileges_created_by_users_id_fk": {
"name": "privileges_created_by_users_id_fk",
"tableFrom": "privileges",
"tableTo": "users",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"privileges_updated_by_users_id_fk": {
"name": "privileges_updated_by_users_id_fk",
"tableFrom": "privileges",
"tableTo": "users",
"columnsFrom": [
"updated_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.refresh_tokens": {
"name": "refresh_tokens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_jti": {
"name": "access_jti",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "bigint",
"primaryKey": false,
"notNull": false
},
"replaced_by": {
"name": "replaced_by",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"refresh_tokens_token_hash_unique": {
"name": "refresh_tokens_token_hash_unique",
"columns": [
{
"expression": "token_hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"refresh_tokens_user_id_idx": {
"name": "refresh_tokens_user_id_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"refresh_tokens_user_id_users_id_fk": {
"name": "refresh_tokens_user_id_users_id_fk",
"tableFrom": "refresh_tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.revoked_access_tokens": {
"name": "revoked_access_tokens",
"schema": "",
"columns": {
"jti": {
"name": "jti",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"privilege_id": {
"name": "privilege_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
{
"expression": "username",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_privilege_id_idx": {
"name": "users_privilege_id_idx",
"columns": [
{
"expression": "privilege_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+513
View File
@@ -0,0 +1,513 @@
{
"id": "892b17dc-a719-40eb-a6e1-ac43663d4f3c",
"prevId": "687eb28d-2560-40f6-8390-06555bd39836",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.privilege_details": {
"name": "privilege_details",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"privilege_id": {
"name": "privilege_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"privilege_key_id": {
"name": "privilege_key_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"action": {
"name": "action",
"type": "text",
"primaryKey": false,
"notNull": true
},
"value": {
"name": "value",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"privilege_details_privilege_key_action_unique": {
"name": "privilege_details_privilege_key_action_unique",
"columns": [
{
"expression": "privilege_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "privilege_key_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "action",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"privilege_details_privilege_id_idx": {
"name": "privilege_details_privilege_id_idx",
"columns": [
{
"expression": "privilege_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"privilege_details_privilege_id_privileges_id_fk": {
"name": "privilege_details_privilege_id_privileges_id_fk",
"tableFrom": "privilege_details",
"tableTo": "privileges",
"columnsFrom": [
"privilege_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"privilege_details_privilege_key_id_privilege_keys_id_fk": {
"name": "privilege_details_privilege_key_id_privilege_keys_id_fk",
"tableFrom": "privilege_details",
"tableTo": "privilege_keys",
"columnsFrom": [
"privilege_key_id"
],
"columnsTo": [
"id"
],
"onDelete": "restrict",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.privilege_keys": {
"name": "privilege_keys",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"label": {
"name": "label",
"type": "text",
"primaryKey": false,
"notNull": true
},
"sort_order": {
"name": "sort_order",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"privilege_keys_code_unique": {
"name": "privilege_keys_code_unique",
"columns": [
{
"expression": "code",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.privileges": {
"name": "privileges",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"code": {
"name": "code",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'draft'"
},
"created_at": {
"name": "created_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"created_by": {
"name": "created_by",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"updated_by": {
"name": "updated_by",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"privileges_code_unique": {
"name": "privileges_code_unique",
"columns": [
{
"expression": "code",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"privileges_created_by_users_id_fk": {
"name": "privileges_created_by_users_id_fk",
"tableFrom": "privileges",
"tableTo": "users",
"columnsFrom": [
"created_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"privileges_updated_by_users_id_fk": {
"name": "privileges_updated_by_users_id_fk",
"tableFrom": "privileges",
"tableTo": "users",
"columnsFrom": [
"updated_by"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.refresh_tokens": {
"name": "refresh_tokens",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"token_hash": {
"name": "token_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"access_jti": {
"name": "access_jti",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"revoked_at": {
"name": "revoked_at",
"type": "bigint",
"primaryKey": false,
"notNull": false
},
"replaced_by": {
"name": "replaced_by",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"refresh_tokens_token_hash_unique": {
"name": "refresh_tokens_token_hash_unique",
"columns": [
{
"expression": "token_hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"refresh_tokens_user_id_idx": {
"name": "refresh_tokens_user_id_idx",
"columns": [
{
"expression": "user_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"refresh_tokens_user_id_users_id_fk": {
"name": "refresh_tokens_user_id_users_id_fk",
"tableFrom": "refresh_tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.revoked_access_tokens": {
"name": "revoked_access_tokens",
"schema": "",
"columns": {
"jti": {
"name": "jti",
"type": "text",
"primaryKey": true,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"privilege_id": {
"name": "privilege_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"is_superadmin": {
"name": "is_superadmin",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "bigint",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"users_username_unique": {
"name": "users_username_unique",
"columns": [
{
"expression": "username",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_privilege_id_idx": {
"name": "users_privilege_id_idx",
"columns": [
{
"expression": "privilege_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+14
View File
@@ -15,6 +15,20 @@
"when": 1787285129382,
"tag": "0001_amusing_maximus",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1787305078482,
"tag": "0002_wooden_king_cobra",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1787544143116,
"tag": "0003_pretty_darkhawk",
"breakpoints": true
}
]
}