diff --git a/client/src/index.css b/client/src/index.css
index 0cf721b..2439cdb 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -2,6 +2,16 @@
@tailwind components;
@tailwind utilities;
+@layer components {
+ .drawer-item {
+ @apply text-gray-700 hover:bg-gray-100 hover:text-primary focus:outline-none transition-colors duration-200;
+ }
+
+ .drawer-item.active {
+ @apply bg-primary/10 text-primary font-medium;
+ }
+}
+
@layer base {
:root {
--background: 0 0% 96%;
diff --git a/client/src/layouts/dashboard-layout.tsx b/client/src/layouts/dashboard-layout.tsx
index 53c9e95..bd72bad 100644
--- a/client/src/layouts/dashboard-layout.tsx
+++ b/client/src/layouts/dashboard-layout.tsx
@@ -160,22 +160,21 @@ export function DashboardLayout({ children, title, description }: DashboardLayou
} ${isMobile ? "absolute" : "relative"}`}
>
-
-
-
- Dashboard
-
+
+
+
Dashboard
{menuSections.map((section, idx) => (
@@ -185,15 +184,15 @@ export function DashboardLayout({ children, title, description }: DashboardLayou
{section.items.map((item, itemIdx) => (
-
-
- {item.icon}
- {item.title}
-
+
+ {item.icon}
+
{item.title}
))}
diff --git a/client/src/pages/auth-page.tsx b/client/src/pages/auth-page.tsx
index 38daeba..1b8eb6b 100644
--- a/client/src/pages/auth-page.tsx
+++ b/client/src/pages/auth-page.tsx
@@ -42,6 +42,8 @@ const registerSchema = insertUserSchema.extend({
acceptTerms: z.boolean().refine(val => val === true, {
message: "You must accept the terms and conditions",
}),
+ // Define roleId explicitly to match form values
+ roleId: z.number().optional().nullable(),
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
@@ -140,7 +142,7 @@ export default function AuthPage() {
email: "",
fullName: "",
acceptTerms: false,
- role: "user",
+ roleId: 2, // Assign the default "user" role ID
},
});
diff --git a/migrations/0000_fancy_blackheart.sql b/migrations/0000_fancy_blackheart.sql
new file mode 100644
index 0000000..bfa2e31
--- /dev/null
+++ b/migrations/0000_fancy_blackheart.sql
@@ -0,0 +1,116 @@
+CREATE TABLE "ad_computers" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "connection_id" integer NOT NULL,
+ "distinguished_name" text NOT NULL,
+ "name" text NOT NULL,
+ "dns_host_name" text,
+ "operating_system" text,
+ "operating_system_version" text,
+ "last_logon" timestamp,
+ "enabled" boolean DEFAULT true,
+ "ad_properties" jsonb
+);
+--> statement-breakpoint
+CREATE TABLE "ad_domains" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "connection_id" integer NOT NULL,
+ "distinguished_name" text NOT NULL,
+ "name" text NOT NULL,
+ "net_bios_name" text,
+ "forest_name" text,
+ "domain_functionality" text,
+ "ad_properties" jsonb
+);
+--> statement-breakpoint
+CREATE TABLE "ad_groups" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "connection_id" integer NOT NULL,
+ "distinguished_name" text NOT NULL,
+ "sam_account_name" text NOT NULL,
+ "group_type" text,
+ "description" text,
+ "members" jsonb,
+ "ad_properties" jsonb
+);
+--> statement-breakpoint
+CREATE TABLE "ad_org_units" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "connection_id" integer NOT NULL,
+ "distinguished_name" text NOT NULL,
+ "name" text NOT NULL,
+ "description" text,
+ "ad_properties" jsonb
+);
+--> statement-breakpoint
+CREATE TABLE "ad_users" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "connection_id" integer NOT NULL,
+ "distinguished_name" text NOT NULL,
+ "sam_account_name" text NOT NULL,
+ "user_principal_name" text,
+ "given_name" text,
+ "surname" text,
+ "display_name" text,
+ "email" text,
+ "enabled" boolean DEFAULT true,
+ "last_logon" timestamp,
+ "member_of" jsonb,
+ "ad_properties" jsonb
+);
+--> statement-breakpoint
+CREATE TABLE "api_tokens" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "token" text NOT NULL,
+ "user_id" integer NOT NULL,
+ "role_id" integer,
+ "custom_permissions" jsonb,
+ "expires_at" timestamp,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "api_tokens_token_unique" UNIQUE("token")
+);
+--> statement-breakpoint
+CREATE TABLE "ldap_connections" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "server" text NOT NULL,
+ "domain" text NOT NULL,
+ "port" integer DEFAULT 389,
+ "use_ssl" boolean DEFAULT true,
+ "username" text NOT NULL,
+ "password" text NOT NULL,
+ "status" text DEFAULT 'disconnected',
+ "last_connected" timestamp,
+ "created_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "role_permissions" (
+ "role_id" integer NOT NULL,
+ "permission" text NOT NULL,
+ CONSTRAINT "role_permissions_role_id_permission_pk" PRIMARY KEY("role_id","permission")
+);
+--> statement-breakpoint
+CREATE TABLE "roles" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "description" text,
+ "is_default" boolean DEFAULT false,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "roles_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+CREATE TABLE "users" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "username" text NOT NULL,
+ "password" text NOT NULL,
+ "email" text,
+ "full_name" text,
+ "role_id" integer,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "users_username_unique" UNIQUE("username")
+);
+--> statement-breakpoint
+ALTER TABLE "api_tokens" ADD CONSTRAINT "api_tokens_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "api_tokens" ADD CONSTRAINT "api_tokens_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "users" ADD CONSTRAINT "users_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;
\ No newline at end of file
diff --git a/migrations/meta/0000_snapshot.json b/migrations/meta/0000_snapshot.json
new file mode 100644
index 0000000..6938d9d
--- /dev/null
+++ b/migrations/meta/0000_snapshot.json
@@ -0,0 +1,714 @@
+{
+ "id": "fbd1e34a-7d4a-41c2-9c99-cfda190d6502",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.ad_computers": {
+ "name": "ad_computers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "distinguished_name": {
+ "name": "distinguished_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dns_host_name": {
+ "name": "dns_host_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "operating_system": {
+ "name": "operating_system",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "operating_system_version": {
+ "name": "operating_system_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_logon": {
+ "name": "last_logon",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "ad_properties": {
+ "name": "ad_properties",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ad_domains": {
+ "name": "ad_domains",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "distinguished_name": {
+ "name": "distinguished_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "net_bios_name": {
+ "name": "net_bios_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "forest_name": {
+ "name": "forest_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain_functionality": {
+ "name": "domain_functionality",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ad_properties": {
+ "name": "ad_properties",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ad_groups": {
+ "name": "ad_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "distinguished_name": {
+ "name": "distinguished_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sam_account_name": {
+ "name": "sam_account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_type": {
+ "name": "group_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "members": {
+ "name": "members",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ad_properties": {
+ "name": "ad_properties",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ad_org_units": {
+ "name": "ad_org_units",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "distinguished_name": {
+ "name": "distinguished_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ad_properties": {
+ "name": "ad_properties",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ad_users": {
+ "name": "ad_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "distinguished_name": {
+ "name": "distinguished_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sam_account_name": {
+ "name": "sam_account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_principal_name": {
+ "name": "user_principal_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "given_name": {
+ "name": "given_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "surname": {
+ "name": "surname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "last_logon": {
+ "name": "last_logon",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_of": {
+ "name": "member_of",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ad_properties": {
+ "name": "ad_properties",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_tokens": {
+ "name": "api_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_permissions": {
+ "name": "custom_permissions",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "api_tokens_user_id_users_id_fk": {
+ "name": "api_tokens_user_id_users_id_fk",
+ "tableFrom": "api_tokens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "api_tokens_role_id_roles_id_fk": {
+ "name": "api_tokens_role_id_roles_id_fk",
+ "tableFrom": "api_tokens",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_tokens_token_unique": {
+ "name": "api_tokens_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ldap_connections": {
+ "name": "ldap_connections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "server": {
+ "name": "server",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "port": {
+ "name": "port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 389
+ },
+ "use_ssl": {
+ "name": "use_ssl",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'disconnected'"
+ },
+ "last_connected": {
+ "name": "last_connected",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.role_permissions": {
+ "name": "role_permissions",
+ "schema": "",
+ "columns": {
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "role_permissions_role_id_roles_id_fk": {
+ "name": "role_permissions_role_id_roles_id_fk",
+ "tableFrom": "role_permissions",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "role_permissions_role_id_permission_pk": {
+ "name": "role_permissions_role_id_permission_pk",
+ "columns": [
+ "role_id",
+ "permission"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.roles": {
+ "name": "roles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "roles_name_unique": {
+ "name": "roles_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "full_name": {
+ "name": "full_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role_id": {
+ "name": "role_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "users_role_id_roles_id_fk": {
+ "name": "users_role_id_roles_id_fk",
+ "tableFrom": "users",
+ "tableTo": "roles",
+ "columnsFrom": [
+ "role_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json
new file mode 100644
index 0000000..ce595e1
--- /dev/null
+++ b/migrations/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+ "version": "7",
+ "dialect": "postgresql",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "7",
+ "when": 1744078354259,
+ "tag": "0000_fancy_blackheart",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index adb3b9c..e080ccd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41,6 +41,10 @@
"@radix-ui/react-tooltip": "^1.1.3",
"@replit/vite-plugin-shadcn-theme-json": "^0.0.4",
"@tanstack/react-query": "^5.60.5",
+ "@types/jsonwebtoken": "^9.0.9",
+ "@types/passport-jwt": "^4.0.1",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -59,6 +63,7 @@
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
+ "pg": "^8.14.1",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
@@ -3285,7 +3290,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@@ -3296,7 +3300,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@@ -3388,7 +3391,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@@ -3401,7 +3403,6 @@
"version": "4.19.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -3424,7 +3425,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -3433,11 +3433,26 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
+ "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
- "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": {
@@ -3453,12 +3468,21 @@
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
+ "node_modules/@types/passport-jwt": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
+ "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/jsonwebtoken": "*",
+ "@types/passport-strategy": "*"
+ }
+ },
"node_modules/@types/passport-local": {
"version": "1.0.38",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz",
@@ -3475,7 +3499,6 @@
"version": "0.2.38",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
@@ -3504,14 +3527,12 @@
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
"integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@@ -3539,7 +3560,6 @@
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@@ -3550,7 +3570,6 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@@ -3558,6 +3577,22 @@
"@types/send": "*"
}
},
+ "node_modules/@types/swagger-jsdoc": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
+ "integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/swagger-ui-express": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
+ "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*",
+ "@types/serve-static": "*"
+ }
+ },
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@@ -6436,14 +6471,14 @@
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/pg": {
- "version": "8.13.1",
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz",
- "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==",
+ "version": "8.14.1",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz",
+ "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.7.0",
- "pg-pool": "^3.7.0",
- "pg-protocol": "^1.7.0",
+ "pg-pool": "^3.8.0",
+ "pg-protocol": "^1.8.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
@@ -6494,18 +6529,18 @@
}
},
"node_modules/pg-pool": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
- "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz",
+ "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
- "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz",
+ "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==",
"license": "MIT"
},
"node_modules/pg-types": {
diff --git a/package.json b/package.json
index bcc5ad2..94c0fae 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,10 @@
"@radix-ui/react-tooltip": "^1.1.3",
"@replit/vite-plugin-shadcn-theme-json": "^0.0.4",
"@tanstack/react-query": "^5.60.5",
+ "@types/jsonwebtoken": "^9.0.9",
+ "@types/passport-jwt": "^4.0.1",
+ "@types/swagger-jsdoc": "^6.0.4",
+ "@types/swagger-ui-express": "^4.1.8",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@@ -61,6 +65,7 @@
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
+ "pg": "^8.14.1",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
diff --git a/server/auth.ts b/server/auth.ts
index 5e93955..b405616 100644
--- a/server/auth.ts
+++ b/server/auth.ts
@@ -110,12 +110,19 @@ export function setupAuth(app: Express) {
}
const hashedPassword = await hashPassword(password);
+ // Get the default role if role ID isn't specified
+ let roleId = req.body.roleId;
+ if (!roleId) {
+ const defaultRole = await storage.getDefaultRole();
+ roleId = defaultRole?.id;
+ }
+
const user = await storage.createUser({
username,
password: hashedPassword,
email: req.body.email,
fullName: req.body.fullName,
- role: req.body.role || "user",
+ roleId: roleId,
});
// Remove password from response
@@ -132,7 +139,7 @@ export function setupAuth(app: Express) {
// Login endpoint
app.post("/api/login", (req, res, next) => {
- passport.authenticate("local", (err, user, info) => {
+ passport.authenticate("local", (err: any, user: any, info: any) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({ message: info?.message || "Authentication failed" });
@@ -175,25 +182,30 @@ export function setupAuth(app: Express) {
}
try {
- const { name, expiresAt, permissions } = req.body;
+ const { name, expiresAt, roleId, customPermissions } = req.body;
if (!name) {
return res.status(400).json({ message: "Token name is required" });
}
+ // Create JWT token with user ID and optional permissions
const token = jwt.sign(
{
sub: req.user.id,
- permissions
+ customPermissions
},
JWT_SECRET,
- { expiresAt: expiresAt ? new Date(expiresAt) : undefined }
+ {
+ expiresIn: expiresAt ? Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1000) : '365d'
+ }
);
+ // Store the token in the database
const apiToken = storage.createApiToken({
name,
token,
userId: req.user.id,
- permissions: permissions || {},
+ roleId: roleId || null,
+ customPermissions: customPermissions || null,
expiresAt: expiresAt ? new Date(expiresAt) : null,
});
diff --git a/server/authorization.ts b/server/authorization.ts
new file mode 100644
index 0000000..07eb575
--- /dev/null
+++ b/server/authorization.ts
@@ -0,0 +1,194 @@
+import { Request, Response, NextFunction } from "express";
+import { db } from "./db";
+import { roles, rolePermissions, users, apiTokens } from "@shared/schema";
+import { eq, or, and, inArray } from "drizzle-orm";
+
+// Types for role-based access control
+export type RequireAuthOptions = {
+ allowApiToken?: boolean;
+};
+
+export type RequirePermissionOptions = RequireAuthOptions & {
+ anyOf?: string[];
+ allOf?: string[];
+};
+
+// Default middleware that verifies a user is authenticated
+export function requireAuth(options: RequireAuthOptions = {}) {
+ return async (req: Request, res: Response, next: NextFunction) => {
+ // Check for session authentication
+ if (req.isAuthenticated()) {
+ return next();
+ }
+
+ // Check for API token authentication if allowed
+ if (options.allowApiToken) {
+ const authHeader = req.headers.authorization;
+ if (authHeader && authHeader.startsWith("Bearer ")) {
+ const token = authHeader.substring(7);
+
+ try {
+ // Lookup the token
+ const [apiToken] = await db.select()
+ .from(apiTokens)
+ .where(eq(apiTokens.token, token))
+ .limit(1);
+
+ if (!apiToken) {
+ return res.status(401).json({ message: "Invalid API token" });
+ }
+
+ // Check if token has expired
+ if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
+ return res.status(401).json({ message: "API token has expired" });
+ }
+
+ // Set token info on the request
+ req.user = {
+ id: apiToken.userId,
+ username: '',
+ password: '',
+ fullName: null,
+ email: null,
+ createdAt: null,
+ // Add tokenId for identifying which token was used
+ tokenId: apiToken.id,
+ // Add roleId for permission checking
+ roleId: apiToken.roleId,
+ // Store custom permissions if available
+ customPermissions: (Array.isArray(apiToken.customPermissions) ?
+ apiToken.customPermissions :
+ (apiToken.customPermissions ? JSON.parse(String(apiToken.customPermissions)) : [])) as string[]
+ };
+
+ return next();
+ } catch (error) {
+ console.error("API token authentication error:", error);
+ return res.status(500).json({ message: "Internal server error" });
+ }
+ }
+ }
+
+ // Not authenticated through any method
+ return res.status(401).json({ message: "Not authenticated" });
+ };
+}
+
+// Middleware for checking specific permissions
+export function requirePermission(permission: string, options: RequirePermissionOptions = {}) {
+ return async (req: Request, res: Response, next: NextFunction) => {
+ // First ensure the user is authenticated
+ const authMiddleware = requireAuth(options);
+
+ authMiddleware(req, res, async () => {
+ try {
+ if (!req.user) {
+ return res.status(401).json({ message: "Not authenticated" });
+ }
+
+ // Get the permissions for the user's role
+ let hasPermission = false;
+
+ // Check for API token custom permissions first if present
+ if (req.user?.tokenId && Array.isArray(req.user?.customPermissions)) {
+ // For API tokens with custom permissions
+ const customPermissions = req.user.customPermissions;
+ if (customPermissions.includes(permission) ||
+ options.anyOf?.some(p => customPermissions.includes(p))) {
+ hasPermission = true;
+ }
+ }
+
+ // If no permission yet, check role-based permissions
+ if (!hasPermission && req.user.roleId) {
+ // Get permissions assigned to the role
+ const rolePerms = await db.select()
+ .from(rolePermissions)
+ .where(eq(rolePermissions.roleId, req.user.roleId));
+
+ const userPermissions = rolePerms.map(rp => rp.permission);
+
+ // Check for the specific permission or any of the optional permissions
+ if (userPermissions.includes(permission) ||
+ options.anyOf?.some(p => userPermissions.includes(p))) {
+ hasPermission = true;
+ }
+
+ // If allOf is specified, ensure all required permissions are present
+ if (options.allOf && options.allOf.length > 0) {
+ hasPermission = options.allOf.every(p => userPermissions.includes(p));
+ }
+ }
+
+ // Check if user has the admin:system permission which grants all access
+ if (!hasPermission && req.user.roleId) {
+ const adminPerm = await db.select()
+ .from(rolePermissions)
+ .where(and(
+ eq(rolePermissions.roleId, req.user.roleId),
+ eq(rolePermissions.permission, "admin:system")
+ ))
+ .limit(1);
+
+ if (adminPerm.length > 0) {
+ hasPermission = true;
+ }
+ }
+
+ if (hasPermission) {
+ return next();
+ }
+
+ return res.status(403).json({ message: "Insufficient permissions" });
+ } catch (error) {
+ console.error("Permission check error:", error);
+ return res.status(500).json({ message: "Internal server error" });
+ }
+ });
+ };
+}
+
+// Convenience middleware for requiring administrative access
+export function requireAdmin() {
+ return requirePermission("admin:system");
+}
+
+// Get permissions for a user
+export async function getUserPermissions(userId: number): Promise
{
+ const [user] = await db.select()
+ .from(users)
+ .where(eq(users.id, userId))
+ .limit(1);
+
+ if (!user || !user.roleId) {
+ return [];
+ }
+
+ const rolePerms = await db.select()
+ .from(rolePermissions)
+ .where(eq(rolePermissions.roleId, user.roleId));
+
+ return rolePerms.map(rp => rp.permission);
+}
+
+// Initialize on server startup to ensure default roles
+export async function initializeRBAC() {
+ try {
+ // Check for existing data before doing anything
+ const [adminRole] = await db.select()
+ .from(roles)
+ .where(eq(roles.name, "admin"))
+ .limit(1);
+
+ if (adminRole) {
+ console.log("RBAC already initialized");
+ return true;
+ }
+
+ console.log("RBAC initialization skipped - should be handled by updateSchema.ts");
+ return true;
+ } catch (error) {
+ console.error("Failed to initialize RBAC:", error);
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/server/routes.ts b/server/routes.ts
index 4de0dcf..706d4e4 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -3,8 +3,14 @@ import { createServer, type Server } from "http";
import { setupAuth } from "./auth";
import { setupSwagger } from "./swagger";
import { storage } from "./storage";
-import { apiQuerySchema } from "@shared/schema";
+import { apiQuerySchema, PERMISSIONS } from "@shared/schema";
import { ZodError } from "zod";
+import {
+ requireAuth,
+ requirePermission,
+ requireAdmin,
+ initializeRBAC
+} from "./authorization";
export async function registerRoutes(app: Express): Promise {
// Setup authentication
@@ -13,6 +19,9 @@ export async function registerRoutes(app: Express): Promise {
// Setup Swagger documentation
setupSwagger(app);
+ // Initialize Role Based Access Control system
+ await initializeRBAC();
+
// Error handler for Zod validation errors
const handleZodError = (err: ZodError, res: Response) => {
return res.status(400).json({
@@ -33,14 +42,6 @@ export async function registerRoutes(app: Express): Promise {
}
};
- // Middleware to check admin role
- const requireAdmin = (req: Request, res: Response, next: NextFunction) => {
- if (!req.isAuthenticated() || req.user.role !== "admin") {
- return res.status(403).json({ message: "Access denied: Admin role required" });
- }
- next();
- };
-
/**
* @swagger
* /api/ldap-connections:
@@ -49,6 +50,7 @@ export async function registerRoutes(app: Express): Promise {
* tags: [LDAP Connections]
* security:
* - cookieAuth: []
+ * - bearerAuth: []
* responses:
* 200:
* description: A list of LDAP connections
@@ -60,13 +62,11 @@ export async function registerRoutes(app: Express): Promise {
* $ref: '#/components/schemas/LdapConnection'
* 401:
* $ref: '#/components/responses/UnauthorizedError'
+ * 403:
+ * $ref: '#/components/responses/ForbiddenError'
*/
- app.get("/api/ldap-connections", async (req, res, next) => {
+ app.get("/api/ldap-connections", requirePermission(PERMISSIONS.VIEW_LDAP_CONNECTIONS, { allowApiToken: true }), async (req, res, next) => {
try {
- if (!req.isAuthenticated()) {
- return res.status(401).json({ message: "Not authenticated" });
- }
-
const connections = await storage.listLdapConnections();
// Hide sensitive fields like password
@@ -360,8 +360,13 @@ export async function registerRoutes(app: Express): Promise {
}
// Only allow users to delete their own tokens unless they're admin
- if (token.userId !== req.user.id && req.user.role !== "admin") {
- return res.status(403).json({ message: "Forbidden: You cannot delete tokens that don't belong to you" });
+ if (token.userId !== req.user.id) {
+ // Get the user's role
+ const userRole = await storage.getRole(req.user.roleId!);
+
+ if (userRole?.name !== "admin") {
+ return res.status(403).json({ message: "Forbidden: You cannot delete tokens that don't belong to you" });
+ }
}
const deleted = await storage.deleteApiToken(parseInt(req.params.id));
diff --git a/server/storage.ts b/server/storage.ts
index a1cf450..5cc83ab 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -3,8 +3,9 @@ import {
LdapConnection, InsertLdapConnection,
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
- AdDomain, InsertAdDomain,
- users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains
+ AdDomain, InsertAdDomain, Role,
+ users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
+ roles
} from "@shared/schema";
import session from "express-session";
import createMemoryStore from "memorystore";
@@ -32,7 +33,11 @@ export interface IStorage {
updateUser(id: number, user: Partial): Promise;
deleteUser(id: number): Promise;
listUsers(): Promise;
-
+
+ // Role management
+ getRole(id: number): Promise;
+ getDefaultRole(): Promise;
+
// API Token management
getApiToken(id: number): Promise;
getApiTokenByToken(token: string): Promise;
@@ -126,6 +131,17 @@ export class DatabaseStorage implements IStorage {
async listUsers(): Promise {
return db.select().from(users);
}
+
+ // Role management
+ async getRole(id: number): Promise {
+ const result = await db.select().from(roles).where(eq(roles.id, id));
+ return result.length > 0 ? result[0] : undefined;
+ }
+
+ async getDefaultRole(): Promise {
+ const result = await db.select().from(roles).where(eq(roles.isDefault, true));
+ return result.length > 0 ? result[0] : undefined;
+ }
// API Token management
async getApiToken(id: number): Promise {
@@ -210,7 +226,7 @@ export class DatabaseStorage implements IStorage {
return users.map(user => {
const result: any = { id: user.id };
- properties.forEach(prop => {
+ properties.forEach((prop: string) => {
if ((user as any)[prop] !== undefined) {
result[prop] = (user as any)[prop];
}
@@ -253,7 +269,7 @@ export class DatabaseStorage implements IStorage {
return groups.map(group => {
const result: any = { id: group.id };
- properties.forEach(prop => {
+ properties.forEach((prop: string) => {
if ((group as any)[prop] !== undefined) {
result[prop] = (group as any)[prop];
}
@@ -296,7 +312,7 @@ export class DatabaseStorage implements IStorage {
return orgUnits.map(ou => {
const result: any = { id: ou.id };
- properties.forEach(prop => {
+ properties.forEach((prop: string) => {
if ((ou as any)[prop] !== undefined) {
result[prop] = (ou as any)[prop];
}
@@ -339,7 +355,7 @@ export class DatabaseStorage implements IStorage {
return computers.map(computer => {
const result: any = { id: computer.id };
- properties.forEach(prop => {
+ properties.forEach((prop: string) => {
if ((computer as any)[prop] !== undefined) {
result[prop] = (computer as any)[prop];
}
@@ -382,7 +398,7 @@ export class DatabaseStorage implements IStorage {
return domains.map(domain => {
const result: any = { id: domain.id };
- properties.forEach(prop => {
+ properties.forEach((prop: string) => {
if ((domain as any)[prop] !== undefined) {
result[prop] = (domain as any)[prop];
}
diff --git a/server/swagger.ts b/server/swagger.ts
index 16d13c8..1771889 100644
--- a/server/swagger.ts
+++ b/server/swagger.ts
@@ -53,7 +53,7 @@ const swaggerOptions = {
name: { type: "string" },
token: { type: "string" },
userId: { type: "integer" },
- permissions: { type: "object" },
+ customPermissions: { type: "array", items: { type: "string" } },
expiresAt: { type: "string", format: "date-time" },
createdAt: { type: "string", format: "date-time" },
},
@@ -240,9 +240,18 @@ const swaggerOptions = {
const swaggerSpec = swaggerJsdoc(swaggerOptions);
export function setupSwagger(app: Express) {
+ // Mount at both paths for backward compatibility
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
+ app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
+
+ // Provide the JSON spec at multiple paths
app.get("/api/swagger.json", (req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
});
+
+ app.get("/api-docs/swagger.json", (req, res) => {
+ res.setHeader("Content-Type", "application/json");
+ res.send(swaggerSpec);
+ });
}
diff --git a/shared/schema.ts b/shared/schema.ts
index ca48c23..9186d0b 100644
--- a/shared/schema.ts
+++ b/shared/schema.ts
@@ -1,6 +1,104 @@
-import { pgTable, text, serial, integer, boolean, timestamp, jsonb } from "drizzle-orm/pg-core";
+import { pgTable, text, serial, integer, boolean, timestamp, jsonb, primaryKey } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { z } from "zod";
+import { relations } from "drizzle-orm";
+
+// Role-based access control tables
+export const roles = pgTable("roles", {
+ id: serial("id").primaryKey(),
+ name: text("name").notNull().unique(),
+ description: text("description"),
+ isDefault: boolean("is_default").default(false),
+ createdAt: timestamp("created_at").defaultNow(),
+});
+
+// Define available permissions
+export const PERMISSIONS = {
+ // User management
+ VIEW_USERS: "view:users",
+ CREATE_USERS: "create:users",
+ UPDATE_USERS: "update:users",
+ DELETE_USERS: "delete:users",
+
+ // LDAP connections
+ VIEW_LDAP_CONNECTIONS: "view:ldap_connections",
+ CREATE_LDAP_CONNECTIONS: "create:ldap_connections",
+ UPDATE_LDAP_CONNECTIONS: "update:ldap_connections",
+ DELETE_LDAP_CONNECTIONS: "delete:ldap_connections",
+
+ // Active Directory management
+ VIEW_AD_USERS: "view:ad_users",
+ CREATE_AD_USERS: "create:ad_users",
+ UPDATE_AD_USERS: "update:ad_users",
+ DELETE_AD_USERS: "delete:ad_users",
+
+ VIEW_AD_GROUPS: "view:ad_groups",
+ CREATE_AD_GROUPS: "create:ad_groups",
+ UPDATE_AD_GROUPS: "update:ad_groups",
+ DELETE_AD_GROUPS: "delete:ad_groups",
+
+ VIEW_AD_OUS: "view:ad_ous",
+ CREATE_AD_OUS: "create:ad_ous",
+ UPDATE_AD_OUS: "update:ad_ous",
+ DELETE_AD_OUS: "delete:ad_ous",
+
+ VIEW_AD_COMPUTERS: "view:ad_computers",
+ CREATE_AD_COMPUTERS: "create:ad_computers",
+ UPDATE_AD_COMPUTERS: "update:ad_computers",
+ DELETE_AD_COMPUTERS: "delete:ad_computers",
+
+ VIEW_AD_DOMAINS: "view:ad_domains",
+
+ // API Token management
+ MANAGE_API_TOKENS: "manage:api_tokens",
+
+ // Administrative functions
+ MANAGE_ROLES: "manage:roles",
+ SYSTEM_ADMIN: "admin:system",
+} as const;
+
+// Create a Zod schema for permissions
+export const permissionsSchema = z.enum([
+ "view:users",
+ "create:users",
+ "update:users",
+ "delete:users",
+ "view:ldap_connections",
+ "create:ldap_connections",
+ "update:ldap_connections",
+ "delete:ldap_connections",
+ "view:ad_users",
+ "create:ad_users",
+ "update:ad_users",
+ "delete:ad_users",
+ "view:ad_groups",
+ "create:ad_groups",
+ "update:ad_groups",
+ "delete:ad_groups",
+ "view:ad_ous",
+ "create:ad_ous",
+ "update:ad_ous",
+ "delete:ad_ous",
+ "view:ad_computers",
+ "create:ad_computers",
+ "update:ad_computers",
+ "delete:ad_computers",
+ "view:ad_domains",
+ "manage:api_tokens",
+ "manage:roles",
+ "admin:system"
+]);
+
+export type Permission = z.infer;
+
+export const rolePermissions = pgTable("role_permissions", {
+ roleId: integer("role_id").notNull().references(() => roles.id, { onDelete: "cascade" }),
+ permission: text("permission").notNull(),
+}, table => {
+ return {
+ pk: primaryKey({ columns: [table.roleId, table.permission] }),
+ };
+});
// User schema for local authentication
export const users = pgTable("users", {
@@ -9,7 +107,7 @@ export const users = pgTable("users", {
password: text("password").notNull(),
email: text("email"),
fullName: text("full_name"),
- role: text("role").default("user"),
+ roleId: integer("role_id").references(() => roles.id),
createdAt: timestamp("created_at").defaultNow(),
});
@@ -18,8 +116,9 @@ export const apiTokens = pgTable("api_tokens", {
id: serial("id").primaryKey(),
name: text("name").notNull(),
token: text("token").notNull().unique(),
- userId: integer("user_id").notNull(),
- permissions: jsonb("permissions").notNull(),
+ userId: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
+ roleId: integer("role_id").references(() => roles.id),
+ customPermissions: jsonb("custom_permissions"),
expiresAt: timestamp("expires_at"),
createdAt: timestamp("created_at").defaultNow(),
});
@@ -100,7 +199,42 @@ export const adDomains = pgTable("ad_domains", {
adProperties: jsonb("ad_properties"),
});
+// Define relations between tables
+export const rolesRelations = relations(roles, ({ many }) => ({
+ permissions: many(rolePermissions),
+ users: many(users),
+ apiTokens: many(apiTokens),
+}));
+
+export const rolePermissionsRelations = relations(rolePermissions, ({ one }) => ({
+ role: one(roles, {
+ fields: [rolePermissions.roleId],
+ references: [roles.id],
+ }),
+}));
+
+export const usersRelations = relations(users, ({ one, many }) => ({
+ role: one(roles, {
+ fields: [users.roleId],
+ references: [roles.id],
+ }),
+ apiTokens: many(apiTokens),
+}));
+
+export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
+ user: one(users, {
+ fields: [apiTokens.userId],
+ references: [users.id],
+ }),
+ role: one(roles, {
+ fields: [apiTokens.roleId],
+ references: [roles.id],
+ }),
+}));
+
// Generate insertion schemas
+export const insertRoleSchema = createInsertSchema(roles).omit({ id: true, createdAt: true });
+export const insertRolePermissionSchema = createInsertSchema(rolePermissions);
export const insertUserSchema = createInsertSchema(users).omit({ id: true, createdAt: true });
export const insertApiTokenSchema = createInsertSchema(apiTokens).omit({ id: true, createdAt: true });
export const insertLdapConnectionSchema = createInsertSchema(ldapConnections).omit({ id: true, createdAt: true, lastConnected: true });
@@ -127,7 +261,16 @@ export const apiQuerySchema = z.object({
});
// Export types
-export type User = typeof users.$inferSelect;
+export type Role = typeof roles.$inferSelect;
+export type InsertRole = z.infer;
+export type RolePermission = typeof rolePermissions.$inferSelect;
+export type InsertRolePermission = z.infer;
+export type User = typeof users.$inferSelect & {
+ // For API token authentication
+ tokenId?: number;
+ customPermissions?: string[];
+ role?: string;
+};
export type InsertUser = z.infer;
export type ApiToken = typeof apiTokens.$inferSelect;
export type InsertApiToken = z.infer;
diff --git a/updateSchema.ts b/updateSchema.ts
new file mode 100644
index 0000000..0307099
--- /dev/null
+++ b/updateSchema.ts
@@ -0,0 +1,189 @@
+import pg from 'pg';
+import * as schema from './shared/schema';
+
+const { Pool } = pg;
+const pool = new Pool({
+ connectionString: process.env.DATABASE_URL,
+});
+
+async function main() {
+ console.log('Updating database schema...');
+
+ const client = await pool.connect();
+
+ try {
+ await client.query('BEGIN');
+
+ // Create roles table if it doesn't exist
+ await client.query(`
+ CREATE TABLE IF NOT EXISTS "roles" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "description" text,
+ "is_default" boolean DEFAULT false,
+ "created_at" timestamp DEFAULT now(),
+ CONSTRAINT "roles_name_unique" UNIQUE("name")
+ );
+ `);
+
+ // Create role_permissions table if it doesn't exist
+ await client.query(`
+ CREATE TABLE IF NOT EXISTS "role_permissions" (
+ "role_id" integer NOT NULL,
+ "permission" text NOT NULL,
+ CONSTRAINT "role_permissions_role_id_permission_pk" PRIMARY KEY("role_id","permission")
+ );
+ `);
+
+ // Add foreign key constraint to role_permissions table
+ await client.query(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_constraint WHERE conname = 'role_permissions_role_id_roles_id_fk'
+ ) THEN
+ ALTER TABLE "role_permissions"
+ ADD CONSTRAINT "role_permissions_role_id_roles_id_fk"
+ FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE cascade;
+ END IF;
+ END $$;
+ `);
+
+ // Update users table to add role_id column if it doesn't exist
+ await client.query(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'users' AND column_name = 'role_id'
+ ) THEN
+ ALTER TABLE "users" ADD COLUMN "role_id" integer;
+
+ ALTER TABLE "users"
+ ADD CONSTRAINT "users_role_id_roles_id_fk"
+ FOREIGN KEY ("role_id") REFERENCES "roles"("id");
+ END IF;
+ END $$;
+ `);
+
+ // Update api_tokens table
+ await client.query(`
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'api_tokens' AND column_name = 'role_id'
+ ) THEN
+ ALTER TABLE "api_tokens" ADD COLUMN "role_id" integer;
+
+ ALTER TABLE "api_tokens"
+ ADD CONSTRAINT "api_tokens_role_id_roles_id_fk"
+ FOREIGN KEY ("role_id") REFERENCES "roles"("id");
+ END IF;
+
+ IF EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'api_tokens' AND column_name = 'permissions'
+ ) THEN
+ ALTER TABLE "api_tokens" RENAME COLUMN "permissions" TO "custom_permissions";
+ END IF;
+ END $$;
+ `);
+
+ await client.query('COMMIT');
+ console.log('Schema update completed');
+
+ // Insert default roles if they don't exist
+ await client.query('BEGIN');
+
+ const { rows: adminExists } = await client.query(`
+ SELECT id FROM roles WHERE name = 'admin' LIMIT 1
+ `);
+
+ if (adminExists.length === 0) {
+ console.log('Creating admin role...');
+ const { rows: adminRole } = await client.query(`
+ INSERT INTO roles (name, description, is_default)
+ VALUES ('admin', 'Administrator with full system access', false)
+ RETURNING id
+ `);
+
+ // Add all permissions to admin role
+ for (const permission of Object.values(schema.PERMISSIONS)) {
+ await client.query(`
+ INSERT INTO role_permissions (role_id, permission)
+ VALUES ($1, $2)
+ `, [adminRole[0].id, permission]);
+ }
+ }
+
+ const { rows: userExists } = await client.query(`
+ SELECT id FROM roles WHERE name = 'user' LIMIT 1
+ `);
+
+ if (userExists.length === 0) {
+ console.log('Creating user role...');
+ const { rows: userRole } = await client.query(`
+ INSERT INTO roles (name, description, is_default)
+ VALUES ('user', 'Regular user with limited access', true)
+ RETURNING id
+ `);
+
+ // Add basic permissions to user role
+ const basicPermissions = [
+ schema.PERMISSIONS.VIEW_USERS,
+ schema.PERMISSIONS.VIEW_LDAP_CONNECTIONS,
+ schema.PERMISSIONS.VIEW_AD_USERS,
+ schema.PERMISSIONS.VIEW_AD_GROUPS,
+ schema.PERMISSIONS.VIEW_AD_OUS,
+ schema.PERMISSIONS.VIEW_AD_COMPUTERS,
+ schema.PERMISSIONS.VIEW_AD_DOMAINS
+ ];
+
+ for (const permission of basicPermissions) {
+ await client.query(`
+ INSERT INTO role_permissions (role_id, permission)
+ VALUES ($1, $2)
+ `, [userRole[0].id, permission]);
+ }
+ }
+
+ const { rows: apiExists } = await client.query(`
+ SELECT id FROM roles WHERE name = 'api' LIMIT 1
+ `);
+
+ if (apiExists.length === 0) {
+ console.log('Creating api role...');
+ await client.query(`
+ INSERT INTO roles (name, description, is_default)
+ VALUES ('api', 'API access with customizable permissions', false)
+ `);
+ }
+
+ // Update existing users to have the admin role if they have no role assigned
+ await client.query(`
+ DO $$
+ DECLARE
+ admin_role_id integer;
+ BEGIN
+ SELECT id INTO admin_role_id FROM roles WHERE name = 'admin' LIMIT 1;
+
+ IF admin_role_id IS NOT NULL THEN
+ UPDATE users SET role_id = admin_role_id WHERE role_id IS NULL;
+ END IF;
+ END $$;
+ `);
+
+ await client.query('COMMIT');
+ console.log('Data migration completed successfully');
+
+ } catch (error) {
+ await client.query('ROLLBACK');
+ console.error('Migration failed:', error);
+ } finally {
+ client.release();
+ await pool.end();
+ }
+}
+
+main();