Update API documentation and UI to improve clarity and add role-based access control.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/994ee92d-6a6d-4ca4-99c0-fe0dbc7f160b.jpg
This commit is contained in:
alphaeusmote
2025-04-08 02:23:25 +00:00
parent 58f8360240
commit a872feecd2
17 changed files with 1666 additions and 106 deletions
+98
View File
@@ -0,0 +1,98 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import pg from 'pg';
import * as schema from './shared/schema';
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const db = drizzle(pool, { schema });
async function main() {
console.log('Applying migrations...');
try {
// Apply the migrations
await migrate(db, { migrationsFolder: './migrations' });
console.log('Migrations applied successfully');
// Insert default roles if they don't exist
await db.transaction(async (tx) => {
const adminRoleExists = await tx.query.roles.findFirst({
where: (roles, { eq }) => eq(roles.name, 'admin')
});
if (!adminRoleExists) {
console.log('Creating admin role...');
const [adminRole] = await tx.insert(schema.roles).values({
name: 'admin',
description: 'Administrator with full system access',
isDefault: false
}).returning();
// Add all permissions to admin role
const permissions = Object.values(schema.PERMISSIONS);
await Promise.all(permissions.map(permission =>
tx.insert(schema.rolePermissions).values({
roleId: adminRole.id,
permission
})
));
}
const userRoleExists = await tx.query.roles.findFirst({
where: (roles, { eq }) => eq(roles.name, 'user')
});
if (!userRoleExists) {
console.log('Creating user role...');
const [userRole] = await tx.insert(schema.roles).values({
name: 'user',
description: 'Regular user with limited access',
isDefault: true
}).returning();
// 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
];
await Promise.all(basicPermissions.map(permission =>
tx.insert(schema.rolePermissions).values({
roleId: userRole.id,
permission
})
));
}
const apiRoleExists = await tx.query.roles.findFirst({
where: (roles, { eq }) => eq(roles.name, 'api')
});
if (!apiRoleExists) {
console.log('Creating api role...');
await tx.insert(schema.roles).values({
name: 'api',
description: 'API access with customizable permissions',
isDefault: false
});
}
});
console.log('Setup completed successfully');
} catch (error) {
console.error('Migration failed:', error);
} finally {
await pool.end();
}
}
main();
@@ -15,50 +15,50 @@ export default function ApiDocumentationCard() {
<Button
variant="link"
className="p-0 h-auto font-medium text-sm text-primary"
onClick={() => window.open("/api/docs", "_blank")}
onClick={() => window.open("/api-docs", "_blank")}
>
View Full Documentation
</Button>
</CardHeader>
<CardContent className="p-4">
<div className="bg-muted rounded-md p-4 font-mono text-sm overflow-x-auto">
<pre className="text-xs whitespace-pre-wrap">
# Active Directory Management API
<div className="text-xs whitespace-pre-wrap">
<p className="font-bold mb-2"># Active Directory Management API</p>
## User Endpoints
<p className="font-bold mt-3 mb-2">## User Endpoints</p>
GET /api/users
- Query Parameters:
- filter: Filter users by property values (e.g. ?filter=name eq 'John')
- select: Select specific properties to return (e.g. ?select=id,name,email)
- expand: Include related entities (e.g. ?expand=groups)
- orderBy: Order results (e.g. ?orderBy=name asc)
- top: Limit number of results (e.g. ?top=10)
- skip: Skip number of results (e.g. ?skip=10)
<p className="mb-1">GET /api/users</p>
<p className="mb-1">- Query Parameters:</p>
<p className="ml-4 mb-1">- filter: Filter users by property values (e.g. ?filter=name eq 'John')</p>
<p className="ml-4 mb-1">- select: Select specific properties to return (e.g. ?select=id,name,email)</p>
<p className="ml-4 mb-1">- expand: Include related entities (e.g. ?expand=groups)</p>
<p className="ml-4 mb-1">- orderBy: Order results (e.g. ?orderBy=name asc)</p>
<p className="ml-4 mb-1">- top: Limit number of results (e.g. ?top=10)</p>
<p className="ml-4 mb-1">- skip: Skip number of results (e.g. ?skip=10)</p>
POST /api/users
- Create a new user in Active Directory
- Request body: User object
<p className="mt-3 mb-1">POST /api/users</p>
<p className="mb-1">- Create a new user in Active Directory</p>
<p className="mb-1">- Request body: User object</p>
GET /api/users/{id}
- Get a specific user by ID
- Query Parameters:
- select: Select specific properties to return
<p className="mt-3 mb-1">GET /api/users/{"{userId}"}</p>
<p className="mb-1">- Get a specific user by ID</p>
<p className="mb-1">- Query Parameters:</p>
<p className="ml-4 mb-1">- select: Select specific properties to return</p>
PUT /api/users/{id}
- Update a specific user
- Request body: User object with updated properties
<p className="mt-3 mb-1">PUT /api/users/{"{userId}"}</p>
<p className="mb-1">- Update a specific user</p>
<p className="mb-1">- Request body: User object with updated properties</p>
DELETE /api/users/{id}
- Delete a specific user
</pre>
<p className="mt-3 mb-1">DELETE /api/users/{"{userId}"}</p>
<p className="mb-1">- Delete a specific user</p>
</div>
</div>
<div className="mt-4 flex justify-end">
<Button
variant="link"
className="p-0 h-auto font-medium text-sm text-primary"
onClick={() => window.open("/api/docs", "_blank")}
onClick={() => window.open("/api-docs", "_blank")}
>
Go to Swagger Documentation <ArrowRight className="ml-1 h-4 w-4" />
</Button>
+10
View File
@@ -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%;
+7 -8
View File
@@ -160,22 +160,21 @@ export function DashboardLayout({ children, title, description }: DashboardLayou
} ${isMobile ? "absolute" : "relative"}`}
>
<div className="h-16 flex items-center px-4 border-b">
<Link href="/">
<a className="font-medium text-lg text-primary">AD Management API</a>
<Link href="/" className="font-medium text-lg text-primary">
AD Management API
</Link>
</div>
<div className="overflow-y-auto h-[calc(100%-64px)]">
<div className="px-2 pt-4">
<Link href="/">
<a
<Link
href="/"
className={`drawer-item px-4 py-2 flex items-center space-x-3 rounded cursor-pointer ${
location === "/" ? "active" : ""
}`}
>
<Home className="h-4 w-4" />
<span>Dashboard</span>
</a>
</Link>
{menuSections.map((section, idx) => (
@@ -185,15 +184,15 @@ export function DashboardLayout({ children, title, description }: DashboardLayou
</div>
{section.items.map((item, itemIdx) => (
<Link href={item.path} key={itemIdx}>
<a
<Link
href={item.path}
key={itemIdx}
className={`drawer-item px-4 py-2 flex items-center space-x-3 rounded cursor-pointer ${
location === item.path ? "active" : ""
}`}
>
{item.icon}
<span>{item.title}</span>
</a>
</Link>
))}
</div>
+3 -1
View File
@@ -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
},
});
+116
View File
@@ -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;
+714
View File
@@ -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": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1744078354259,
"tag": "0000_fancy_blackheart",
"breakpoints": true
}
]
}
+58 -23
View File
@@ -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": {
+5
View File
@@ -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",
+18 -6
View File
@@ -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,
});
+194
View File
@@ -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<string[]> {
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;
}
}
+20 -15
View File
@@ -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<Server> {
// Setup authentication
@@ -13,6 +19,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
// 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<Server> {
}
};
// 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<Server> {
* 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<Server> {
* $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,9 +360,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
// Only allow users to delete their own tokens unless they're admin
if (token.userId !== req.user.id && req.user.role !== "admin") {
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));
+23 -7
View File
@@ -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";
@@ -33,6 +34,10 @@ export interface IStorage {
deleteUser(id: number): Promise<boolean>;
listUsers(): Promise<User[]>;
// Role management
getRole(id: number): Promise<Role | undefined>;
getDefaultRole(): Promise<Role | undefined>;
// API Token management
getApiToken(id: number): Promise<ApiToken | undefined>;
getApiTokenByToken(token: string): Promise<ApiToken | undefined>;
@@ -127,6 +132,17 @@ export class DatabaseStorage implements IStorage {
return db.select().from(users);
}
// Role management
async getRole(id: number): Promise<Role | undefined> {
const result = await db.select().from(roles).where(eq(roles.id, id));
return result.length > 0 ? result[0] : undefined;
}
async getDefaultRole(): Promise<Role | undefined> {
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<ApiToken | undefined> {
const result = await db.select().from(apiTokens).where(eq(apiTokens.id, id));
@@ -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];
}
+10 -1
View File
@@ -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);
});
}
+148 -5
View File
@@ -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<typeof permissionsSchema>;
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<typeof insertRoleSchema>;
export type RolePermission = typeof rolePermissions.$inferSelect;
export type InsertRolePermission = z.infer<typeof insertRolePermissionSchema>;
export type User = typeof users.$inferSelect & {
// For API token authentication
tokenId?: number;
customPermissions?: string[];
role?: string;
};
export type InsertUser = z.infer<typeof insertUserSchema>;
export type ApiToken = typeof apiTokens.$inferSelect;
export type InsertApiToken = z.infer<typeof insertApiTokenSchema>;
+189
View File
@@ -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();