Skip to content

migration to services subpage from modals - issue #39#49

Merged
zeedivx merged 54 commits intomainfrom
feat/services-subpage
Jan 21, 2026
Merged

migration to services subpage from modals - issue #39#49
zeedivx merged 54 commits intomainfrom
feat/services-subpage

Conversation

@zeedivx
Copy link
Contributor

@zeedivx zeedivx commented Jan 19, 2026

Summary by CodeRabbit

  • New Features

    • Granular per-resource role permissions and management.
    • Service details page with tabs (Overview, Permissions, Roles) and new confidential flag.
    • Global toast notifications for app-wide feedback.
  • UI Improvements

    • New form primitives and controls (Tabs, Switch, TagInput, FormField, SaveActions).
    • Enhanced service form, credentials/metadata/status cards, and streamlined service list navigation.
  • Bug Fixes / Validation

    • Improved API error parsing and invalid-ID format handling.

✏️ Tip: You can customize this high-level summary in your review settings.

…ions by role; implement user retrieval by role ID
…ed styles and transitions; update event_type enum formatting in schema
…ermissions, and roles tabs; add service details and authentication sections
…ault Authly service; enhance user experience by preventing modifications
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Warning

Rate limit exceeded

@zeedivx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 54 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Introduce per-resource role permissions: add RolePermission model and migration, replace role-level bitmask with permissions arrays, implement propagation logic to bulk-update user permissions and increment user permission versions, update APIs/docs/schemas, and add matching frontend hooks, UI components, and tooling changes.

Changes

