Skip to content

Commit 9c56c38

Browse files
Clément VALENTINclaude
andcommitted
feat(roles): add persistent default roles and CRUD operations
- Make role seeding truly idempotent: check each permission/role individually - Add CRUD endpoints: POST/GET/PATCH/DELETE for custom roles - Add RoleCreate and RoleUpdate Pydantic schemas with validation - Update frontend with create/delete modals and API methods - Fix updateRolePermissions request body format (embed=True) - Protect system roles from modification/deletion 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 2c8c7fd commit 9c56c38

File tree

6 files changed

+755
-211
lines changed

6 files changed

+755
-211
lines changed

apps/api/src/models/seed.py

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -133,56 +133,73 @@ async def init_default_roles_and_permissions(db: AsyncSession) -> None:
133133
"""
134134
Initialize default roles and permissions in the database.
135135
136-
This function is idempotent - it only creates entries that don't exist yet.
137-
It will not modify existing roles or permissions.
136+
This function is truly idempotent - it creates missing permissions and roles
137+
without modifying existing ones. Each permission and role is checked individually.
138138
"""
139139
try:
140-
# Check if permissions already exist
141-
result = await db.execute(select(Permission).limit(1))
142-
existing_permissions = result.scalar_one_or_none()
140+
logger.info("[SEED] Checking default permissions and roles...")
143141

144-
if existing_permissions:
145-
logger.info("[SEED] Permissions already exist, skipping seed")
146-
return
142+
# Get existing permissions by name
143+
result = await db.execute(select(Permission))
144+
existing_permissions = {p.name: p for p in result.scalars().all()}
147145

148-
logger.info("[SEED] Initializing default permissions and roles...")
146+
# Create missing permissions
147+
permission_objects: dict[str, Permission] = dict(existing_permissions)
148+
permissions_created = 0
149149

150-
# Create permissions
151-
permission_objects = {}
152150
for perm_data in DEFAULT_PERMISSIONS:
153-
permission = Permission(
154-
name=perm_data["name"],
155-
display_name=perm_data["display_name"],
156-
description=perm_data["description"],
157-
resource=perm_data["resource"],
158-
)
159-
db.add(permission)
160-
permission_objects[perm_data["name"]] = permission
161-
logger.info(f"[SEED] Created permission: {perm_data['name']}")
162-
163-
# Flush to get permission IDs
164-
await db.flush()
165-
166-
# Create roles with their permissions
167-
for role_data in DEFAULT_ROLES:
168-
role = Role(
169-
name=role_data["name"],
170-
display_name=role_data["display_name"],
171-
description=role_data["description"],
172-
is_system=role_data["is_system"],
173-
)
151+
if perm_data["name"] not in existing_permissions:
152+
permission = Permission(
153+
name=perm_data["name"],
154+
display_name=perm_data["display_name"],
155+
description=perm_data["description"],
156+
resource=perm_data["resource"],
157+
)
158+
db.add(permission)
159+
permission_objects[perm_data["name"]] = permission
160+
permissions_created += 1
161+
logger.info(f"[SEED] Created permission: {perm_data['name']}")
162+
163+
if permissions_created > 0:
164+
await db.flush()
165+
logger.info(f"[SEED] Created {permissions_created} missing permission(s)")
166+
else:
167+
logger.info("[SEED] All permissions already exist")
174168

175-
# Assign permissions to role
176-
permissions_list = cast(list[str], role_data["permissions"])
177-
for perm_name in permissions_list:
178-
if perm_name in permission_objects:
179-
role.permissions.append(permission_objects[perm_name])
169+
# Get existing roles by name
170+
result = await db.execute(select(Role))
171+
existing_roles = {r.name: r for r in result.scalars().all()}
180172

181-
db.add(role)
182-
logger.info(f"[SEED] Created role: {role_data['name']} with {len(permissions_list)} permissions")
173+
# Create missing roles with their permissions
174+
roles_created = 0
183175

184-
await db.commit()
185-
logger.info("[SEED] Default roles and permissions initialized successfully")
176+
for role_data in DEFAULT_ROLES:
177+
if role_data["name"] not in existing_roles:
178+
role = Role(
179+
name=role_data["name"],
180+
display_name=role_data["display_name"],
181+
description=role_data["description"],
182+
is_system=role_data["is_system"],
183+
)
184+
185+
# Assign permissions to role
186+
permissions_list = cast(list[str], role_data["permissions"])
187+
for perm_name in permissions_list:
188+
if perm_name in permission_objects:
189+
role.permissions.append(permission_objects[perm_name])
190+
191+
db.add(role)
192+
roles_created += 1
193+
logger.info(f"[SEED] Created role: {role_data['name']} with {len(permissions_list)} permissions")
194+
195+
if roles_created > 0:
196+
await db.commit()
197+
logger.info(f"[SEED] Created {roles_created} missing role(s)")
198+
elif permissions_created > 0:
199+
await db.commit()
200+
logger.info("[SEED] All roles already exist, committed new permissions")
201+
else:
202+
logger.info("[SEED] All roles and permissions already exist, nothing to do")
186203

187204
except Exception as e:
188205
logger.error(f"[SEED] Error initializing roles and permissions: {e}")

0 commit comments

Comments
 (0)