mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-31 12:48:03 +00:00
Implement custom role-based access control. Adds support for creating and managing custom roles, groups, and group memberships.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/c73adeb7-0636-4b42-81ad-b861016d2129.jpg
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { MainLayout } from "@/components/layouts/main-layout";
|
import { MainLayout } from "@/components/layouts/main-layout";
|
||||||
import { ApiToken, InsertApiToken, userRoles } from "@shared/schema";
|
import { ApiToken, InsertApiToken, systemRoles } from "@shared/schema";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { useOrganization } from "@/context/organization-context";
|
import { useOrganization } from "@/context/organization-context";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
@@ -337,7 +337,7 @@ export default function ApiTokensPage() {
|
|||||||
Select the permissions for this token
|
Select the permissions for this token
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
{userRoles.map((role) => (
|
{systemRoles.map((role) => (
|
||||||
<FormField
|
<FormField
|
||||||
key={role}
|
key={role}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { MainLayout } from "@/components/layouts/main-layout";
|
import { MainLayout } from "@/components/layouts/main-layout";
|
||||||
import { User, InsertUser, userRoles, UserRole } from "@shared/schema";
|
import { User, InsertUser, systemRoles, UserRole } from "@shared/schema";
|
||||||
import { useAuth } from "@/hooks/use-auth";
|
import { useAuth } from "@/hooks/use-auth";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -76,7 +76,7 @@ const userFormSchema = z.object({
|
|||||||
email: z.string().email("Please enter a valid email"),
|
email: z.string().email("Please enter a valid email"),
|
||||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||||
fullName: z.string().optional(),
|
fullName: z.string().optional(),
|
||||||
role: z.enum(userRoles as [string, ...string[]]),
|
role: z.enum(systemRoles as [string, ...string[]]),
|
||||||
organizationId: z.number().optional(),
|
organizationId: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export default function UsersRolesPage() {
|
|||||||
// Form for editing a user's role
|
// Form for editing a user's role
|
||||||
const editRoleForm = useForm<{ role: UserRole }>({
|
const editRoleForm = useForm<{ role: UserRole }>({
|
||||||
resolver: zodResolver(z.object({
|
resolver: zodResolver(z.object({
|
||||||
role: z.enum(userRoles as [string, ...string[]]),
|
role: z.enum(systemRoles as [string, ...string[]]),
|
||||||
})),
|
})),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
role: "user",
|
role: "user",
|
||||||
@@ -551,7 +551,7 @@ export default function UsersRolesPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{userRoles.map(role => (
|
{systemRoles.map(role => (
|
||||||
<SelectItem key={role} value={role}>
|
<SelectItem key={role} value={role}>
|
||||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -642,7 +642,7 @@ export default function UsersRolesPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{userRoles.map(role => (
|
{systemRoles.map(role => (
|
||||||
<SelectItem key={role} value={role}>
|
<SelectItem key={role} value={role}>
|
||||||
{role.charAt(0).toUpperCase() + role.slice(1)}
|
{role.charAt(0).toUpperCase() + role.slice(1)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|||||||
+143
-2
@@ -89,6 +89,52 @@ export const apiTokens = pgTable("api_tokens", {
|
|||||||
expiresAt: timestamp("expires_at"),
|
expiresAt: timestamp("expires_at"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Custom roles table for user-defined roles beyond system defaults
|
||||||
|
export const customRoles = pgTable("custom_roles", {
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
name: text("name").notNull().unique(),
|
||||||
|
description: text("description"),
|
||||||
|
permissions: text("permissions").array().notNull(),
|
||||||
|
isActive: boolean("is_active").default(true).notNull(),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
createdBy: uuid("created_by").notNull().references(() => users.id, { onDelete: "set null" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Groups can contain users, organizations, or other groups
|
||||||
|
export const groups = pgTable("groups", {
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
isActive: boolean("is_active").default(true).notNull(),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
createdBy: uuid("created_by").notNull().references(() => users.id, { onDelete: "set null" }),
|
||||||
|
parentGroupId: uuid("parent_group_id"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group members - can be users, organizations, or other groups
|
||||||
|
export const groupMembers = pgTable("group_members", {
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
groupId: uuid("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }),
|
||||||
|
// Specify the type of member: "user", "organization", or "group"
|
||||||
|
memberType: text("member_type").notNull(),
|
||||||
|
// ID of the member (user, organization, or group)
|
||||||
|
memberId: uuid("member_id").notNull(),
|
||||||
|
addedAt: timestamp("added_at").defaultNow().notNull(),
|
||||||
|
addedBy: uuid("added_by").notNull().references(() => users.id, { onDelete: "set null" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group role assignments - associates groups with roles
|
||||||
|
export const groupRoles = pgTable("group_roles", {
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
groupId: uuid("group_id").notNull().references(() => groups.id, { onDelete: "cascade" }),
|
||||||
|
// Can be either a system role (string) or a custom role ID (uuid)
|
||||||
|
roleId: text("role_id").notNull(),
|
||||||
|
// Indicates if this is a system role or a custom role
|
||||||
|
isSystemRole: boolean("is_system_role").notNull(),
|
||||||
|
assignedAt: timestamp("assigned_at").defaultNow().notNull(),
|
||||||
|
assignedBy: uuid("assigned_by").notNull().references(() => users.id, { onDelete: "set null" }),
|
||||||
|
});
|
||||||
|
|
||||||
// Schema Validation
|
// Schema Validation
|
||||||
export const insertUserSchema = createInsertSchema(users).pick({
|
export const insertUserSchema = createInsertSchema(users).pick({
|
||||||
username: true,
|
username: true,
|
||||||
@@ -140,6 +186,36 @@ export const insertApiTokenSchema = createInsertSchema(apiTokens).pick({
|
|||||||
expiresAt: true,
|
expiresAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertCustomRoleSchema = createInsertSchema(customRoles).pick({
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
permissions: true,
|
||||||
|
isActive: true,
|
||||||
|
createdBy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertGroupSchema = createInsertSchema(groups).pick({
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
isActive: true,
|
||||||
|
createdBy: true,
|
||||||
|
parentGroupId: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertGroupMemberSchema = createInsertSchema(groupMembers).pick({
|
||||||
|
groupId: true,
|
||||||
|
memberType: true,
|
||||||
|
memberId: true,
|
||||||
|
addedBy: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertGroupRoleSchema = createInsertSchema(groupRoles).pick({
|
||||||
|
groupId: true,
|
||||||
|
roleId: true,
|
||||||
|
isSystemRole: true,
|
||||||
|
assignedBy: true,
|
||||||
|
});
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type InsertUser = z.infer<typeof insertUserSchema>;
|
export type InsertUser = z.infer<typeof insertUserSchema>;
|
||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
@@ -160,10 +236,27 @@ export type InsertApiToken = z.infer<typeof insertApiTokenSchema>;
|
|||||||
export type ApiToken = typeof apiTokens.$inferSelect;
|
export type ApiToken = typeof apiTokens.$inferSelect;
|
||||||
|
|
||||||
export type DnsHistory = typeof dnsHistory.$inferSelect;
|
export type DnsHistory = typeof dnsHistory.$inferSelect;
|
||||||
|
export type CustomRole = typeof customRoles.$inferSelect;
|
||||||
|
export type Group = typeof groups.$inferSelect;
|
||||||
|
export type GroupMember = typeof groupMembers.$inferSelect;
|
||||||
|
export type GroupRole = typeof groupRoles.$inferSelect;
|
||||||
|
|
||||||
|
export type InsertCustomRole = z.infer<typeof insertCustomRoleSchema>;
|
||||||
|
export type InsertGroup = z.infer<typeof insertGroupSchema>;
|
||||||
|
export type InsertGroupMember = z.infer<typeof insertGroupMemberSchema>;
|
||||||
|
export type InsertGroupRole = z.infer<typeof insertGroupRoleSchema>;
|
||||||
|
|
||||||
// Role Types
|
// Role Types
|
||||||
export const userRoles = ['admin', 'manager', 'user', 'readonly'] as const;
|
// System default roles - these will still be available alongside custom roles
|
||||||
export type UserRole = typeof userRoles[number];
|
export const systemRoles = ['admin', 'manager', 'user', 'readonly'] as const;
|
||||||
|
export type SystemRole = typeof systemRoles[number];
|
||||||
|
|
||||||
|
// User roles can be system roles or custom roles
|
||||||
|
export type UserRole = SystemRole | string;
|
||||||
|
|
||||||
|
// Define member types for group members
|
||||||
|
export const memberTypes = ['user', 'organization', 'group'] as const;
|
||||||
|
export type MemberType = typeof memberTypes[number];
|
||||||
|
|
||||||
// Provider Types
|
// Provider Types
|
||||||
export const providerTypes = ['cloudflare', 'route53', 'godaddy', 'other'] as const;
|
export const providerTypes = ['cloudflare', 'route53', 'godaddy', 'other'] as const;
|
||||||
@@ -225,3 +318,51 @@ export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
|||||||
references: [users.id],
|
references: [users.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Custom roles relations
|
||||||
|
export const customRolesRelations = relations(customRoles, ({ one, many }) => ({
|
||||||
|
creator: one(users, {
|
||||||
|
fields: [customRoles.createdBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
groupRoles: many(groupRoles),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Group relations
|
||||||
|
export const groupsRelations = relations(groups, ({ one, many }) => ({
|
||||||
|
creator: one(users, {
|
||||||
|
fields: [groups.createdBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
parentGroup: one(groups, {
|
||||||
|
fields: [groups.parentGroupId],
|
||||||
|
references: [groups.id],
|
||||||
|
relationName: "parentGroup",
|
||||||
|
}),
|
||||||
|
members: many(groupMembers),
|
||||||
|
roles: many(groupRoles),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Group members relations
|
||||||
|
export const groupMembersRelations = relations(groupMembers, ({ one }) => ({
|
||||||
|
group: one(groups, {
|
||||||
|
fields: [groupMembers.groupId],
|
||||||
|
references: [groups.id],
|
||||||
|
}),
|
||||||
|
addedByUser: one(users, {
|
||||||
|
fields: [groupMembers.addedBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Group roles relations
|
||||||
|
export const groupRolesRelations = relations(groupRoles, ({ one }) => ({
|
||||||
|
group: one(groups, {
|
||||||
|
fields: [groupRoles.groupId],
|
||||||
|
references: [groups.id],
|
||||||
|
}),
|
||||||
|
assignedByUser: one(users, {
|
||||||
|
fields: [groupRoles.assignedBy],
|
||||||
|
references: [users.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user