Cohort / File(s) Summary
Backend models & migrations
apps/api/internal/domain/role/model.go, apps/api/internal/migrations/000020_create_role_permissions_table.up.sql, apps/api/internal/migrations/000020_create_role_permissions_table.down.sql, apps/api/internal/database/base.go
Add RolePermission and RolePermissionRequest; add Permissions []RolePermission on Role; create role_permissions table; add JSON tags to BaseModel.
Role service & propagation logic
apps/api/internal/domain/role/service.go
Add propagateRoleChanges(oldRole,newRole) to compute per-resource diffs and apply bulk updates/deletes to user permissions; replace per-role bitmask delta path with propagation flow.
Permission repository
apps/api/internal/domain/permission/repository.go
Add GetUsersByRoleID, BulkUpdateUserPermissionsByRole, BulkDeleteUserPermissionsByRole used by propagation logic (handle NULL vs resource).
Role handlers & repo queries
apps/api/internal/domain/role/handler.go, apps/api/internal/domain/role/repository.go
Build Role.Permissions from requests; remove top-level bitmask assignment; preload Permissions in role queries.
Permission model tweak
apps/api/internal/domain/permission/model.go
Make CreatePermissionRequest.Resource json:"resource,omitempty".
API docs / schemas
apps/api/docs/docs.go, apps/api/docs/swagger.{json,yaml}, apps/web/src/lib/api/schema.d.ts
Add RolePermission / RolePermissionRequest types; swap role fields to snake_case; replace bitmask with permissions arrays; mark bitmask deprecated; add 400 invalid-ID responses; add confidential on CreateServiceRequest.
Frontend: admin hooks & API utils
apps/web/src/lib/hooks/admin/useRoles.ts, apps/web/src/lib/hooks/admin/usePermissions.ts, apps/web/src/lib/api/utils.ts, apps/web/src/lib/config.ts
Add react-query hooks for roles/permissions (list/create/update/delete) and cache invalidation; add parseApiError to normalize API errors; add isDefaultAuthlyService.
Frontend: service management UI & components
apps/web/src/app/dashboard/admin/services/[id]/page.tsx, apps/web/src/components/dashboard/admin/services/OverviewTab.tsx, apps/web/src/components/dashboard/admin/services/overview/*, apps/web/src/components/dashboard/admin/services/CreateServiceModal.tsx, apps/web/src/components/dashboard/admin/services/ServiceRow.tsx
Add service detail page with Tabs and Overview form; new overview subcomponents (ServiceForm, BasicDetailsSection, AuthenticationSection, CredentialsCard, MetadataCard, SaveActions, StatusCard); refactor CreateServiceModal to use FormField; ServiceRow replaced per-row modals with a view/manage button.
Frontend: remove per-row modals
apps/web/src/components/dashboard/admin/services/DeleteServiceModal.tsx, .../EditServiceModal.tsx, .../ServiceSecretModal.tsx
Remove Delete/Edit/Secret modal components (migrated to detail workflows).
Frontend: UI primitives & inputs
apps/web/src/components/ui/FormField.tsx, apps/web/src/components/ui/Input.tsx, apps/web/src/components/ui/Switch.tsx, apps/web/src/components/ui/Tabs.tsx, apps/web/src/components/ui/TagInput.tsx, apps/web/src/components/ui/Button.tsx, apps/web/src/components/ui/CopyButton.tsx
Add FormField, Switch, Tabs, TagInput, CopyButton; refactor Input API to startIcon/endIcon and boolean error; expand Button styles. (Audit call sites for Input prop changes.)
Frontend: new copy / utility components
apps/web/src/components/ui/CopyButton.tsx, apps/web/src/components/ui/TagInput.tsx, apps/web/src/components/ui/Switch.tsx
New small UI primitives (copy button, tag input, toggle switch).
Frontend: hooks & pages
apps/web/src/lib/hooks/*, many apps/web/src/app/**
Add/use new admin hooks; many import reorderings and small behavioral adjustments in pages (notably some effect dependency changes—review).
Frontend: layout, toasts & tooling
apps/web/src/app/layout.tsx, apps/web/package.json, apps/web/biome.json, apps/web/.husky/pre-commit, apps/web/.prettierrc.json, Makefile
Add global Toaster (sonner); migrate lint/format to biome with Husky and lint-staged; remove Prettier config; run frontend formatting in Makefile.
Misc frontend edits
many apps/web/src/** files
Numerous import reorderings, prefix→startIcon replacements, CSS focus/icon rule removals, and UI refinements. (Sweep for broken imports and behavior regressions after Input API changes.)

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant AdminUI as Admin UI
  participant RoleSvc as RoleService
  participant PermRepo as PermissionRepo
  participant UserRepo as UserPermissionRepo
  participant DB as Database

  AdminUI->>RoleSvc: UpdateRole(updatedRole)
  RoleSvc->>PermRepo: Load oldRole (with Permissions)
  Note right of RoleSvc: compute per-resource and global diffs (added/removed bits)
  RoleSvc->>PermRepo: BulkUpdateUserPermissionsByRole(roleID, resource, addedBits, removedBits)
  PermRepo->>DB: UPDATE user_permissions (bitwise ops)
  RoleSvc->>PermRepo: BulkDeleteUserPermissionsByRole(roleID, resource) (if resource removed)
  PermRepo->>DB: DELETE FROM user_permissions
  RoleSvc->>PermRepo: GetUsersByRoleID(roleID)
  PermRepo->>DB: SELECT DISTINCT user_id FROM user_permissions
  RoleSvc->>UserRepo: IncrementPermissionVersion(userIDs)
  UserRepo->>DB: UPDATE users SET permission_version = permission_version + 1
  RoleSvc-->>AdminUI: return updated role
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through schemas and tables new,
I split the bitmask so permissions grew.
Tabs and toasts now glow in the light,
Hooks sync versions through day and night —
A little rabbit cheers the permission view.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: migrating from modal-based service management to a dedicated subpage view, which is confirmed by the removal of modal components and addition of a service detail page.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Walkthrough

This pull request refactors the permission system from a single bitmask field to a granular, resource-based model. It introduces new RolePermission and RolePermissionRequest types, updates role creation/update flows to handle multiple permissions, migrates field names to snake_case convention, and provides new UI components for service management with tabs, form fields, and credential display.

Changes

Cohort / File(s) Summary
API Schema & Documentation
apps/api/docs/docs.go, apps/api/docs/swagger.json, apps/api/docs/swagger.yaml
Removed bitmask from CreateRoleRequest/UpdateRoleRequest; added RolePermission and RolePermissionRequest types; added permissions array fields to Role and request structures; renamed timestamp/flag fields (camelCase to snake_case); marked bitmask as deprecated on Role; added confidential field to CreateServiceRequest.
Backend Domain Models
apps/api/internal/domain/role/model.go
Introduced RolePermission struct with RoleID, Resource (nullable), Bitmask fields; added RolePermissionRequest for request payloads; updated Role with Permissions association and JSON tags; updated CreateRoleRequest/UpdateRoleRequest to use Permissions array instead of single Bitmask.
Backend Business Logic
apps/api/internal/domain/role/handler.go, apps/api/internal/domain/role/service.go
Updated CreateRole/UpdateRole handlers to construct RolePermission entries from request Permissions; introduced propagateRoleChanges helper for computing and applying permission deltas across affected users; enhanced AssignRole with per-resource permission handling; added token invalidation via version incrementing.
Permission Repository & Database
apps/api/internal/database/base.go, apps/api/internal/domain/permission/repository.go, apps/api/internal/domain/role/repository.go
Added JSON struct tags (id, created_at, updated_at, deleted_at) to BaseModel; introduced GetUsersByRoleID, BulkUpdateUserPermissionsByRole, BulkDeleteUserPermissionsByRole methods; added Preload("Permissions") to all role repository queries for eager loading.
Database Migrations
apps/api/internal/migrations/000020_create_role_permissions_table.up.sql, .down.sql
Created role_permissions table with id (UUID), role_id (FK), resource (nullable VARCHAR), bitmask, timestamps, and unique constraint on (role_id, resource).
Service Detail Page & Layout
apps/web/src/app/dashboard/admin/services/[id]/page.tsx, apps/web/src/app/layout.tsx, apps/web/package.json
Added new service detail page with tabbed UI (Overview, Permissions, Roles); integrated Toaster component for notifications; added @radix-ui/react-tabs and sonner dependencies.
Service Management Components
apps/web/src/components/dashboard/admin/services/{CreateServiceModal,ServiceRow,ServicesToolbar}.tsx
Updated CreateServiceModal with FormField wrappers and granular field management; simplified ServiceRow to show navigation link instead of dropdown modals; changed Input prop from prefix to startIcon in toolbar.
Removed Service Modals
apps/web/src/components/dashboard/admin/services/{DeleteServiceModal,EditServiceModal,ServiceSecretModal}.tsx
Removed DeleteServiceModal, EditServiceModal, and ServiceSecretModal components—functionality migrated to new ServiceForm and detail pages.
Service Overview Components
apps/web/src/components/dashboard/admin/services/overview/{BasicDetailsSection,AuthenticationSection,CredentialsCard,MetadataCard,StatusCard,SaveActions,ServiceForm}.tsx, apps/web/src/components/dashboard/admin/services/OverviewTab.tsx
Introduced comprehensive service editing UI: BasicDetailsSection (name, description, domain), AuthenticationSection (redirect URIs, scopes), CredentialsCard (client ID/secret), MetadataCard (timestamps), StatusCard (active toggle), SaveActions (unsaved indicator), ServiceForm (orchestrates all sections), and OverviewTab (fetches and displays service).
Base UI Components
apps/web/src/components/ui/{FormField,Input,Switch,Tabs,TagInput}.tsx, apps/web/src/components/ui/Button.tsx, apps/web/src/app/globals.css
Added FormField (label+error+hint wrapper), TagInput (multi-tag input), Tabs (Radix wrapper), Switch (accessible toggle); refactored Input to use startIcon/endIcon instead of prefix/suffix and removed label/description rendering; expanded Button variant styling with richer disabled/active states; removed focus/accessibility styles from globals.css.
Frontend API Integration
apps/web/src/lib/api/schema.d.ts, apps/web/src/lib/api/utils.ts, apps/web/src/lib/hooks/admin/{useRoles,usePermissions}.ts, apps/web/src/lib/config.ts
Added RolePermission/RolePermissionRequest schema definitions; introduced parseApiError utility for structured error extraction; added useServiceRoles, useCreateRole, useUpdateRole, useDeleteRole hooks; added useServicePermissions, useCreatePermission, useUpdatePermission, useDeletePermission hooks; added isDefaultAuthlyService utility function.
Miscellaneous
apps/web/src/components/dashboard/overview/RecentActivityToolbar.tsx
Changed Input prop from prefix to startIcon for search icon rendering.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Handler as Role Handler
    participant Service as Role Service
    participant PermRepo as Permission Repo
    participant UserPermRepo as User Permission Repo
    participant DB as Database

    User->>Handler: UpdateRole(roleID, permissions)
    Handler->>DB: Fetch old role with permissions
    DB-->>Handler: oldRole with Permissions
    Handler->>DB: Update Role + Permissions
    DB-->>Handler: updatedRole
    Handler->>Service: propagateRoleChanges(oldRole, updatedRole)
    Service->>Service: Calculate delta per resource
    Service->>PermRepo: GetUsersByRoleID(roleID)
    PermRepo-->>Service: List of userIDs
    loop For each resource delta
        alt Resource added
            Service->>UserPermRepo: Create/Update user permissions
            UserPermRepo->>DB: Insert/Update records
        else Resource removed
            Service->>UserPermRepo: BulkDeleteUserPermissionsByRole
            UserPermRepo->>DB: Delete records
        else Bitmask changed
            Service->>UserPermRepo: BulkUpdateUserPermissionsByRole
            UserPermRepo->>DB: Update bitmask via bitwise operation
        end
    end
    loop For each affected user
        Service->>UserPermRepo: Increment permission version
        UserPermRepo->>DB: Update permission_version
    end
    Handler-->>User: Success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 From bitmask flat to permissions fine,
Granular resources now align,
Roles with tabs and forms so neat,
Service management complete,
Radix UI makes UI bright—
Hop along, it's done just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: migrating from modal-based service management to a dedicated subpage implementation for services administration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Docstrings were successfully generated.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Fix all issues with AI agents
In `@apps/api/internal/domain/role/service.go`:
- Around line 350-378: The lookup that finds an existing user permission for
each role permission (inside the role.Permissions loop) misses global
permissions because it only matches up.Resource != nil; update the search over
userPerms to explicitly handle nil resources by treating nil==nil as a match for
"__global__" (i.e. if up.Resource == nil && rp.Resource == nil then set
targetPerm), otherwise if up.Resource != nil compare *up.Resource to
resourceKey; keep the rest of the creation logic but ensure the created
permission's Resource remains nil for global permissions and mark
processedResources["__global__"] appropriately to avoid duplicates.
- Around line 217-223: The loop that calls
s.permissionRepo.IncrementPermissionVersion for each uid currently ignores
errors; change it to propagate failures by checking the returned error and
returning it immediately (or returning a wrapped error with context including
uid and oldRole.ID.String()) so token versions don't stay stale; locate the code
around s.permissionRepo.GetUsersByRoleID(...) and update the for _, uid := range
userIDs loop to handle and return IncrementPermissionVersion errors instead of
discarding them.
- Around line 193-205: The FindUserPermission error is being treated as "not
found" unconditionally; update the handling in the block around
s.permissionRepo.FindUserPermission(userID, oldRole.ServiceID.String(), resPtr)
to import "errors" and use errors.Is(err, gorm.ErrRecordNotFound) to only create
a new permission when the record is truly not found, and return any other err
immediately; keep the creation logic for permission.UserPermission and the
subsequent s.permissionRepo.CreateUserPermission(newPerm) unchanged but guarded
by the not-found check.
- Around line 107-112: The unconditional call to
tx.Model(updatedRole).Association("Permissions").Replace(...) can wipe
permissions when updatedRole.Permissions is unpopulated; guard it so you only
replace associations when permissions were explicitly provided by the caller. In
the role update logic (around the Association("Permissions").Replace call in
service.go), wrap the Replace call in a conditional that checks for nil (not
length) — e.g., if updatedRole.Permissions != nil {
tx.Model(updatedRole).Association("Permissions").Replace(...) } — so an omitted
Permissions field leaves existing role_permissions intact while allowing an
intentional empty slice to clear permissions; leave the
txSvc.propagateRoleChanges(oldRole, updatedRole) call as-is.

In `@apps/api/internal/migrations/000020_create_role_permissions_table.up.sql`:
- Around line 1-12: The role_permissions table currently allows NULL resource
and uses UNIQUE(role_id, resource), which combined with soft deletes
(deleted_at) permits duplicates and blocks re-inserts; change the resource
column to NOT NULL and replace the UNIQUE(role_id, resource) constraint with a
partial unique index that only enforces uniqueness for active rows (WHERE
deleted_at IS NULL). Locate the CREATE TABLE for role_permissions and update the
resource definition to NOT NULL, remove the UNIQUE(role_id, resource) line, and
add a partial unique index like UNIQUE INDEX ON role_permissions(role_id,
resource) WHERE deleted_at IS NULL (ensuring any migration also drops the old
constraint/index if present). Ensure the existing idx_role_permissions_role_id
index is preserved.

In
`@apps/web/src/components/dashboard/admin/services/overview/AuthenticationSection.tsx`:
- Around line 43-90: The list rendering uses key={uri} which will collide for
duplicate entries; change the key to a stable unique value such as
`${uri}-${idx}` or just use the index `idx` in the map (modify the map callback
referencing redirectURIs.map((uri, idx) => ...) and the motion.div key) to avoid
collisions, and add an accessible label to the icon-only add Button (the one
using onAddRedirect and HugeiconsIcon/PlusSignIcon) by adding an aria-label like
"Add redirect URI" or visible text to meet accessibility requirements.

In
`@apps/web/src/components/dashboard/admin/services/overview/CredentialsCard.tsx`:
- Around line 28-37: The Client ID copy button lacks the toast feedback that the
Client Secret uses; update the copyId handler (used by the onClick on the button
rendering copiedId, Tick01Icon/Copy01Icon) to call the same
toast.success("Copied") (or the shared helper the secret uses) when copy
succeeds so both copy actions provide consistent feedback; ensure you
import/toast or reuse the existing toast helper and keep the checkmark visual
behavior unchanged.

In `@apps/web/src/components/dashboard/admin/services/overview/MetadataCard.tsx`:
- Around line 34-43: The copy button lacks an accessible label and a button
type; update the button element that calls copyId(serviceId) (the one rendering
HugeiconsIcon with Copy01Icon/Tick01Icon and using copiedId) to include
type="button" and an appropriate aria-label (e.g., "Copy service ID" or "Copied"
depending on copiedId) so screen readers can announce the action and to prevent
accidental form submission.

In `@apps/web/src/components/dashboard/admin/services/overview/SaveActions.tsx`:
- Around line 9-39: The prop type for onSave in SaveActionsProps is incorrect:
it currently expects a React.FormEvent but the Button passes a click handler, so
update SaveActionsProps.onSave to accept a React.MouseEvent handler (e.g.,
React.MouseEvent<HTMLButtonElement> or a generic React.MouseEvent) and update
SaveActions's destructured prop signature accordingly so the onClick signature
of the Button matches the declared type (refer to SaveActionsProps, onSave,
SaveActions and the Button onClick usage).

In `@apps/web/src/components/dashboard/admin/services/overview/StatusCard.tsx`:
- Around line 15-22: The Switch in StatusCard.tsx is missing an accessible name;
update the Switch usage (the one with props checked={isActive}
onCheckedChange={() => onToggle()} disabled={disabled}) to include an accessible
label—either add an explicit aria-label like "Status toggle" or add
aria-labelledby pointing to the existing heading/span by giving the heading/span
a stable id (e.g., id="status-label") and set aria-labelledby="status-label" on
the Switch so screen readers announce the control.

In `@apps/web/src/components/ui/Switch.tsx`:
- Around line 12-29: The Switch component's internal onClick handler is being
overridden because {...props} is spread after the explicit onClick; update the
rendering of the button in the Switch (the forwardRef component) to either
spread {...props} before the explicit controlled props or compose the click
handlers: capture any consumer onClick from props (e.g., const { onClick:
propsOnClick, ...rest } = props) and set button onClick to an composed function
that first runs the internal toggle (if not disabled, call
onCheckedChange(!checked)) and then calls propsOnClick(event); ensure disabled
still short-circuits and ref and other props are preserved.

In `@apps/web/src/components/ui/TagInput.tsx`:
- Around line 61-75: The icon-only remove button in TagInput.tsx lacks an
accessible label so screen readers can't announce the action; update the button
(the button that calls removeTag(tag) and renders HugeiconsIcon with
Cancel01Icon) to include an accessible name (e.g., add aria-label="Remove tag"
or aria-label={`Remove ${tag}`} and/or a visually-hidden text alternative), and
ensure the label is localized if needed and still present when disabled is false
so assistive tech can identify the remove action.
🧹 Nitpick comments (3)
apps/web/src/components/ui/FormField.tsx (1)

15-18: Consider associating label with input for improved accessibility.

The <label> element is not associated with an input via htmlFor/id. Screen readers may not correctly announce the label for the input. Since children is a generic ReactNode, you could add an optional id prop to support this pattern.

♿ Optional enhancement for accessibility
 interface FormFieldProps {
     label?: string;
     error?: string;
     hint?: string;
     children: React.ReactNode;
     className?: string;
+    id?: string;
 }

-export function FormField({ label, error, hint, children, className }: FormFieldProps) {
+export function FormField({ label, error, hint, children, className, id }: FormFieldProps) {
     return (
         <div className={cn("space-y-1.5", className)}>
             {label && (
-                <label className="text-xs font-medium text-white/60 transition-colors group-focus-within:text-white/80">
+                <label htmlFor={id} className="text-xs font-medium text-white/60 transition-colors group-focus-within:text-white/80">
                     {label}
                 </label>
             )}
apps/web/src/components/ui/Input.tsx (1)

16-59: Expose validation state via aria-invalid.

error currently only affects styling. Adding aria-invalid improves accessibility and keeps the control’s state machine clear for assistive tech.

♿ Suggested change
                     <input
                         type={type}
                         className={cn(
                             inputClasses,
                             // Padding with icons
                             startIcon && "pl-9 pr-3",
                             endIcon && !startIcon && "pl-3 pr-9",
                             startIcon && endIcon && "pl-9 pr-9",
                         )}
                         ref={ref}
                         disabled={disabled}
+                        aria-invalid={error || undefined}
                         {...props}
                     />
@@
-        return <input type={type} className={inputClasses} ref={ref} disabled={disabled} {...props} />;
+        return (
+            <input
+                type={type}
+                className={inputClasses}
+                ref={ref}
+                disabled={disabled}
+                aria-invalid={error || undefined}
+                {...props}
+            />
+        );
apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx (1)

55-68: Normalize redirect URIs before validation.

Line 55-68: Trim once and reuse so trailing/leading whitespace doesn’t falsely trigger “Invalid URL format.”

♻️ Suggested refactor
     const handleAddRedirect = () => {
-        if (!newRedirectURI.trim()) return;
-        try {
-            new URL(newRedirectURI);
+        const normalized = newRedirectURI.trim();
+        if (!normalized) return;
+        try {
+            new URL(normalized);
         } catch {
             toast.error("Invalid URL format");
             return;
         }
-        if (redirectURIs.includes(newRedirectURI.trim())) {
+        if (redirectURIs.includes(normalized)) {
             toast.error("URI already exists");
             return;
         }
-        setRedirectURIs([...redirectURIs, newRedirectURI.trim()]);
+        setRedirectURIs([...redirectURIs, normalized]);
         setNewRedirectURI("");
         toast.success("Redirect URI added");
     };

Comment on lines +1 to +12
CREATE TABLE role_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP WITH TIME ZONE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
resource VARCHAR(100),
bitmask NUMERIC(20) NOT NULL DEFAULT 0,
UNIQUE(role_id, resource)
);

CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce resource non-null + uniqueness for active rows.

resource is nullable and combined with soft deletes, which allows multiple NULLs and can prevent re-adding a permission after soft delete. Consider making resource NOT NULL and enforcing uniqueness only for active rows.

🛠️ Proposed migration adjustment
 CREATE TABLE role_permissions (
     id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
     created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
     updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
     deleted_at TIMESTAMP WITH TIME ZONE,
     role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
-    resource VARCHAR(100),
+    resource VARCHAR(100) NOT NULL,
     bitmask NUMERIC(20) NOT NULL DEFAULT 0,
-    UNIQUE(role_id, resource)
 );
 
+CREATE UNIQUE INDEX uq_role_permissions_role_id_resource
+    ON role_permissions(role_id, resource)
+    WHERE deleted_at IS NULL;
+
 CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id);
🤖 Prompt for AI Agents
In `@apps/api/internal/migrations/000020_create_role_permissions_table.up.sql`
around lines 1 - 12, The role_permissions table currently allows NULL resource
and uses UNIQUE(role_id, resource), which combined with soft deletes
(deleted_at) permits duplicates and blocks re-inserts; change the resource
column to NOT NULL and replace the UNIQUE(role_id, resource) constraint with a
partial unique index that only enforces uniqueness for active rows (WHERE
deleted_at IS NULL). Locate the CREATE TABLE for role_permissions and update the
resource definition to NOT NULL, remove the UNIQUE(role_id, resource) line, and
add a partial unique index like UNIQUE INDEX ON role_permissions(role_id,
resource) WHERE deleted_at IS NULL (ensuring any migration also drops the old
constraint/index if present). Ensure the existing idx_role_permissions_role_id
index is preserved.

Comment on lines +9 to +39
interface SaveActionsProps {
hasChanges: boolean;
isSaving: boolean;
onSave: (e: React.FormEvent) => void;
disabled?: boolean;
}

export function SaveActions({ hasChanges, isSaving, onSave, disabled = false }: SaveActionsProps) {
return (
<div className="pt-6 flex items-center justify-between border-t border-white/10">
<div className="text-xs text-white/40">
{hasChanges ? (
<span className="text-amber-400 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
Unsaved changes
</span>
) : (
"All changes saved"
)}
</div>
<Button
onClick={onSave}
disabled={!hasChanges || isSaving || disabled}
icon={isSaving ? <Loader size="sm" /> : <HugeiconsIcon icon={FloppyDiskIcon} size={16} />}
className={cn(
"cursor-pointer",
hasChanges && "bg-emerald-500 hover:bg-emerald-600 border-emerald-600 text-white",
)}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find where SaveActions is imported/used
rg "SaveActions" --type tsx --type ts -B 2 -A 2

Repository: Anvoria/authly

Length of output: 85


🏁 Script executed:

# Check the Button component implementation to understand how it handles onClick/type
fd "Button" --type f | grep -E "\.(tsx|ts)$" | head -20

Repository: Anvoria/authly

Length of output: 95


🏁 Script executed:

# Look for the ServiceForm component mentioned in the original scratchpad
find . -name "*ServiceForm*" -type f

Repository: Anvoria/authly

Length of output: 133


🏁 Script executed:

# Check Button component implementation
cat apps/web/src/components/ui/Button.tsx

Repository: Anvoria/authly

Length of output: 6862


🏁 Script executed:

# Check ServiceForm to see how SaveActions is used and what onSave does
cat apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx

Repository: Anvoria/authly

Length of output: 7260


🏁 Script executed:

# Also search for SaveActions usage with context
rg "SaveActions" -C 5

Repository: Anvoria/authly

Length of output: 4762


Fix type annotation for onSave handler to match actual event type.

onSave is typed as React.FormEvent, but it receives a React.MouseEvent from the button's onClick. While the code works at runtime (both event types have preventDefault()), the type mismatch causes incorrect typing that could mislead future developers about the event semantics.

Suggested fix
 interface SaveActionsProps {
     hasChanges: boolean;
     isSaving: boolean;
-    onSave: (e: React.FormEvent) => void;
+    onSave: (e: React.MouseEvent<HTMLButtonElement>) => void;
     disabled?: boolean;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface SaveActionsProps {
hasChanges: boolean;
isSaving: boolean;
onSave: (e: React.FormEvent) => void;
disabled?: boolean;
}
export function SaveActions({ hasChanges, isSaving, onSave, disabled = false }: SaveActionsProps) {
return (
<div className="pt-6 flex items-center justify-between border-t border-white/10">
<div className="text-xs text-white/40">
{hasChanges ? (
<span className="text-amber-400 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
Unsaved changes
</span>
) : (
"All changes saved"
)}
</div>
<Button
onClick={onSave}
disabled={!hasChanges || isSaving || disabled}
icon={isSaving ? <Loader size="sm" /> : <HugeiconsIcon icon={FloppyDiskIcon} size={16} />}
className={cn(
"cursor-pointer",
hasChanges && "bg-emerald-500 hover:bg-emerald-600 border-emerald-600 text-white",
)}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
interface SaveActionsProps {
hasChanges: boolean;
isSaving: boolean;
onSave: (e: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
}
export function SaveActions({ hasChanges, isSaving, onSave, disabled = false }: SaveActionsProps) {
return (
<div className="pt-6 flex items-center justify-between border-t border-white/10">
<div className="text-xs text-white/40">
{hasChanges ? (
<span className="text-amber-400 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
Unsaved changes
</span>
) : (
"All changes saved"
)}
</div>
<Button
onClick={onSave}
disabled={!hasChanges || isSaving || disabled}
icon={isSaving ? <Loader size="sm" /> : <HugeiconsIcon icon={FloppyDiskIcon} size={16} />}
className={cn(
"cursor-pointer",
hasChanges && "bg-emerald-500 hover:bg-emerald-600 border-emerald-600 text-white",
)}
>
{isSaving ? "Saving..." : "Save Changes"}
</Button>
🤖 Prompt for AI Agents
In `@apps/web/src/components/dashboard/admin/services/overview/SaveActions.tsx`
around lines 9 - 39, The prop type for onSave in SaveActionsProps is incorrect:
it currently expects a React.FormEvent but the Button passes a click handler, so
update SaveActionsProps.onSave to accept a React.MouseEvent handler (e.g.,
React.MouseEvent<HTMLButtonElement> or a generic React.MouseEvent) and update
SaveActions's destructured prop signature accordingly so the onClick signature
of the Button matches the declared type (refer to SaveActionsProps, onSave,
SaveActions and the Button onClick usage).

Comment on lines +15 to +22
<h4 className="text-xs font-semibold text-white uppercase tracking-wider">Status</h4>

<div className="flex items-center justify-between">
<span className={cn("text-sm font-medium", isActive ? "text-emerald-400" : "text-white/40")}>
{isActive ? "Active" : "Disabled"}
</span>
<Switch checked={isActive} onCheckedChange={() => onToggle()} disabled={disabled} />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add an accessible name for the switch.

Right now the toggle has no accessible label; screen readers will announce an unlabeled “switch.” Provide an aria-label (or link to a labeled element) so the control is discoverable.

♿ Suggested fix
-                <Switch checked={isActive} onCheckedChange={() => onToggle()} disabled={disabled} />
+                <Switch
+                    checked={isActive}
+                    onCheckedChange={onToggle}
+                    disabled={disabled}
+                    aria-label={`Service status: ${isActive ? "Active" : "Disabled"}`}
+                />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<h4 className="text-xs font-semibold text-white uppercase tracking-wider">Status</h4>
<div className="flex items-center justify-between">
<span className={cn("text-sm font-medium", isActive ? "text-emerald-400" : "text-white/40")}>
{isActive ? "Active" : "Disabled"}
</span>
<Switch checked={isActive} onCheckedChange={() => onToggle()} disabled={disabled} />
</div>
<h4 className="text-xs font-semibold text-white uppercase tracking-wider">Status</h4>
<div className="flex items-center justify-between">
<span className={cn("text-sm font-medium", isActive ? "text-emerald-400" : "text-white/40")}>
{isActive ? "Active" : "Disabled"}
</span>
<Switch
checked={isActive}
onCheckedChange={onToggle}
disabled={disabled}
aria-label={`Service status: ${isActive ? "Active" : "Disabled"}`}
/>
</div>
🤖 Prompt for AI Agents
In `@apps/web/src/components/dashboard/admin/services/overview/StatusCard.tsx`
around lines 15 - 22, The Switch in StatusCard.tsx is missing an accessible
name; update the Switch usage (the one with props checked={isActive}
onCheckedChange={() => onToggle()} disabled={disabled}) to include an accessible
label—either add an explicit aria-label like "Status toggle" or add
aria-labelledby pointing to the existing heading/span by giving the heading/span
a stable id (e.g., id="status-label") and set aria-labelledby="status-label" on
the Switch so screen readers announce the control.

Comment on lines +61 to +75
{!disabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(tag);
}}
className="ml-1 p-0.5 rounded-md outline-none transition-colors hover:bg-white/20 cursor-pointer"
>
<HugeiconsIcon
icon={Cancel01Icon}
size={12}
className="text-white/40 group-hover/tag:text-white/60"
/>
</button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Icon-only remove button needs an accessible label.

Without an aria-label, screen readers can’t announce the remove action.

🛠️ Suggested fix
-                        <button
+                        <button
                             type="button"
                             onClick={(e) => {
                                 e.stopPropagation();
                                 removeTag(tag);
                             }}
                             className="ml-1 p-0.5 rounded-md outline-none transition-colors hover:bg-white/20 cursor-pointer"
+                            aria-label={`Remove ${tag}`}
                         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{!disabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(tag);
}}
className="ml-1 p-0.5 rounded-md outline-none transition-colors hover:bg-white/20 cursor-pointer"
>
<HugeiconsIcon
icon={Cancel01Icon}
size={12}
className="text-white/40 group-hover/tag:text-white/60"
/>
</button>
{!disabled && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(tag);
}}
className="ml-1 p-0.5 rounded-md outline-none transition-colors hover:bg-white/20 cursor-pointer"
aria-label={`Remove ${tag}`}
>
<HugeiconsIcon
icon={Cancel01Icon}
size={12}
className="text-white/40 group-hover/tag:text-white/60"
/>
</button>
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/TagInput.tsx` around lines 61 - 75, The icon-only
remove button in TagInput.tsx lacks an accessible label so screen readers can't
announce the action; update the button (the button that calls removeTag(tag) and
renders HugeiconsIcon with Cancel01Icon) to include an accessible name (e.g.,
add aria-label="Remove tag" or aria-label={`Remove ${tag}`} and/or a
visually-hidden text alternative), and ensure the label is localized if needed
and still present when disabled is false so assistive tech can identify the
remove action.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/api/internal/domain/service/handler.go (1)

53-61: Document the new 400 response for invalid IDs.

GetService can now return 400 on UUID parse failures, but the Swagger annotations only list 404. Please add a 400 failure entry so clients know about the validation error.

📝 Suggested doc update
 // `@Param` id path string true "Service ID"
 // `@Success` 200 {object} utils.Response{data=ServiceDataResponse} "Service details"
+// `@Failure` 400 {object} utils.APIError "Invalid ID format"
 // `@Failure` 404 {object} utils.APIError "Service not found"
 // `@Router` /admin/services/{id} [get]
apps/web/src/components/dashboard/admin/services/ServiceRow.tsx (1)

44-69: Add type="button" to the copy button.

Similar to MetadataCard, this button lacks an explicit type="button", which could cause unintended form submission if the component is ever nested within a form.

🔧 Proposed fix
                 <TableCell className="py-3">
                     <button
+                        type="button"
                         onClick={handleCopy}
                         className={cn(
apps/web/src/components/dashboard/admin/services/CreateServiceModal.tsx (1)

37-42: Minor regex edge case for slug generation.

The regex /^-|-$/g removes only a single hyphen from the start or end. If a name produces multiple leading/trailing hyphens, they won't all be removed in one pass.

Consider using a more robust pattern:

🔧 Suggested fix
                 newState.slug =
                     newName
                         .toLowerCase()
                         .replace(/[^a-z0-9-_]/g, "-")
                         .replace(/-+/g, "-")
-                        .replace(/^-|-$/g, "") || "";
+                        .replace(/^-+|-+$/g, "") || "";
🤖 Fix all issues with AI agents
In `@apps/api/internal/domain/role/model.go`:
- Around line 23-27: The Resource field in RolePermission is a *string so when
nil it serializes as JSON null which conflicts with the Swagger schema; update
the json tag to omit nulls (add ,omitempty) on the Resource field in
RolePermission (and any other structs that define Resource, e.g., the similar
struct at lines ~40-43) so the field is omitted instead of emitting null, or
alternatively update the API model/schema to mark Resource as nullable—pick one
approach and apply consistently to the Resource fields.

In `@apps/api/internal/domain/role/service.go`:
- Around line 119-234: propagateRoleChanges currently builds newMap from
newRole.Permissions and can delete existing permissions when newRole.Permissions
is nil (meaning "not provided"); change the logic so that if newRole.Permissions
== nil you preserve existing permissions by copying oldMap into newMap
(including the global Bitmask) instead of constructing newMap from newRole;
otherwise keep the existing behavior of building newMap from
newRole.Bitmask/newRole.Permissions. Update references inside
propagateRoleChanges to use this preserved newMap when computing added/removed
bits and when collecting addedResources.

In `@apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx`:
- Around line 161-170: The current onToggle handler in StatusCard uses
setIsActive and immediately calls toast.success with "Service
activated/deactivated", which misleads users because the change is only local
until persisted; update the handler in ServiceForm's onToggle so it either
removes the toast or changes the message to reflect a pending change (e.g.,
"Status changed — remember to save") and ensure the toast is only shown when
isDefaultService is false and after calling setIsActive (or after a successful
save if you prefer persistence-confirmation feedback); reference: StatusCard,
onToggle, isActive, setIsActive, toast.success, isDefaultService, and the Save
flow.

In `@apps/web/src/components/ui/FormField.tsx`:
- Around line 14-18: The container div in the FormField component is missing the
Tailwind "group" class so the label's group-focus-within:text-white/80 variant
won't work; update the container element (the div created with cn("space-y-1.5",
className) inside FormField.tsx) to include "group" (ensuring any incoming
className is preserved) so the label's group-focus-within styles take effect
when child inputs are focused.

In `@apps/web/src/components/ui/Input.tsx`:
- Around line 6-10: Edit the EditUserModal component to stop passing the removed
label and string error props directly to the Input component: remove any
label="..." and error={errors.field} usages on Input and instead wrap each Input
in the new FormField component (use <FormField label="FieldLabel"
error={errors.field}> ... </FormField>), move the placeholder/value/onChange
props into the Input, and pass a boolean error to Input as
error={!!errors.field}; update all Input instances in EditUserModal.tsx (where
formData and errors are used) to follow this pattern so labels and error
messages are handled by FormField.

In `@apps/web/src/lib/api/schema.d.ts`:
- Around line 1556-1569: The TypeScript schema marks bitmask as optional in the
internal_domain_role.RolePermissionRequest type but the backend defines it as a
required uint64; update the RolePermissionRequest definition so bitmask is
non-optional (change bitmask?: number to bitmask: number) to match the backend
contract and keep resource optional as-is.

In `@apps/web/src/lib/hooks/admin/usePermissions.ts`:
- Around line 36-44: The specific invalidation only includes service_id but
useServicePermissions composes the queryKey with optional params (resource,
limit, offset), so update the invalidation in the mutation success path to
either include those optional params from variables.body (e.g., include
variables.body.resource, limit, offset in the queryKey alongside service_id) so
it matches the exact query keys, or remove the specific-key invalidate and rely
solely on the broader invalidateQueries({ queryKey: ["get",
"/admin/permissions"] }) call; adjust the code around
queryClient.invalidateQueries and the variables.body references accordingly.
♻️ Duplicate comments (5)
apps/web/src/components/dashboard/admin/services/overview/MetadataCard.tsx (1)

34-44: Add type="button" to prevent accidental form submission.

The aria-label was added (addressing the previous review), but type="button" is still missing. If this component is ever placed inside a <form>, the button would default to type="submit" and could trigger unintended form submission.

🔧 Proposed fix
                         <button
+                            type="button"
                             onClick={() => copyId(serviceId)}
                             className="absolute right-1 top-1 bottom-1 px-2 text-white/40 hover:text-white hover:bg-white/10 rounded transition-colors cursor-pointer"
                             aria-label={copiedId ? "Service ID copied" : "Copy Service ID"}
                         >
apps/web/src/components/dashboard/admin/services/overview/SaveActions.tsx (1)

9-14: Type mismatch between onSave prop and actual usage persists.

The onSave handler is typed as React.FormEvent but receives a React.MouseEvent<HTMLButtonElement> from the Button's onClick. This was flagged in a previous review and should be addressed.

Suggested fix
 interface SaveActionsProps {
     hasChanges: boolean;
     isSaving: boolean;
-    onSave: (e: React.FormEvent) => void;
+    onSave: (e: React.MouseEvent<HTMLButtonElement>) => void;
     disabled?: boolean;
 }
apps/web/src/components/dashboard/admin/services/overview/CredentialsCard.tsx (1)

28-39: Inconsistent copy feedback between Client ID and Client Secret persists.

Client Secret copy shows a toast notification (toast.success("Copied")), but Client ID copy does not. This was flagged in a previous review. Consider adding consistent feedback for both actions.

Option: Add toast feedback for Client ID copy
                         <button
-                            onClick={() => copyId(clientId)}
+                            onClick={() => {
+                                copyId(clientId);
+                                toast.success("Copied");
+                            }}
                             className="absolute right-1 top-1 bottom-1 px-2 text-white/40 hover:text-white hover:bg-white/10 rounded transition-colors cursor-pointer"

Also applies to: 48-63

apps/web/src/components/dashboard/admin/services/overview/AuthenticationSection.tsx (1)

43-69: Use a stable unique key to prevent collisions with duplicate URIs.

Using key={uri} will cause React key collisions if duplicate redirect URIs are added, leading to unexpected behavior in animations and state.

🔧 Suggested fix
                             {redirectURIs.map((uri, idx) => (
                                 <motion.div
-                                    key={uri}
+                                    key={`${uri}-${idx}`}
                                     initial={{ opacity: 0, height: 0 }}
                                     animate={{ opacity: 1, height: "auto" }}
                                     exit={{ opacity: 0, height: 0 }}
                                     className="flex items-center gap-2 group"
                                 >
apps/api/internal/migrations/000020_create_role_permissions_table.up.sql (1)

1-12: Previous feedback on nullable resource and uniqueness constraint still applies.

The concern about resource being nullable combined with the UNIQUE(role_id, resource) constraint and soft deletes was already flagged in a prior review. The recommended fix—making resource NOT NULL and using a partial unique index on active rows—should be addressed.

🧹 Nitpick comments (8)
apps/api/internal/domain/service/handler.go (1)

68-70: Consider reusing UUID validation for Update/Delete too.

Line 68 introduces a 400 for invalid IDs in GetService, but UpdateService/DeleteService still pass any string to the service layer, which may surface inconsistent 404/500 responses. A shared helper keeps behavior consistent across endpoints.

♻️ Possible helper and usage
+func validateServiceID(c *fiber.Ctx) (string, error) {
+    id := c.Params("id")
+    if id == "" {
+        return "", utils.ErrorResponse(c, utils.NewAPIError("VALIDATION_ERROR", "ID is required", fiber.StatusBadRequest))
+    }
+    if _, err := uuid.Parse(id); err != nil {
+        return "", utils.ErrorResponse(c, utils.NewAPIError("VALIDATION_ERROR", "Invalid ID format", fiber.StatusBadRequest))
+    }
+    return id, nil
+}
- id := c.Params("id")
- if id == "" {
-     return utils.ErrorResponse(c, utils.NewAPIError("VALIDATION_ERROR", "ID is required", fiber.StatusBadRequest))
- }
+ id, err := validateServiceID(c)
+ if err != nil { return err }
apps/web/src/components/dashboard/admin/services/overview/MetadataCard.tsx (1)

21-26: Consider consistent date formatting across the application.

toLocaleString() without explicit options produces locale-dependent output that can vary between users and environments. For an admin dashboard, you may want consistent formatting.

💡 Optional: Use explicit locale/options for consistent output
-                    <span className="text-xs text-white/80">{new Date(createdAt).toLocaleString()}</span>
+                    <span className="text-xs text-white/80">
+                        {new Date(createdAt).toLocaleString("en-US", {
+                            dateStyle: "medium",
+                            timeStyle: "short",
+                        })}
+                    </span>

Apply similarly to updatedAt. Alternatively, create a shared date formatting utility for consistency across the app.

apps/web/src/components/dashboard/admin/services/ServiceRow.tsx (1)

98-107: Redundant accessibility attributes.

The Button has both aria-label="Manage service" and a <span className="sr-only">Manage</span> inside. Screen readers will announce both, which is redundant. Keep one or the other.

💡 Option 1: Keep aria-label, remove sr-only span
                     <Button
                         variant="ghost"
                         size="sm"
                         href={`/dashboard/admin/services/${service.id}`}
                         icon={<HugeiconsIcon icon={ViewIcon} size={16} />}
                         className="px-3"
                         aria-label="Manage service"
-                    >
-                        <span className="sr-only">Manage</span>
-                    </Button>
+                    />
apps/web/src/components/ui/TagInput.tsx (1)

64-88: Consider using a unique key instead of tag value.

Using key={tag} works because duplicates are prevented, but if the deduplication logic were ever bypassed or refactored, this could cause React key conflicts. A more defensive approach would use an index or generate unique IDs.

apps/web/src/components/ui/Input.tsx (1)

40-45: Simplify redundant padding conditions.

The padding logic can be simplified. When both startIcon and endIcon exist, line 44 applies, but lines 42-43 also have partial conditions that become irrelevant.

Suggested simplification
                     className={cn(
                         inputClasses,
-                        // Padding with icons
-                        startIcon && "pl-9 pr-3",
-                        endIcon && !startIcon && "pl-3 pr-9",
-                        startIcon && endIcon && "pl-9 pr-9",
+                        startIcon && "pl-9",
+                        endIcon && "pr-9",
                     )}
apps/web/src/components/dashboard/admin/services/overview/AuthenticationSection.tsx (1)

76-76: Consider using block syntax for clarity.

The comma operator works but is unconventional. Using a block makes the intent clearer.

💡 Suggested improvement
-                                onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), onAddRedirect())}
+                                onKeyDown={(e) => {
+                                    if (e.key === "Enter") {
+                                        e.preventDefault();
+                                        onAddRedirect();
+                                    }
+                                }}
apps/web/src/components/dashboard/admin/services/OverviewTab.tsx (1)

18-30: Consider distinct handling for missing service vs. error.

When !service is true but isError is false (e.g., service doesn't exist or API returned empty data), the toast won't fire but the error UI will show. Consider adding a distinct message or logging for this edge case.

💡 Suggested improvement
     useEffect(() => {
         if (isError) {
             toast.error("Failed to load service. Please try again.");
         }
-    }, [isError]);
+    }, [isError]);
+
+    useEffect(() => {
+        if (!isLoading && !isError && !service) {
+            toast.error("Service not found.");
+        }
+    }, [isLoading, isError, service]);

     if (isLoading)
         return (
             <div className="flex justify-center py-20">
                 <Loader size="lg" />
             </div>
         );
-    if (isError || !service) return <div className="text-red-400 text-center py-20">Failed to load service</div>;
+    if (isError) return <div className="text-red-400 text-center py-20">Failed to load service</div>;
+    if (!service) return <div className="text-red-400 text-center py-20">Service not found</div>;
apps/web/src/app/dashboard/admin/services/[id]/page.tsx (1)

110-111: TODO placeholders are tracked for future implementation.

The Permissions and Roles tabs are scaffolded with TODO comments. Consider creating issues to track this work if not already done.

Would you like me to help open issues to track implementing the Permissions and Roles tabs?

Comment on lines +161 to +170
<StatusCard
isActive={isActive}
onToggle={() => {
if (!isDefaultService) {
setIsActive(!isActive);
toast.success(`Service ${!isActive ? "activated" : "deactivated"}`);
}
}}
disabled={isDefaultService}
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading feedback: toast suggests change is saved when it's only local state.

The toast on line 166 says "Service activated/deactivated" when the user toggles the switch, but the change isn't actually persisted until the user clicks Save. This could mislead users into thinking the change took effect immediately.

Consider either removing this toast or rewording it to indicate the change is pending (e.g., "Status changed – remember to save").

💡 Suggested fix
                     onToggle={() => {
                         if (!isDefaultService) {
                             setIsActive(!isActive);
-                            toast.success(`Service ${!isActive ? "activated" : "deactivated"}`);
                         }
                     }}

Or, if you want to keep feedback:

-                            toast.success(`Service ${!isActive ? "activated" : "deactivated"}`);
+                            toast.info(`Status will be ${!isActive ? "active" : "disabled"} after saving`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<StatusCard
isActive={isActive}
onToggle={() => {
if (!isDefaultService) {
setIsActive(!isActive);
toast.success(`Service ${!isActive ? "activated" : "deactivated"}`);
}
}}
disabled={isDefaultService}
/>
<StatusCard
isActive={isActive}
onToggle={() => {
if (!isDefaultService) {
setIsActive(!isActive);
}
}}
disabled={isDefaultService}
/>
Suggested change
<StatusCard
isActive={isActive}
onToggle={() => {
if (!isDefaultService) {
setIsActive(!isActive);
toast.success(`Service ${!isActive ? "activated" : "deactivated"}`);
}
}}
disabled={isDefaultService}
/>
<StatusCard
isActive={isActive}
onToggle={() => {
if (!isDefaultService) {
setIsActive(!isActive);
toast.info(`Status will be ${!isActive ? "active" : "disabled"} after saving`);
}
}}
disabled={isDefaultService}
/>
🤖 Prompt for AI Agents
In `@apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx`
around lines 161 - 170, The current onToggle handler in StatusCard uses
setIsActive and immediately calls toast.success with "Service
activated/deactivated", which misleads users because the change is only local
until persisted; update the handler in ServiceForm's onToggle so it either
removes the toast or changes the message to reflect a pending change (e.g.,
"Status changed — remember to save") and ensure the toast is only shown when
isDefaultService is false and after calling setIsActive (or after a successful
save if you prefer persistence-confirmation feedback); reference: StatusCard,
onToggle, isActive, setIsActive, toast.success, isDefaultService, and the Save
flow.

Comment on lines +14 to +18
<div className={cn("space-y-1.5", className)}>
{label && (
<label className="text-xs font-medium text-white/60 transition-colors group-focus-within:text-white/80">
{label}
</label>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The group-focus-within class won't take effect without a parent group class.

The label uses group-focus-within:text-white/80 to change color when a child input is focused, but the container div at line 14 is missing the group class that Tailwind requires for group-based variants to work.

🔧 Proposed fix
-        <div className={cn("space-y-1.5", className)}>
+        <div className={cn("group space-y-1.5", className)}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className={cn("space-y-1.5", className)}>
{label && (
<label className="text-xs font-medium text-white/60 transition-colors group-focus-within:text-white/80">
{label}
</label>
<div className={cn("group space-y-1.5", className)}>
{label && (
<label className="text-xs font-medium text-white/60 transition-colors group-focus-within:text-white/80">
{label}
</label>
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/FormField.tsx` around lines 14 - 18, The container
div in the FormField component is missing the Tailwind "group" class so the
label's group-focus-within:text-white/80 variant won't work; update the
container element (the div created with cn("space-y-1.5", className) inside
FormField.tsx) to include "group" (ensuring any incoming className is preserved)
so the label's group-focus-within styles take effect when child inputs are
focused.

Comment on lines +36 to +44
queryClient.invalidateQueries({
queryKey: [
"get",
"/admin/permissions",
{ params: { query: { service_id: variables.body.service_id } } },
],
});
// Also invalidate without specific params just in case
queryClient.invalidateQueries({ queryKey: ["get", "/admin/permissions"] });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the usePermissions.ts file to see the complete context
cat -n apps/web/src/lib/hooks/admin/usePermissions.ts

Repository: Anvoria/authly

Length of output: 2521


🏁 Script executed:

#!/bin/bash
# Find how $api.useQuery is called with /admin/permissions endpoint
rg -n "useQuery.*permissions|/admin/permissions" apps/web/src/ -t ts -t tsx -B 2 -A 2

Repository: Anvoria/authly

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Locate the $api client implementation to understand query key structure
fd -e ts -e tsx "api" apps/web/src/lib | head -20

Repository: Anvoria/authly

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Find the $api client implementation
fd -e ts "client" apps/web/src/lib/api/ 2>/dev/null || fd -e ts "client" apps/web/src/ 2>/dev/null | head -20

Repository: Anvoria/authly

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Also check the authly lib path mentioned in imports
find . -path "*authly/lib/api*" -name "*.ts" 2>/dev/null | head -20

Repository: Anvoria/authly

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for $api definition and useQuery implementation
rg -n "\$api\s*=" apps/web/src/ -A 5 --max-count=10

Repository: Anvoria/authly

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Examine the $api client implementation
cat -n apps/web/src/lib/api/client.ts

Repository: Anvoria/authly

Length of output: 3204


🏁 Script executed:

#!/bin/bash
# Look for useQuery and useMutation implementations specifically
rg -n "useQuery|useMutation" apps/web/src/lib/api/client.ts -A 15

Repository: Anvoria/authly

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for openapi-react-query package to understand how it constructs query keys
rg -n "openapi-react-query" apps/web/ -A 2 -B 2

Repository: Anvoria/authly

Length of output: 1757


🏁 Script executed:

#!/bin/bash
# Check package.json for the version of openapi-react-query
cat -n apps/web/package.json | grep -A 2 -B 2 "openapi-react-query"

Repository: Anvoria/authly

Length of output: 234


🌐 Web query:

openapi-react-query useQuery query key structure

💡 Result:

In openapi-react-query the query key is an array: [method, path, params]. "method" is the HTTP verb, "path" is the OpenAPI path string, and "params" are the request params (path/query/header/body as applicable). This is documented for useQuery/useInfiniteQuery/queryOptions. [1][2]

Sources

  • openapi-react-query useQuery docs. [1]
  • openapi-react-query queryOptions / useInfiniteQuery docs. [2]

The specific query invalidation won't match queries with optional parameters.

The useServicePermissions hook spreads optional params (resource, limit, offset) into its query key, but the invalidation at lines 37-41 only includes service_id. This means queries made with those optional parameters won't be invalidated by the specific key—only the broad fallback at line 44 will invalidate them.

Either include the relevant optional params in the specific invalidation, or rely solely on the partial key match at line 44:

queryClient.invalidateQueries({ queryKey: ["get", "/admin/permissions"] });
🤖 Prompt for AI Agents
In `@apps/web/src/lib/hooks/admin/usePermissions.ts` around lines 36 - 44, The
specific invalidation only includes service_id but useServicePermissions
composes the queryKey with optional params (resource, limit, offset), so update
the invalidation in the mutation success path to either include those optional
params from variables.body (e.g., include variables.body.resource, limit, offset
in the queryKey alongside service_id) so it matches the exact query keys, or
remove the specific-key invalidate and rely solely on the broader
invalidateQueries({ queryKey: ["get", "/admin/permissions"] }) call; adjust the
code around queryClient.invalidateQueries and the variables.body references
accordingly.

… rules; refactor components for better performance and consistency
… for improved error handling in service endpoints
…ePermissionRequest and RolePermissionRequest for better JSON handling
…nsuring correct bitmask mapping from old roles
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/web/src/app/auth/login/page.tsx (1)

54-54: Use uppercase "S256" for code_challenge_method per RFC 7636.

The PKCE specification (RFC 7636, Section 4.3) defines the code challenge method as "S256" (uppercase). Many authorization servers are case-sensitive and may reject or mishandle lowercase values.

🔧 Proposed fix
-                        params.set("code_challenge_method", "s256");
+                        params.set("code_challenge_method", "S256");
apps/web/src/app/auth/register/page.tsx (1)

93-152: Fix type mismatch: error prop expects boolean but receives string from zod validation.

The Input component's error prop is typed as boolean (line 6 of Input.tsx), but the form passes string values from zod: errors.field?.message. This causes a type error and the error styling won't apply correctly.

Options to fix:

  1. Update Input component to accept string | boolean and display error messages directly, or
  2. Pass boolean: error={!!errors.field?.message} and display messages separately (e.g., below input)
apps/web/src/app/authorize/page.tsx (1)

184-198: Prevent repeated auto-approval after mutation settles.
When confirmMutation.isPending flips back to false (success/error), the effect can fire again while still on the consent screen, triggering a second non‑idempotent confirmation. Add a one‑shot guard (e.g., useRef) so auto-approve runs only once.

✅ Suggested fix
-import { Suspense, useCallback, useEffect, useMemo } from "react";
+import { Suspense, useCallback, useEffect, useMemo, useRef } from "react";
@@
 function AuthorizePageContent() {
+    const autoApproveOnce = useRef(false);
@@
     useEffect(() => {
         if (
             state.type === "consent" &&
             state.client.client_id === OIDC_CONFIG.client_id &&
-            !confirmMutation.isPending
+            !confirmMutation.isPending &&
+            !autoApproveOnce.current
         ) {
+            autoApproveOnce.current = true;
             handleApprove();
         }
     }, [
         state.type,
         confirmMutation.isPending,
         handleApprove,
         // `@ts-expect-error` - state.client.client_id is safe here because we check state.type === "consent" in the effect
         state.client.client_id,
     ]);
apps/web/src/app/dashboard/admin/users/page.tsx (1)

16-34: Pagination is effectively locked to page 1.

The effect runs on page changes and immediately resets to 1, preventing users from navigating beyond the first page. The dependency array should be [debouncedSearch] instead, as documented in the component's JSDoc—resetting on search changes, not on pagination changes.

🛠️ Suggested fix
     useEffect(() => {
         if (page !== 1) {
             setPage(1);
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [page]);
+    }, [debouncedSearch]);
🤖 Fix all issues with AI agents
In `@apps/api/internal/domain/role/service.go`:
- Around line 321-333: The code only subtracts oldRole.Bitmask when up.Resource
== nil and otherwise only subtracts the matching resource bitmask, so global
RolePermission entries (RolePermission with Resource == nil) are not removed;
update the logic around oldRoleMask (used in the block handling up.Resource ==
nil and the branch that searches oldRole.Permissions) to OR-in (oldRoleMask |=
rp.Bitmask) any rp where rp.Resource == nil (and still OR the matching resource
bitmask when present), ensuring oldRoleMask accumulates global permission bits
from oldRole.Permissions before computing up.Bitmask = up.Bitmask &^
oldRoleMask; reference variables: up, oldRole, oldRoleMask, oldRole.Permissions,
rp.Resource, and rp.Bitmask.

In `@apps/web/biome.json`:
- Around line 128-778: The overrides array contains three near-identical JS/TS
override blocks, three TS globals blocks, three TS linter blocks and two JSON
formatter blocks; deduplicate by keeping a single JS/TS override (the one that
includes "globals" and "linter.rules") and ensure it contains the extra
"noBlankTarget": "off" entry present in the third occurrence, keep a single TS
globals override (empty "javascript.globals": []), keep a single TS linter rules
override (with "complexity.noArguments", "style.useConst", and the listed
"suspicious" and "correctness" settings), and keep one JSON formatter override
(trailingCommas: "none"); remove all duplicated blocks so the overrides array
contains only these unique entries.
- Around line 61-101: The includes array in apps/web/biome.json contains
repeated entries; remove duplicates so the array has a single "**" entry and a
single set of exclusion patterns ("!.next/**", "!out/**", "!build/**",
"!next-env.d.ts"); locate the includes key in the JSON, collapse repeated blocks
into one concise list and ensure ordering remains ["**", "!.next/**", "!out/**",
"!build/**", "!next-env.d.ts"] to preserve intent.

In `@apps/web/package.json`:
- Around line 54-55: Remove the unused ESLint artifacts: delete the "eslint" and
"eslint-config-next" entries from package.json (where they appear in
dependencies/devDependencies) and remove the unused configuration file
eslint.config.mjs; after removal, run your package manager install to update the
lockfile (npm/yarn/pnpm) so the dependency graph is consistent—verify existing
lint scripts (e.g., "lint" and "lint:fix") continue to reference Biome and no
other code references "eslint" or "eslint-config-next".

In `@apps/web/src/components/dashboard/admin/services/CreateServiceModal.tsx`:
- Around line 155-160: The Description field doesn't display or clear schema
validation errors; update the FormField/Input wiring so it reads
errors.description (from the component state) and passes it to the FormField's
error prop (or Input's error-related prop) and modify the onChange handler for
Input to also clear errors.description by calling the error-updating function
(or setFormData/setErrors) when the user types; locate the FormField and Input
usage around the Description block and ensure errors.description is shown and
removed on change.

In
`@apps/web/src/components/dashboard/admin/services/overview/CredentialsCard.tsx`:
- Around line 28-31: The copy buttons in the CredentialsCard component (the
button using onClick={() => copyId(clientId)} with aria-label tied to copiedId,
and the similar secret-copy button around lines 49-55) can accidentally submit a
surrounding form because they lack an explicit type; update both buttons to
include type="button" to prevent default form submission while preserving their
onClick handlers (reference the button elements that call copyId and the
secret-copy counterpart and the copiedId state/prop for locating them).

In `@apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx`:
- Around line 39-53: The hasChanges memo currently compares name to service.name
directly which yields a false positive when service.name is undefined/null;
update the comparison in hasChanges to normalize service.name (e.g., use
(service.name || "") ) before comparing so both sides are strings, and likewise
ensure any other optional fields already compared (description, domain) use the
same normalization pattern; modify the equality check in the useMemo block that
references name !== service.name to use name !== (service.name || "") and keep
the rest of the sorted redirectURIs/allowedScopes logic unchanged.
- Around line 55-75: handleAddRedirect trims newRedirectURI only on storage
which causes valid URLs with surrounding whitespace to be rejected; fix by
trimming once at the start (e.g., const uri = newRedirectURI.trim()) and use
that trimmed value for validation (new URL(uri)), duplicate check
(redirectURIs.includes(uri)), adding (setRedirectURIs([...redirectURIs, uri]))
and clearing (setNewRedirectURI("")) so all logic consistently uses the same
trimmed string; update references in handleAddRedirect accordingly.

In `@apps/web/src/components/dashboard/Sidebar.tsx`:
- Around line 70-72: The effect that calls setIsMobileOpen(false) in useEffect
no longer depends on route changes, so the mobile sidebar won't auto-close after
navigation; update the effect in Sidebar.tsx (the useEffect that references
setIsMobileOpen) to include the current route (e.g., pathname from useLocation)
in its dependency array so it re-runs on navigation, or alternatively update the
component's doc comment to state that the sidebar no longer auto-closes on route
changes if this behavior is intentional.

In `@apps/web/src/lib/api/utils.ts`:
- Around line 29-35: The current return uses errorBody?.details cast to
Record<string,string> without runtime checks; update the normalization so
details is only accepted when it's a plain object (typeof === "object" &&
!Array.isArray && not null) and then build a Record<string,string> by iterating
Object.entries(errorBody.details), skipping non-enumerable keys, and converting
each value to a string (for primitives use String(value); for objects/arrays use
JSON.stringify(value)) so setError always receives string values; encapsulate
this logic in a small helper (e.g., normalizeErrorDetails) or inline it where
the returned object is constructed, referencing errorBody and details to locate
the change.
♻️ Duplicate comments (2)
apps/web/src/components/dashboard/admin/services/overview/SaveActions.tsx (1)

9-13: Align onSave type with click handler.
onClick supplies a MouseEvent, not FormEvent. The current type misleads callers.

✅ Suggested fix
 interface SaveActionsProps {
     hasChanges: boolean;
     isSaving: boolean;
-    onSave: (e: React.FormEvent) => void;
+    onSave: (e: React.MouseEvent<HTMLButtonElement>) => void;
     disabled?: boolean;
 }
apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx (1)

161-167: Toast implies persistence before save.

The success toast fires on toggle even though the change is only local until Save. Consider info wording or deferring to save completion.

🧹 Nitpick comments (4)
apps/web/src/app/layout.tsx (1)

51-56: LGTM! Provider hierarchy is correct.

The nesting order (QueryProviderAuthProvider → children) ensures that authentication hooks can use react-query internally. Placing Toaster inside AuthProvider allows toast notifications to be triggered from anywhere in the app.

Consider updating the docstring (lines 41-46) to reflect that content is now also wrapped by AuthProvider and includes a global Toaster.

apps/web/src/app/auth/login/page.tsx (1)

72-82: Consider adding PKCE to the fallback authorization flow.

The OIDC path with oidc_params generates a code verifier/challenge, but the fallback path (lines 72-82) does not use PKCE. For public clients like SPAs, PKCE is recommended as a security best practice to prevent authorization code interception attacks.

♻️ Suggested improvement
         } else {
+            const verifier = generateCodeVerifier();
+            const challenge = await generateCodeChallenge(verifier);
+            LocalStorageTokenService.setOidcCodeVerifier(verifier);
+
             const params = new URLSearchParams({
                 client_id: OIDC_CONFIG.client_id,
                 redirect_uri: OIDC_CONFIG.redirect_uri,
                 response_type: OIDC_CONFIG.response_type,
                 scope: OIDC_CONFIG.scope,
+                code_challenge: challenge,
+                code_challenge_method: "S256",
             });
apps/web/biome.json (1)

132-259: Excessive browser globals list.

The globals list contains ~125 browser event handlers (onanimationend, onclick, etc.). In a typical Next.js/React project, these are rarely needed as globals since event handlers are typically attached via React's synthetic event system.

Consider whether this extensive list is necessary, or if it was auto-generated and could be trimmed to only the globals actually used (e.g., location, exports).

apps/web/.husky/pre-commit (1)

1-1: Consider adding error handling for robustness.

The hook works but will fail silently if the apps/web directory doesn't exist. Adding set -e or explicit error handling would make failures more visible.

💡 Suggested improvement
-cd apps/web && bunx lint-staged
+set -e
+cd apps/web
+bunx lint-staged

Or with an explicit error message:

cd apps/web || { echo "Failed to change to apps/web directory"; exit 1; }
bunx lint-staged

Comment on lines +321 to +333
var oldRoleMask uint64
if up.Resource == nil {
oldRoleMask = oldRole.Bitmask
} else {
for _, rp := range oldRole.Permissions {
if rp.Resource != nil && *rp.Resource == *up.Resource {
oldRoleMask = rp.Bitmask
break
}
}
}
up.Bitmask = up.Bitmask &^ oldRoleMask
up.RoleID = nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove global RolePermission bits when clearing old role.

When up.Resource == nil, you only subtract oldRole.Bitmask. If global permissions are stored via RolePermission{Resource:nil}, those bits remain and users can retain old global access after reassignment. Include nil‑resource RolePermission bitmasks in oldRoleMask.

🐛 Suggested fix
-                if up.Resource == nil {
-                    oldRoleMask = oldRole.Bitmask
-                } else {
+                if up.Resource == nil {
+                    oldRoleMask = oldRole.Bitmask
+                    for _, rp := range oldRole.Permissions {
+                        if rp.Resource == nil {
+                            oldRoleMask |= rp.Bitmask
+                        }
+                    }
+                } else {
🤖 Prompt for AI Agents
In `@apps/api/internal/domain/role/service.go` around lines 321 - 333, The code
only subtracts oldRole.Bitmask when up.Resource == nil and otherwise only
subtracts the matching resource bitmask, so global RolePermission entries
(RolePermission with Resource == nil) are not removed; update the logic around
oldRoleMask (used in the block handling up.Resource == nil and the branch that
searches oldRole.Permissions) to OR-in (oldRoleMask |= rp.Bitmask) any rp where
rp.Resource == nil (and still OR the matching resource bitmask when present),
ensuring oldRoleMask accumulates global permission bits from oldRole.Permissions
before computing up.Bitmask = up.Bitmask &^ oldRoleMask; reference variables:
up, oldRole, oldRoleMask, oldRole.Permissions, rp.Resource, and rp.Bitmask.

zeedivx and others added 13 commits January 21, 2026 15:30
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…opyButton component across ServiceRow, CredentialsCard, and MetadataCard
…g client ID to a variable and updating useEffect dependencies
…rom package.json and bun.lock for cleaner project setup
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@apps/web/biome.json`:
- Around line 242-244: The security rule "noBlankTarget" is currently disabled
which allows target="_blank" without rel attributes; re-enable it by changing
its value from "off" to "error" (or "warn") in the security object (the
"noBlankTarget" property) so the linter enforces adding rel="noopener
noreferrer" for links that use target="_blank".

In `@apps/web/src/components/dashboard/admin/services/ServiceRow.tsx`:
- Around line 37-45: The CopyButton in ServiceRow.tsx currently renders even
when service.client_id is falsy, causing empty strings to be copied and success
toasts to show; update the ServiceRow component so the CopyButton (component
name: CopyButton) is only rendered when service.client_id is truthy (e.g.,
conditional render around the CopyButton using service.client_id), and ensure
the value prop passed is the actual ID (not empty string) and ariaLabel remains
appropriate so no copy action or success toast can occur for missing IDs.

In `@apps/web/src/components/ui/CopyButton.tsx`:
- Around line 18-22: handleClick currently fires toast.success and onCopy
synchronously regardless of whether the clipboard write succeeded; change it to
await the copy operation (or use navigator.clipboard.writeText(value) if
appropriate), then call toast.success(`${ariaLabel} copied!`) and onCopy() only
on success, and catch errors to call toast.error with a helpful message (e.g.,
`${ariaLabel} copy failed`) so failures are handled before showing success.
♻️ Duplicate comments (2)
apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx (1)

162-168: Toast implies save happened, but it’s only local state.

Line 164-168 still shows “activated/deactivated” even though the change isn’t persisted until Save. Consider removing the toast or rewording to “will be … after saving”.

apps/web/package.json (1)

54-55: ESLint deps may be redundant with Biome-only scripts.
If ESLint is no longer invoked anywhere (scripts, CI, editors), consider removing eslint and eslint-config-next to reduce the dependency surface. Please verify usage before removal.

#!/bin/bash
# Verify ESLint usage before removing deps
rg -n --hidden -g '!**/node_modules/**' -e '\beslint\b'
fd -H -t f -g '.eslintrc*'
fd -H -t f -g 'eslint.config.*'
🧹 Nitpick comments (3)
apps/web/src/components/dashboard/Sidebar.tsx (1)

70-73: Remove the unnecessary lint suppression.
The dependency list already includes all referenced values, so the biome-ignore just hides future regressions.

♻️ Suggested change
-    // biome-ignore lint/correctness/useExhaustiveDependencies: we want to close the mobile sidebar when the route changes
     useEffect(() => {
         setIsMobileOpen(false);
     }, [pathname, setIsMobileOpen]);
apps/apps/web/.husky/pre-commit (1)

1-8: Potential duplicate/unused hook path.
Given core.hooksPath is set to apps/web/.husky, this hook under apps/apps/web/.husky likely won’t execute. Consider removing it or aligning the hooks path to keep a single source of truth.

Makefile (1)

27-27: Scope formatting to generated outputs.
Running bun format here formats the whole apps/web tree, which can rewrite unrelated files during codegen. Consider targeting only generated artifacts (e.g., src/lib/api/**) to keep diffs focused and speed up the target. Please verify the script accepts the scoped args in your Bun/Biome setup.

♻️ Suggested adjustment
-	`@cd` apps/web && bun format
+	`@cd` apps/web && bun format -- src/lib/api

Comment on lines +18 to +22
const handleClick = () => {
copy(value);
toast.success(`${ariaLabel} copied!`);
onCopy?.();
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle clipboard failures before toasting success.

Line 18-21 always shows success even if the clipboard write fails. Consider awaiting the copy and only firing success (and onCopy) on success, with a fallback error toast.

💡 Suggested fix
-    const handleClick = () => {
-        copy(value);
-        toast.success(`${ariaLabel} copied!`);
-        onCopy?.();
-    };
+    const handleClick = async () => {
+        try {
+            await copy(value);
+            toast.success(`${ariaLabel} copied!`);
+            onCopy?.();
+        } catch {
+            toast.error(`Failed to copy ${ariaLabel}`);
+        }
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleClick = () => {
copy(value);
toast.success(`${ariaLabel} copied!`);
onCopy?.();
};
const handleClick = async () => {
try {
await copy(value);
toast.success(`${ariaLabel} copied!`);
onCopy?.();
} catch {
toast.error(`Failed to copy ${ariaLabel}`);
}
};
🤖 Prompt for AI Agents
In `@apps/web/src/components/ui/CopyButton.tsx` around lines 18 - 22, handleClick
currently fires toast.success and onCopy synchronously regardless of whether the
clipboard write succeeded; change it to await the copy operation (or use
navigator.clipboard.writeText(value) if appropriate), then call
toast.success(`${ariaLabel} copied!`) and onCopy() only on success, and catch
errors to call toast.error with a helpful message (e.g., `${ariaLabel} copy
failed`) so failures are handled before showing success.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #50

Docstrings generation was requested by @zeedivx.

* #49 (comment)

The following files were modified:

* `apps/api/internal/domain/service/handler.go`
* `apps/web/src/app/auth/login/page.tsx`
* `apps/web/src/app/authorize/page.tsx`
* `apps/web/src/app/dashboard/admin/services/[id]/page.tsx`
* `apps/web/src/app/dashboard/admin/users/page.tsx`
* `apps/web/src/app/layout.tsx`
* `apps/web/src/app/page.tsx`
* `apps/web/src/components/dashboard/admin/services/OverviewTab.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/AuthenticationSection.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/BasicDetailsSection.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/CredentialsCard.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/MetadataCard.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/SaveActions.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/ServiceForm.tsx`
* `apps/web/src/components/dashboard/admin/services/overview/StatusCard.tsx`
* `apps/web/src/components/dashboard/admin/users/EditUserModal.tsx`
* `apps/web/src/components/ui/CopyButton.tsx`
* `apps/web/src/components/ui/FormField.tsx`
* `apps/web/src/components/ui/TagInput.tsx`
* `apps/web/src/lib/hooks/admin/usePermissions.ts`
* `apps/web/src/lib/hooks/admin/useRoles.ts`
* `apps/web/src/proxy.ts`
@zeedivx zeedivx merged commit b6ddad4 into main Jan 21, 2026
3 checks passed
@zeedivx zeedivx deleted the feat/services-subpage branch January 21, 2026 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant