Conversation
- Introduced new API routes for managing integration accounts, including GET, POST, PATCH, and DELETE methods. - Implemented authentication and authorization checks to ensure secure access to account information. - Enhanced error handling for various scenarios, such as account existence and integration status. - Updated existing routes to utilize the new integrations module for improved organization and maintainability.
- Introduced IntegrationCard component for displaying integration details with title and optional description. - Added IntegrationManagement component for managing integration accounts, including creation and deletion functionalities. - Implemented loading and error states for improved user experience in account management. - Enhanced UI with alert dialogs and toast notifications for user feedback during account operations.
- Introduced new hooks for managing integration accounts, including fetching, creating, updating, and deleting accounts. - Implemented query and mutation functions using React Query for efficient data handling and state management. - Enhanced integration with existing API routes for seamless account operations.
- Introduced a new integrations module for managing integration accounts, including functions for fetching, creating, updating, and deleting accounts. - Updated the API index to export the new integrations module. - Enhanced query keys to support integration-related queries, improving data management and retrieval for integration accounts. - Refactored XMPP account management functions to utilize the new integrations module for streamlined account operations.
- Created a new SQL migration for the integration_accounts table, including fields for id, user_id, integration_type, status, created_at, updated_at, and metadata. - Defined an ENUM type for integration account statuses (active, suspended, deleted). - Added integration account schema definition in the Drizzle ORM, including necessary indexes and foreign key constraints for user_id.
- Added base integration classes and types for managing integration accounts, including methods for creating, fetching, updating, and deleting accounts. - Introduced an integration registry for registering and retrieving integrations, along with utility functions for integration management. - Defined integration status constants and labels for better status handling. - Implemented user account cleanup functionality to ensure proper deletion of integration accounts during user deletion processes.
- Added a new client module for interacting with the Prosody REST API, including functions for creating, deleting, and checking XMPP accounts. - Introduced configuration management for Prosody integration, validating environment variables and ensuring secure access. - Developed an implementation class for XMPP integration, providing methods for account creation, retrieval, updating, and deletion. - Created utility functions for username validation and JID formatting, enhancing account management capabilities. - Established TypeScript types for XMPP account management and Prosody API responses, improving type safety and clarity.
- Implemented a beforeDelete hook in the user deletion configuration to ensure external integration accounts are cleaned up prior to user deletion. - Integrated the cleanupIntegrationAccounts function to handle the removal of associated integration accounts, enhancing user account management.
- Deleted the XMPP client module, configuration management, TypeScript types, and utility functions for Prosody integration. - This cleanup removes unused code and simplifies the codebase, preparing for a potential redesign or alternative integration approach.
- Introduced a new configuration option for server external packages in next.config.ts to specify packages that should not be bundled by Next.js. - Added entries for "@sentry/node-native" and "@sentry-internal/node-native-stacktrace" to support native modules with dynamic requires, enhancing runtime package management.
- Replaced the import of XMPP keys from the deprecated path to the new integration path, ensuring proper access to the required keys for environment configuration. - This change maintains the integrity of the environment setup while aligning with recent refactoring efforts.
- Updated route configuration to replace the XMPP path with a new integrations path, improving clarity and organization. - Introduced functions to build and merge integration routes dynamically, allowing for better navigation management of integration accounts. - Added TypeScript interfaces for integration route options, enhancing type safety and flexibility in route handling.
- Introduced a new function to register all integrations in an idempotent manner, ensuring that integrations are only registered once. - Implemented the registration of the XMPP integration, enhancing the integration management framework.
- Introduced the IntegrationsContent component to display and manage various integrations, including XMPP account management. - Created the IntegrationsPage component to serve as the main entry point for managing integrations, incorporating session verification and metadata generation. - Enhanced user experience with loading states and informative messages for integration availability.
- Replaced the XMPP section in routes.json with a new integrations section for improved clarity. - Updated metadata to reflect the broader scope of service integrations, including XMPP and IRC management.
Reviewer's GuideIntroduce a generic integrations framework (registry, core types, DB schema, user cleanup) and migrate the existing XMPP account functionality onto it, exposing new API routes, React Query hooks, and UI for managing integrations under /app/integrations while keeping XMPP as a first concrete integration. Sequence diagram for creating an XMPP account via integrations pagesequenceDiagram
actor U as User
participant UI as IntegrationsPage
participant Hook as useCreateIntegrationAccount(xmpp)
participant API as POST api_integrations_xmpp_accounts
participant Reg as registerIntegrations
participant Registry as IntegrationRegistry
participant XInt as XmppIntegration
participant DB as PostgreSQL
participant Prosody as ProsodyServer
U->>UI: Click Create XMPP Account
UI->>Hook: call mutateAsync(input)
Hook->>API: HTTP POST /api/integrations/xmpp/accounts
API->>Reg: registerIntegrations()
Reg->>Registry: register XmppIntegration
API->>Registry: get("xmpp")
Registry-->>API: XmppIntegration instance
API->>XInt: createAccount(userId, input)
XInt->>DB: select xmppAccount where userId
DB-->>XInt: no existing account
XInt->>DB: select user email by userId
DB-->>XInt: user email
XInt->>XInt: determineUsername(input.username, email)
XInt->>DB: check username uniqueness in xmpp_account
DB-->>XInt: username available
XInt->>Prosody: createProsodyAccount(username)
Prosody-->>XInt: success
XInt->>DB: insert xmppAccount row
DB-->>XInt: new xmppAccount
XInt-->>API: XmppAccount
API-->>Hook: JSON { ok: true, account }
Hook-->>UI: resolved XmppAccount
UI-->>U: Show success toast and account details
Entity relationship diagram for integration_accounts and XMPP accountserDiagram
user {
text id PK
text email
}
integration_accounts {
text id PK
text user_id FK
text integration_type
enum status
timestamp created_at
timestamp updated_at
jsonb metadata
}
xmpp_account {
text id PK
text user_id FK
text jid
text username
enum status
jsonb metadata
timestamp created_at
timestamp updated_at
}
user ||--o{ integration_accounts : has
user ||--o{ xmpp_account : has
integration_accounts ||..|| xmpp_account : optional_overlap_by_integration
Class diagram for integrations core and XMPP integrationclassDiagram
%% Core types
class IntegrationAccount {
+string id
+string userId
+string integrationId
+string status
+Date createdAt
+Date updatedAt
+Record~string,unknown~ metadata
}
class IntegrationPublicInfo {
+string id
+string name
+string description
+boolean enabled
}
class Integration {
<<interface>>
+string id
+string name
+string description
+boolean enabled
+createAccount(userId string, input IntegrationCreateInput) Promise~IntegrationAccount~
+getAccount(userId string) Promise~IntegrationAccount or null~
+getAccountById(accountId string) Promise~IntegrationAccount or null~
+updateAccount(accountId string, input IntegrationUpdateInput) Promise~IntegrationAccount~
+deleteAccount(accountId string) Promise~void~
+validateIdentifier(identifier string) boolean
+generateIdentifier(email string) string
}
class IntegrationBaseOptions {
+string id
+string name
+string description
+boolean enabled
}
class IntegrationBase {
<<abstract>>
+string id
+string name
+string description
+boolean enabled
+IntegrationBase(options IntegrationBaseOptions)
+createAccount(userId string, input IntegrationCreateInput) Promise~IntegrationAccount~
+getAccount(userId string) Promise~IntegrationAccount or null~
+updateAccount(accountId string, input IntegrationUpdateInput) Promise~IntegrationAccount~
+deleteAccount(accountId string) Promise~void~
}
IntegrationBase ..|> Integration
class IntegrationRegistry {
-Map~string,Integration~ integrations
+register(integration Integration) void
+get(id string) Integration
+getAll() Integration[]
+getEnabled() Integration[]
+isEnabled(id string) boolean
+getPublicInfo() IntegrationPublicInfo[]
}
class XmppAccount {
+string id
+string userId
+"xmpp" integrationId
+string jid
+string username
+string status
+Date createdAt
+Date updatedAt
+Record~string,unknown~ metadata
}
class CreateXmppAccountRequest {
+string username
}
class UpdateXmppAccountRequest {
+string username
+string status
+Record~string,unknown~ metadata
}
class XmppIntegration {
+XmppIntegration()
+createAccount(userId string, input CreateXmppAccountRequest) Promise~XmppAccount~
+getAccount(userId string) Promise~XmppAccount or null~
+getAccountById(accountId string) Promise~XmppAccount or null~
+updateAccount(accountId string, input UpdateXmppAccountRequest) Promise~XmppAccount~
+deleteAccount(accountId string) Promise~void~
-determineUsername(providedUsername string, userEmail string) username or error
-checkUsernameAvailability(username string) availability or error
}
XmppIntegration --|> IntegrationBase
XmppIntegration ..> XmppAccount
XmppIntegration ..> CreateXmppAccountRequest
XmppIntegration ..> UpdateXmppAccountRequest
IntegrationRegistry o--> Integration
class IntegrationApiResponse~T~ {
+boolean ok
+string error
+T account
+IntegrationPublicInfo[] integrations
+string message
}
class IntegrationApiClient {
+fetchIntegrations() Promise~IntegrationPublicInfo[]~
+fetchIntegrationAccount(integrationId string) Promise~T or null~
+fetchIntegrationAccountById(integrationId string, id string) Promise~T~
+createIntegrationAccount(integrationId string, input Record~string,unknown~) Promise~T~
+updateIntegrationAccount(integrationId string, id string, input Record~string,unknown~) Promise~T~
+deleteIntegrationAccount(integrationId string, id string) Promise~void~
}
IntegrationApiClient ..> IntegrationApiResponse
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
- In
IntegrationManagementandIntegrationsContent, you're manually capitalizing and branching on status strings; consider reusing theintegrationStatusLabels(and possibly a shared mapping to badge variants) so status rendering is consistent across all integrations and easier to extend. - The
cleanupIntegrationAccountshelper processes integrations sequentially; if you expect multiple integrations per user, consider running the per-integration cleanup in parallel withPromise.allSettledto avoid unnecessary latency in user deletion workflows while still isolating individual failures.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `IntegrationManagement` and `IntegrationsContent`, you're manually capitalizing and branching on status strings; consider reusing the `integrationStatusLabels` (and possibly a shared mapping to badge variants) so status rendering is consistent across all integrations and easier to extend.
- The `cleanupIntegrationAccounts` helper processes integrations sequentially; if you expect multiple integrations per user, consider running the per-integration cleanup in parallel with `Promise.allSettled` to avoid unnecessary latency in user deletion workflows while still isolating individual failures.
## Individual Comments
### Comment 1
<location> `src/lib/integrations/core/base.ts:17` </location>
<code_context>
+ enabled?: boolean;
+}
+
+export abstract class IntegrationBase<
+ TAccount extends IntegrationAccount = IntegrationAccount,
+ TCreateInput extends IntegrationCreateInput = IntegrationCreateInput,
</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the abstract IntegrationBase class with a small factory helper so integrations implement the Integration interface directly and avoid an extra inheritance layer.
You can get the same type-safety and shared metadata without the extra inheritance layer by replacing `IntegrationBase` with a small helper and having implementations conform directly to `Integration`.
### 1. Replace `IntegrationBase` with a helper
```ts
// integrationBase.ts
import type {
Integration,
IntegrationAccount,
IntegrationCreateInput,
IntegrationUpdateInput,
} from "./types";
export interface IntegrationBaseOptions {
id: string;
name: string;
description: string;
enabled?: boolean;
}
export function createIntegrationBase(options: IntegrationBaseOptions) {
return {
id: options.id,
name: options.name,
description: options.description,
enabled: options.enabled ?? true,
} as const;
}
```
### 2. Implement `Integration` directly and spread the base metadata
```ts
// xmppIntegration.ts
import type {
Integration,
IntegrationAccount,
IntegrationCreateInput,
IntegrationUpdateInput,
} from "./types";
import { createIntegrationBase } from "./integrationBase";
type XmppAccount = IntegrationAccount;
type XmppCreateInput = IntegrationCreateInput;
type XmppUpdateInput = IntegrationUpdateInput;
export const XmppIntegration: Integration<
XmppAccount,
XmppCreateInput,
XmppUpdateInput
> = {
...createIntegrationBase({
id: "xmpp",
name: "XMPP",
description: "XMPP integration",
enabled: true,
}),
async createAccount(userId, input) {
// existing logic
},
async getAccount(userId) {
// existing logic
},
async updateAccount(accountId, input) {
// existing logic
},
async deleteAccount(accountId) {
// existing logic
},
};
```
This keeps:
- The same fields (`id`, `name`, `description`, `enabled`) with identical defaulting behavior.
- The same `Integration<TAccount, TCreateInput, TUpdateInput>` contract.
But it removes:
- The abstract class and inheritance.
- Duplication of method signatures between `Integration` and `IntegrationBase`.
If you need to keep a class for some integrations, you can still use the helper inside a class without making it a base class:
```ts
class XmppIntegration
implements Integration<XmppAccount, XmppCreateInput, XmppUpdateInput>
{
readonly id: string;
readonly name: string;
readonly description: string;
readonly enabled: boolean;
constructor() {
Object.assign(this, createIntegrationBase({
id: "xmpp",
name: "XMPP",
description: "XMPP integration",
}));
}
// same method implementations as before...
}
```
</issue_to_address>
### Comment 2
<location> `src/lib/db/schema/integrations/base.ts:13` </location>
<code_context>
+
+import { user } from "../auth";
+
+export const integrationAccountStatusEnum = pgEnum(
+ "integration_account_status",
+ ["active", "suspended", "deleted"]
</code_context>
<issue_to_address>
**issue (complexity):** Consider reducing conceptual duplication by reusing a shared account status enum and introducing an explicit enum for integrationType, plus a brief comment documenting the future role of this table.
You’re effectively introducing a second “account” model and a second status enum without wiring this into any runtime path yet, which is what’s driving the complexity feedback.
Two concrete ways to reduce conceptual duplication while keeping this table and all functionality:
1. **Share the status enum with the existing account model**
If `xmppAccount` already has a status enum, you can extract it into a shared enum and reuse it here instead of defining `integrationAccountStatusEnum`. That way there’s only one “account status” concept to reason about.
```ts
// shared/account-status.ts
import { pgEnum } from "drizzle-orm/pg-core";
export const accountStatusEnum = pgEnum("account_status", [
"active",
"suspended",
"deleted",
]);
// xmpp-account.ts
import { accountStatusEnum } from "../shared/account-status";
export const xmppAccount = pgTable("xmpp_accounts", {
// ...
status: accountStatusEnum("status").default("active").notNull(),
// ...
});
// integration-account.ts
import { accountStatusEnum } from "../shared/account-status";
export const integrationAccount = pgTable(
"integration_accounts",
{
// ...
status: accountStatusEnum("status").default("active").notNull(),
// ...
},
// ...
);
```
This makes it clear that “status” means the same thing across all account tables and removes one mental branch.
2. **Make `integrationType` explicit with an enum (even if there’s only one for now)**
Right now `integrationType` is a `text`, which encourages arbitrary strings and makes it harder to reason about what’s valid. A small enum constrained to the integrations you plan to support keeps the abstraction but makes it more concrete and self-documenting:
```ts
// shared/integration-type.ts
import { pgEnum } from "drizzle-orm/pg-core";
export const integrationTypeEnum = pgEnum("integration_type", [
"xmpp",
// add future integrations here: "slack", "discord", ...
]);
// integration-account.ts
import { integrationTypeEnum } from "../shared/integration-type";
export const integrationAccount = pgTable(
"integration_accounts",
{
// ...
integrationType: integrationTypeEnum("integration_type").notNull(),
// ...
},
(table) => [
index("integration_accounts_userId_idx").on(table.userId),
index("integration_accounts_type_idx").on(table.integrationType),
uniqueIndex("integration_accounts_userId_type_idx").on(
table.userId,
table.integrationType,
),
],
);
```
This doesn’t change behavior but reduces “generic string” ambiguity and makes it clearer how this table will evolve as more integrations are added.
If you keep the table unused for now, consider at least adding a brief comment above it describing the planned migration path (e.g., “will back all integrations, including xmppAccount, once X/Y rollout is done”) so future readers don’t have to infer intent.
</issue_to_address>
### Comment 3
<location> `src/components/integrations/integration-management.tsx:180` </location>
<code_context>
+ );
+ }
+
+ const status =
+ "status" in account && typeof account.status === "string"
+ ? account.status
</code_context>
<issue_to_address>
**issue (complexity):** Consider tightening the account typing and extracting the create and account-present branches into smaller components (optionally via a hook) to simplify IntegrationManagement’s logic and layout.
You can keep all functionality while reducing complexity by:
1. **Tightening the account type instead of runtime duck-typing**
2. **Extracting small presentational components for the “no account” and “has account” branches**
3. **Optionally extracting the data/mutation wiring into a hook**
### 1. Use a more concrete account shape
You’re already assuming `status` is a string when present. Make that part of the type instead of checking at runtime:
```ts
interface IntegrationAccountBase {
id: string;
status?: string; // optional but typed
}
interface IntegrationManagementProps<TAccount extends IntegrationAccountBase> {
integrationId: string;
title: string;
description: string;
createLabel: string;
createInputLabel?: string;
createInputPlaceholder?: string;
createInputHelp?: string;
createInputToPayload?: (value: string) => Record<string, unknown>;
renderAccountDetails?: (account: TAccount) => ReactNode;
}
export function IntegrationManagement<TAccount extends IntegrationAccountBase>(props: IntegrationManagementProps<TAccount>) {
// ...
const status = account?.status ?? null;
// ...
}
```
This removes the need for:
```ts
const status =
"status" in account && typeof account.status === "string"
? account.status
: null;
```
and avoids mixing generics with runtime duck-typing.
### 2. Extract the “no account yet” create form
The `if (!account)` branch is large; pulling it out into a small child component cuts branching and makes the main component easier to scan:
```tsx
interface NoAccountCardProps {
integrationId: string;
title: string;
description: string;
createLabel: string;
createInputLabel?: string;
createInputPlaceholder?: string;
createInputHelp?: string;
isCreating: boolean;
inputValue: string;
onInputChange: (value: string) => void;
onCreate: () => void;
}
function NoAccountCard({
integrationId,
title,
description,
createLabel,
createInputLabel,
createInputPlaceholder,
createInputHelp,
isCreating,
inputValue,
onInputChange,
onCreate,
}: NoAccountCardProps) {
return (
<Card>
<CardHeader>
<div className="font-semibold text-lg">{title}</div>
<p className="text-muted-foreground text-sm">{description}</p>
</CardHeader>
<CardContent className="space-y-4">
{createInputLabel ? (
<div className="space-y-2">
<Label htmlFor={`${integrationId}-identifier`}>{createInputLabel}</Label>
<Input
disabled={isCreating}
id={`${integrationId}-identifier`}
onChange={(event) => onInputChange(event.target.value)}
placeholder={createInputPlaceholder}
value={inputValue}
/>
{createInputHelp ? (
<p className="text-muted-foreground text-sm">{createInputHelp}</p>
) : null}
</div>
) : null}
</CardContent>
<CardFooter>
<Button className="w-full" disabled={isCreating} onClick={onCreate}>
{isCreating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
createLabel
)}
</Button>
</CardFooter>
</Card>
);
}
```
Then in `IntegrationManagement`:
```tsx
if (!account) {
return (
<NoAccountCard
integrationId={integrationId}
title={title}
description={description}
createLabel={createLabel}
createInputLabel={createInputLabel}
createInputPlaceholder={createInputPlaceholder}
createInputHelp={createInputHelp}
isCreating={createMutation.isPending}
inputValue={inputValue}
onInputChange={setInputValue}
onCreate={handleCreate}
/>
);
}
```
### 3. Extract the “account present” card
Likewise, the “has account” branch can be a focused presentational component:
```tsx
interface AccountCardProps<TAccount extends IntegrationAccountBase> {
title: string;
description: string;
account: TAccount;
status?: string | null;
isDeleting: boolean;
renderAccountDetails?: (account: TAccount) => ReactNode;
onDelete: () => void;
}
function AccountCard<TAccount extends IntegrationAccountBase>({
title,
description,
account,
status,
isDeleting,
renderAccountDetails,
onDelete,
}: AccountCardProps<TAccount>) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<div className="font-semibold text-lg">{title}</div>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
{status ? (
<Badge variant={status === "deleted" ? "destructive" : "default"}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</Badge>
) : null}
</div>
</CardHeader>
<CardContent className="space-y-4">
{renderAccountDetails ? renderAccountDetails(account) : null}
</CardContent>
<CardFooter className="flex justify-end">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button disabled={isDeleting} variant="destructive">
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Account
</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {title} Account?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your {title} account. This action cannot be undone. You will need to create a new account if you want to use {title} again.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={onDelete}
>
Delete Account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardFooter>
</Card>
);
}
```
Usage in `IntegrationManagement`:
```tsx
return (
<AccountCard
title={title}
description={description}
account={account}
status={status}
isDeleting={deleteMutation.isPending}
renderAccountDetails={renderAccountDetails}
onDelete={handleDelete}
/>
);
```
### 4. Optional: move data/mutation wiring to a hook
If you want to further separate concerns, you can wrap the query/mutations + handlers into a small hook:
```ts
function useIntegrationManagement<TAccount extends IntegrationAccountBase>(
integrationId: string,
title: string,
createInputToPayload?: (value: string) => Record<string, unknown>
) {
const { data: account, isLoading, error } = useIntegrationAccount<TAccount>(integrationId);
const createMutation = useCreateIntegrationAccount<TAccount>(integrationId);
const deleteMutation = useDeleteIntegrationAccount(integrationId);
const [inputValue, setInputValue] = useState("");
const handleCreate = async () => { /* existing logic */ };
const handleDelete = async () => { /* existing logic */ };
return {
account,
isLoading,
error,
createMutation,
deleteMutation,
inputValue,
setInputValue,
handleCreate,
handleDelete,
};
}
```
Then `IntegrationManagement` becomes mostly composition of `NoAccountCard` / `AccountCard`, improving readability and testability without changing behavior.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| enabled?: boolean; | ||
| } | ||
|
|
||
| export abstract class IntegrationBase< |
There was a problem hiding this comment.
issue (complexity): Consider replacing the abstract IntegrationBase class with a small factory helper so integrations implement the Integration interface directly and avoid an extra inheritance layer.
You can get the same type-safety and shared metadata without the extra inheritance layer by replacing IntegrationBase with a small helper and having implementations conform directly to Integration.
1. Replace IntegrationBase with a helper
// integrationBase.ts
import type {
Integration,
IntegrationAccount,
IntegrationCreateInput,
IntegrationUpdateInput,
} from "./types";
export interface IntegrationBaseOptions {
id: string;
name: string;
description: string;
enabled?: boolean;
}
export function createIntegrationBase(options: IntegrationBaseOptions) {
return {
id: options.id,
name: options.name,
description: options.description,
enabled: options.enabled ?? true,
} as const;
}2. Implement Integration directly and spread the base metadata
// xmppIntegration.ts
import type {
Integration,
IntegrationAccount,
IntegrationCreateInput,
IntegrationUpdateInput,
} from "./types";
import { createIntegrationBase } from "./integrationBase";
type XmppAccount = IntegrationAccount;
type XmppCreateInput = IntegrationCreateInput;
type XmppUpdateInput = IntegrationUpdateInput;
export const XmppIntegration: Integration<
XmppAccount,
XmppCreateInput,
XmppUpdateInput
> = {
...createIntegrationBase({
id: "xmpp",
name: "XMPP",
description: "XMPP integration",
enabled: true,
}),
async createAccount(userId, input) {
// existing logic
},
async getAccount(userId) {
// existing logic
},
async updateAccount(accountId, input) {
// existing logic
},
async deleteAccount(accountId) {
// existing logic
},
};This keeps:
- The same fields (
id,name,description,enabled) with identical defaulting behavior. - The same
Integration<TAccount, TCreateInput, TUpdateInput>contract.
But it removes:
- The abstract class and inheritance.
- Duplication of method signatures between
IntegrationandIntegrationBase.
If you need to keep a class for some integrations, you can still use the helper inside a class without making it a base class:
class XmppIntegration
implements Integration<XmppAccount, XmppCreateInput, XmppUpdateInput>
{
readonly id: string;
readonly name: string;
readonly description: string;
readonly enabled: boolean;
constructor() {
Object.assign(this, createIntegrationBase({
id: "xmpp",
name: "XMPP",
description: "XMPP integration",
}));
}
// same method implementations as before...
}|
|
||
| import { user } from "../auth"; | ||
|
|
||
| export const integrationAccountStatusEnum = pgEnum( |
There was a problem hiding this comment.
issue (complexity): Consider reducing conceptual duplication by reusing a shared account status enum and introducing an explicit enum for integrationType, plus a brief comment documenting the future role of this table.
You’re effectively introducing a second “account” model and a second status enum without wiring this into any runtime path yet, which is what’s driving the complexity feedback.
Two concrete ways to reduce conceptual duplication while keeping this table and all functionality:
- Share the status enum with the existing account model
If xmppAccount already has a status enum, you can extract it into a shared enum and reuse it here instead of defining integrationAccountStatusEnum. That way there’s only one “account status” concept to reason about.
// shared/account-status.ts
import { pgEnum } from "drizzle-orm/pg-core";
export const accountStatusEnum = pgEnum("account_status", [
"active",
"suspended",
"deleted",
]);
// xmpp-account.ts
import { accountStatusEnum } from "../shared/account-status";
export const xmppAccount = pgTable("xmpp_accounts", {
// ...
status: accountStatusEnum("status").default("active").notNull(),
// ...
});
// integration-account.ts
import { accountStatusEnum } from "../shared/account-status";
export const integrationAccount = pgTable(
"integration_accounts",
{
// ...
status: accountStatusEnum("status").default("active").notNull(),
// ...
},
// ...
);This makes it clear that “status” means the same thing across all account tables and removes one mental branch.
- Make
integrationTypeexplicit with an enum (even if there’s only one for now)
Right now integrationType is a text, which encourages arbitrary strings and makes it harder to reason about what’s valid. A small enum constrained to the integrations you plan to support keeps the abstraction but makes it more concrete and self-documenting:
// shared/integration-type.ts
import { pgEnum } from "drizzle-orm/pg-core";
export const integrationTypeEnum = pgEnum("integration_type", [
"xmpp",
// add future integrations here: "slack", "discord", ...
]);
// integration-account.ts
import { integrationTypeEnum } from "../shared/integration-type";
export const integrationAccount = pgTable(
"integration_accounts",
{
// ...
integrationType: integrationTypeEnum("integration_type").notNull(),
// ...
},
(table) => [
index("integration_accounts_userId_idx").on(table.userId),
index("integration_accounts_type_idx").on(table.integrationType),
uniqueIndex("integration_accounts_userId_type_idx").on(
table.userId,
table.integrationType,
),
],
);This doesn’t change behavior but reduces “generic string” ambiguity and makes it clearer how this table will evolve as more integrations are added.
If you keep the table unused for now, consider at least adding a brief comment above it describing the planned migration path (e.g., “will back all integrations, including xmppAccount, once X/Y rollout is done”) so future readers don’t have to infer intent.
| ); | ||
| } | ||
|
|
||
| const status = |
There was a problem hiding this comment.
issue (complexity): Consider tightening the account typing and extracting the create and account-present branches into smaller components (optionally via a hook) to simplify IntegrationManagement’s logic and layout.
You can keep all functionality while reducing complexity by:
- Tightening the account type instead of runtime duck-typing
- Extracting small presentational components for the “no account” and “has account” branches
- Optionally extracting the data/mutation wiring into a hook
1. Use a more concrete account shape
You’re already assuming status is a string when present. Make that part of the type instead of checking at runtime:
interface IntegrationAccountBase {
id: string;
status?: string; // optional but typed
}
interface IntegrationManagementProps<TAccount extends IntegrationAccountBase> {
integrationId: string;
title: string;
description: string;
createLabel: string;
createInputLabel?: string;
createInputPlaceholder?: string;
createInputHelp?: string;
createInputToPayload?: (value: string) => Record<string, unknown>;
renderAccountDetails?: (account: TAccount) => ReactNode;
}
export function IntegrationManagement<TAccount extends IntegrationAccountBase>(props: IntegrationManagementProps<TAccount>) {
// ...
const status = account?.status ?? null;
// ...
}This removes the need for:
const status =
"status" in account && typeof account.status === "string"
? account.status
: null;and avoids mixing generics with runtime duck-typing.
2. Extract the “no account yet” create form
The if (!account) branch is large; pulling it out into a small child component cuts branching and makes the main component easier to scan:
interface NoAccountCardProps {
integrationId: string;
title: string;
description: string;
createLabel: string;
createInputLabel?: string;
createInputPlaceholder?: string;
createInputHelp?: string;
isCreating: boolean;
inputValue: string;
onInputChange: (value: string) => void;
onCreate: () => void;
}
function NoAccountCard({
integrationId,
title,
description,
createLabel,
createInputLabel,
createInputPlaceholder,
createInputHelp,
isCreating,
inputValue,
onInputChange,
onCreate,
}: NoAccountCardProps) {
return (
<Card>
<CardHeader>
<div className="font-semibold text-lg">{title}</div>
<p className="text-muted-foreground text-sm">{description}</p>
</CardHeader>
<CardContent className="space-y-4">
{createInputLabel ? (
<div className="space-y-2">
<Label htmlFor={`${integrationId}-identifier`}>{createInputLabel}</Label>
<Input
disabled={isCreating}
id={`${integrationId}-identifier`}
onChange={(event) => onInputChange(event.target.value)}
placeholder={createInputPlaceholder}
value={inputValue}
/>
{createInputHelp ? (
<p className="text-muted-foreground text-sm">{createInputHelp}</p>
) : null}
</div>
) : null}
</CardContent>
<CardFooter>
<Button className="w-full" disabled={isCreating} onClick={onCreate}>
{isCreating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating...
</>
) : (
createLabel
)}
</Button>
</CardFooter>
</Card>
);
}Then in IntegrationManagement:
if (!account) {
return (
<NoAccountCard
integrationId={integrationId}
title={title}
description={description}
createLabel={createLabel}
createInputLabel={createInputLabel}
createInputPlaceholder={createInputPlaceholder}
createInputHelp={createInputHelp}
isCreating={createMutation.isPending}
inputValue={inputValue}
onInputChange={setInputValue}
onCreate={handleCreate}
/>
);
}3. Extract the “account present” card
Likewise, the “has account” branch can be a focused presentational component:
interface AccountCardProps<TAccount extends IntegrationAccountBase> {
title: string;
description: string;
account: TAccount;
status?: string | null;
isDeleting: boolean;
renderAccountDetails?: (account: TAccount) => ReactNode;
onDelete: () => void;
}
function AccountCard<TAccount extends IntegrationAccountBase>({
title,
description,
account,
status,
isDeleting,
renderAccountDetails,
onDelete,
}: AccountCardProps<TAccount>) {
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<div className="font-semibold text-lg">{title}</div>
<p className="text-muted-foreground text-sm">{description}</p>
</div>
{status ? (
<Badge variant={status === "deleted" ? "destructive" : "default"}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</Badge>
) : null}
</div>
</CardHeader>
<CardContent className="space-y-4">
{renderAccountDetails ? renderAccountDetails(account) : null}
</CardContent>
<CardFooter className="flex justify-end">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button disabled={isDeleting} variant="destructive">
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Account
</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {title} Account?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your {title} account. This action cannot be undone. You will need to create a new account if you want to use {title} again.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={onDelete}
>
Delete Account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardFooter>
</Card>
);
}Usage in IntegrationManagement:
return (
<AccountCard
title={title}
description={description}
account={account}
status={status}
isDeleting={deleteMutation.isPending}
renderAccountDetails={renderAccountDetails}
onDelete={handleDelete}
/>
);4. Optional: move data/mutation wiring to a hook
If you want to further separate concerns, you can wrap the query/mutations + handlers into a small hook:
function useIntegrationManagement<TAccount extends IntegrationAccountBase>(
integrationId: string,
title: string,
createInputToPayload?: (value: string) => Record<string, unknown>
) {
const { data: account, isLoading, error } = useIntegrationAccount<TAccount>(integrationId);
const createMutation = useCreateIntegrationAccount<TAccount>(integrationId);
const deleteMutation = useDeleteIntegrationAccount(integrationId);
const [inputValue, setInputValue] = useState("");
const handleCreate = async () => { /* existing logic */ };
const handleDelete = async () => { /* existing logic */ };
return {
account,
isLoading,
error,
createMutation,
deleteMutation,
inputValue,
setInputValue,
handleCreate,
handleDelete,
};
}Then IntegrationManagement becomes mostly composition of NoAccountCard / AccountCard, improving readability and testability without changing behavior.
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughGeneralizes XMPP into a pluggable Integrations framework: adds integration_accounts DB schema, core integration types/registry/base, XMPP integration implementation, server APIs for integrations and accounts, client hooks and UI components, route/config updates, and user-deletion cleanup to remove integration accounts. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Client
participant UI as Integrations UI
participant API as Integrations API
participant Registry as Integration Registry
participant Integration as XmppIntegration
participant DB as Database
participant Prosody as Prosody Server
Browser->>UI: Open /app/integrations
UI->>API: GET /api/integrations
API->>Registry: getPublicInfo()
Registry-->>API: [IntegrationPublicInfo]
API-->>UI: 200 { integrations }
Browser->>UI: Create XMPP account (input)
UI->>API: POST /api/integrations/xmpp/accounts
API->>Registry: get("xmpp")
Registry-->>API: XmppIntegration
API->>Integration: createAccount(userId, input)
Integration->>DB: check existing account / insert record
Integration->>Prosody: createProsodyAccount(...)
Prosody-->>Integration: success
Integration-->>API: created account
API-->>UI: 201 { account }
UI-->>Browser: render account
sequenceDiagram
participant Admin as Admin Client
participant Auth as Auth Service
participant Cleanup as Integration Cleanup
participant Registry as Integration Registry
participant Integration as Integration
participant DB as Database
Admin->>Auth: DELETE /api/admin/users/{id}
Auth->>Cleanup: cleanupIntegrationAccounts(userId)
Cleanup->>Registry: getEnabled()
Registry-->>Cleanup: [integrations]
loop per integration
Cleanup->>Integration: getAccount(userId)
Integration->>DB: fetch account
alt account exists
Cleanup->>Integration: deleteAccount(accountId)
Integration->>DB: update status -> "deleted"
end
end
Cleanup-->>Auth: done
Auth->>DB: delete user record
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/app/`(dashboard)/app/integrations/integrations-content.tsx:
- Around line 63-95: Wrap the copy routine in a Sentry span and add an
aria-label to the icon-only Button: start a Sentry span (e.g., name
"copy_jid_button_click") when the onClick handler begins, finish the span after
the clipboard operation completes (both success and fallback), and capture any
thrown error with Sentry.captureException inside the catch block (in addition to
the existing toast.error) so failures are reported; also add an aria-label prop
to the Button (e.g., aria-label="Copy JID") to make the Copy icon accessible.
In `@src/app/api/integrations/`[integration]/accounts/[id]/route.ts:
- Around line 134-154: The handler verifies integration.getAccountById but never
checks that integration.deleteAccount exists before calling it; update the route
to verify the delete capability (e.g., check integration.deleteAccount) and
throw an APIError (or return 400) like the getAccountById check if the
integration does not support deletion, then call integration.deleteAccount(id)
only after that check and existing authorization/account checks; reference the
existing symbols getAccountById, deleteAccount, APIError, and the current
account and isAdmin(userId) checks when adding the capability guard.
- Around line 102-103: Validate the incoming PATCH body before calling
integration.updateAccount by parsing request.json() into a defined schema (e.g.,
check required fields/types and return a 400/422 on invalid input) and then
verify the integration supports updates the same way getAccountById checks
capabilities (e.g., check an integration capability flag or presence of an
update capability/method) before invoking integration.updateAccount(id, body);
if the integration does not support updates, return the appropriate 405/400
error response.
In `@src/app/api/integrations/`[integration]/accounts/route.ts:
- Around line 69-71: Replace the silent JSON swallow and add explicit Zod
validation at the route boundary: remove .catch(()=>({})) on request.json(),
define/import a Zod schema (e.g. AccountCreateSchema) for the expected request
body, parse and validate the JSON payload using AccountCreateSchema.parse or
safeParse and return a 400 error when validation fails, and then pass the
validated data to integration.createAccount(userId, validatedBody); ensure you
import zod and the schema at the top of the file and keep
integration.createAccount calls unchanged.
In `@src/lib/integrations/xmpp/config.ts`:
- Around line 39-44: The validateXmppConfig function currently runs lazily;
change startup flow so XMPP config is validated during backend initialization:
update the integration registration/startup code that decides whether XMPP is
enabled (e.g., the function or flag like isXmppEnabled(), enableXmpp, or similar
integration registration routine) to call validateXmppConfig() immediately when
XMPP is enabled, and ensure validateXmppConfig throws on missing/invalid env so
startup fails fast; do not rely on first-use calls—invoke validateXmppConfig
from the integration registration/initialization path so misconfigurations
surface at startup.
🧹 Nitpick comments (13)
src/lib/api/index.ts (1)
8-8: Consider importing integrations directly instead of expanding the API barrel.This re-export widens the barrel surface; if feasible, keep consumers importing from
@/lib/api/integrationsto preserve selective imports and tree‑shaking. As per coding guidelines, prefer selective imports and avoid barrel exports.src/lib/integrations/core/types.ts (1)
3-5: Consider deriving the array from the type to prevent drift.The union type and the array are defined separately, which could lead to inconsistencies if one is updated without the other.
♻️ Suggested refactor
-export type IntegrationStatus = "active" | "suspended" | "deleted"; - -export const integrationStatuses = ["active", "suspended", "deleted"] as const; +export const integrationStatuses = ["active", "suspended", "deleted"] as const; + +export type IntegrationStatus = (typeof integrationStatuses)[number];src/app/api/integrations/route.ts (1)
1-23: Add Sentry span instrumentation for the API handler to improve performance monitoring.The route handler would benefit from explicit span instrumentation with
Sentry.startSpan()to track this endpoint's execution. Useop: "http.server"with a descriptive name like"GET /api/integrations"and optionally attach attributes for request metrics.Note: Errors are already captured via
handleAPIError(), so this is an enhancement for observability and performance monitoring rather than error tracking.Proposed implementation
+import * as Sentry from "@sentry/nextjs"; import type { NextRequest } from "next/server"; import { handleAPIError, requireAuth } from "@/lib/api/utils"; import { registerIntegrations } from "@/lib/integrations"; import { getIntegrationRegistry } from "@/lib/integrations/core/registry"; export const dynamic = "force-dynamic"; /** * GET /api/integrations * List available integrations */ export async function GET(request: NextRequest) { - try { - await requireAuth(request); - - registerIntegrations(); - const integrations = getIntegrationRegistry().getPublicInfo(); - - return Response.json({ ok: true, integrations }); - } catch (error) { - return handleAPIError(error); - } + return Sentry.startSpan( + { name: "GET /api/integrations", op: "http.server" }, + async () => { + try { + await requireAuth(request); + registerIntegrations(); + const integrations = getIntegrationRegistry().getPublicInfo(); + return Response.json({ ok: true, integrations }); + } catch (error) { + return handleAPIError(error); + } + } + ); }src/components/integrations/integration-card.tsx (1)
18-28: Consider usingCardTitleandCardDescriptionfor semantic consistency.The component works correctly, but the Card component likely exports
CardTitleandCardDescriptionsub-components that provide semantic markup and consistent styling. Using these would align with shadcn/ui conventions.♻️ Optional: Use semantic Card sub-components
-import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; // ... return ( <Card> <CardHeader className="space-y-1"> - <div className="font-semibold text-lg">{title}</div> - {description ? ( - <p className="text-muted-foreground text-sm">{description}</p> - ) : null} + <CardTitle>{title}</CardTitle> + {description ? <CardDescription>{description}</CardDescription> : null} </CardHeader> <CardContent>{children}</CardContent> </Card> );src/lib/integrations/xmpp/client.ts (2)
3-64: Add a Sentry span around Prosody REST calls.Wrap the HTTP call in
Sentry.startSpanwith meaningfulopandname, and include request attributes.✅ Suggested update
-import { validateXmppConfig, xmppConfig } from "./config"; +import * as Sentry from "@sentry/nextjs"; +import { validateXmppConfig, xmppConfig } from "./config"; ... - const response = await fetch(url, { - ...options, - headers: { - "Content-Type": "application/xml", - Authorization: createAuthHeader(), - ...options.headers, - }, - }); + const response = await Sentry.startSpan( + { + op: "http.client", + name: `Prosody REST ${options.method ?? "GET"} ${endpoint}`, + attributes: { endpoint, url }, + }, + () => + fetch(url, { + ...options, + headers: { + "Content-Type": "application/xml", + Authorization: createAuthHeader(), + ...options.headers, + }, + }) + );
114-137: Capture Prosody errors in Sentry before rethrowing.Add
Sentry.captureExceptionfor unexpected errors (skip expected cases like “not found” / “conflict”).✅ Example (apply similarly to the other catch blocks)
} catch (error) { if (error instanceof Error) { // Check if account already exists if ( error.message.includes("exists") || error.message.includes("409") || error.message.includes("conflict") ) { throw new Error(`XMPP account already exists: ${jid}`); } + Sentry.captureException(error); throw error; } + Sentry.captureException(error); throw new Error("Failed to create Prosody account"); }Also applies to: 162-186, 199-213
src/components/integrations/integration-management.tsx (2)
67-87: Missing Sentry error capture in catch block.Per coding guidelines, exceptions should be captured with
Sentry.captureException(error)in catch blocks. The error is shown to the user via toast but not reported to Sentry for monitoring.🔧 Proposed fix
+"use client"; + +import * as Sentry from "@sentry/nextjs"; import type { ReactNode } from "react"; // ... other imports const handleCreate = async () => { try { const trimmed = inputValue.trim(); const payload = createInputToPayload?.(trimmed) ?? (trimmed ? { identifier: trimmed } : {}); await createMutation.mutateAsync(payload); toast.success(`${title} account created`, { description: `Your ${title} account has been created successfully.`, }); setInputValue(""); - } catch (error) { + } catch (err) { + Sentry.captureException(err); toast.error(`Failed to create ${title.toLowerCase()} account`, { description: - error instanceof Error - ? error.message + err instanceof Error + ? err.message : "An error occurred while creating your account.", }); } };
89-107: Missing Sentry error capture in delete handler.Same issue as
handleCreate- the error should be captured withSentry.captureException(error)for observability.🔧 Proposed fix
const handleDelete = async () => { if (!account) { return; } try { await deleteMutation.mutateAsync(account.id); toast.success(`${title} account deleted`, { description: `Your ${title} account has been deleted successfully.`, }); - } catch (error) { + } catch (err) { + Sentry.captureException(err); toast.error(`Failed to delete ${title.toLowerCase()} account`, { description: - error instanceof Error - ? error.message + err instanceof Error + ? err.message : "An error occurred while deleting your account.", }); } };src/lib/api/query-keys.ts (1)
70-94: Query key factory follows established patterns with minor inconsistency.The new
integrationsquery keys follow the factory pattern. However,integrations.list()doesn't accept optional filters unlike otherlist()methods (e.g.,users.list(filters?),sessions.list(filters?)). Consider adding optional filters for consistency if filtering will be needed in the future.♻️ Optional: Add filters parameter for consistency
integrations: { all: ["integrations"] as const, lists: () => [...queryKeys.integrations.all, "list"] as const, - list: () => [...queryKeys.integrations.lists()] as const, + list: (filters?: { enabled?: boolean }) => + [...queryKeys.integrations.lists(), { filters }] as const,src/app/api/integrations/[integration]/accounts/[id]/route.ts (1)
14-57: Consider extracting shared validation logic.The authorization and integration lookup logic (register → get integration → check enabled → check capability → get account → check ownership) is duplicated across GET, PATCH, and DELETE handlers. Consider extracting a helper function.
♻️ Example helper extraction
async function getAuthorizedAccount( request: NextRequest, integrationId: string, accountId: string ) { const { userId } = await requireAuth(request); registerIntegrations(); const integration = getIntegrationRegistry().get(integrationId); if (!integration) { throw new APIError("Unknown integration", 404); } if (!integration.enabled) { throw new APIError("Integration is disabled", 403); } if (!integration.getAccountById) { throw new APIError("Integration does not support account lookup", 400); } const account = await integration.getAccountById(accountId); if (!account) { throw new APIError("Integration account not found", 404); } const isAdminUser = await isAdmin(userId); if (account.userId !== userId && !isAdminUser) { throw new APIError("Forbidden - Access denied", 403); } return { userId, integration, account, isAdminUser }; }src/lib/integrations/xmpp/implementation.ts (1)
262-274: Consider validating status values.The
updateAccountmethod acceptsinput.statuswithout validating it's a valid status value. If there's a defined set of allowed statuses (e.g., "active", "suspended", "deleted"), consider validating before applying the update.♻️ Optional: Add status validation
+const VALID_STATUSES = ["active", "suspended"] as const; + const updates: Partial<typeof xmppAccount.$inferInsert> = {}; if (input.status && input.status !== account.status) { + if (!VALID_STATUSES.includes(input.status as typeof VALID_STATUSES[number])) { + throw new Error(`Invalid status. Allowed values: ${VALID_STATUSES.join(", ")}`); + } updates.status = input.status; }src/hooks/use-integration.ts (2)
53-68: Consider invalidating theallaccounts key for broader cache consistency.When creating an account, invalidating
accounts.currentis correct for the current user's view. However, if any list views useaccounts.all, they won't refresh automatically.♻️ Optional: Broader cache invalidation
onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.integrations.accounts.current(integrationId), }); + // Also invalidate the parent key to refresh any list views + queryClient.invalidateQueries({ + queryKey: queryKeys.integrations.accounts.all(integrationId), + }); },
96-110: Consider clearing the detail cache entry for the deleted account.When deleting an account, invalidating
accounts.allis good, but the specific detail entry remains cached until it naturally expires. This could cause stale data issues if the ID is reused or queried directly.♻️ Optional: Remove deleted account from cache
-export function useDeleteIntegrationAccount(integrationId: string) { +export function useDeleteIntegrationAccount(integrationId: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => deleteIntegrationAccount(integrationId, id), - onSuccess: () => { + onSuccess: (_data, id) => { + // Remove the specific detail entry + queryClient.removeQueries({ + queryKey: queryKeys.integrations.accounts.detail(integrationId, id), + }); queryClient.invalidateQueries({ queryKey: queryKeys.integrations.accounts.all(integrationId), }); }, }); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (40)
drizzle/20260107000000_integration_accounts/migration.sqllocale/en/routes.jsonnext.config.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/app/(dashboard)/app/integrations/page.tsxsrc/app/api/admin/users/[id]/route.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/integrations/route.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/app/api/xmpp/accounts/route.tssrc/components/integrations/integration-card.tsxsrc/components/integrations/integration-management.tsxsrc/components/xmpp/xmpp-account-management.tsxsrc/env.tssrc/hooks/index.tssrc/hooks/use-integration.tssrc/hooks/use-xmpp-account.tssrc/lib/api/index.tssrc/lib/api/integrations.tssrc/lib/api/query-keys.tssrc/lib/api/xmpp.tssrc/lib/auth/config.tssrc/lib/db/schema/index.tssrc/lib/db/schema/integrations/base.tssrc/lib/integrations/core/base.tssrc/lib/integrations/core/constants.tssrc/lib/integrations/core/factory.tssrc/lib/integrations/core/registry.tssrc/lib/integrations/core/types.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/index.tssrc/lib/integrations/xmpp/client.tssrc/lib/integrations/xmpp/config.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/integrations/xmpp/index.tssrc/lib/integrations/xmpp/keys.tssrc/lib/integrations/xmpp/types.tssrc/lib/integrations/xmpp/utils.tssrc/lib/routes/config.ts
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)
**/*.{js,jsx,ts,tsx}: UseSentry.captureException(error)to capture exceptions and log errors in Sentry, particularly in try-catch blocks or areas where exceptions are expected
UseSentry.startSpan()function to create spans for meaningful actions within applications like button clicks, API calls, and function calls
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/components/xmpp/xmpp-account-management.tsxsrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/lib/routes/config.tssrc/app/(dashboard)/app/integrations/page.tsxsrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/components/integrations/integration-management.tsxsrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{js,ts}
📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)
When creating custom spans for API calls with
Sentry.startSpan(), ensure thenameandopproperties are meaningful (e.g.,op: "http.client"with descriptive names likeGET /api/users/${userId}), and attach attributes based on relevant request information and metrics
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/lib/routes/config.tssrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)
**/*.{js,ts,jsx,tsx}: Import Sentry usingimport * as Sentry from "@sentry/nextjs"when using logs in NextJS projects
Reference the Sentry logger usingconst { logger } = Sentrywhen using logging functionality
Uselogger.fmtas a template literal function to bring variables into structured logs, providing better log formatting and variable interpolation
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/components/xmpp/xmpp-account-management.tsxsrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/lib/routes/config.tssrc/app/(dashboard)/app/integrations/page.tsxsrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/components/integrations/integration-management.tsxsrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{css,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/tailwind-v4.mdc)
Use container query support with
@container,@sm:,@md:for container-based breakpoints and@max-md:for max-width queries
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/components/xmpp/xmpp-account-management.tsxsrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/lib/routes/config.tssrc/app/(dashboard)/app/integrations/page.tsxsrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/components/integrations/integration-management.tsxsrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{ts,tsx,js,jsx,html}
📄 CodeRabbit inference engine (.cursor/rules/tailwind-v4.mdc)
**/*.{ts,tsx,js,jsx,html}: Use 3D transform utilities:transform-3d,rotate-x-*,rotate-y-*,rotate-z-*,scale-z-*,translate-z-*,perspective-*, andperspective-origin-*
Use linear gradient angles withbg-linear-45syntax and gradient interpolation likebg-linear-to-r/oklchorbg-linear-to-r/srgb
Use conic and radial gradients withbg-conicandbg-radial-[at_25%_25%]utilities
Useinset-shadow-*andinset-ring-*utilities instead of deprecated shadow opacity utilities
Usefield-sizing-contentutility for auto-resizing textareas
Usescheme-lightandscheme-darkutilities forcolor-schemeproperty
Usefont-stretch-*utilities for variable font configuration
Chain variants together for composable variants (e.g.,group-has-data-potato:opacity-100)
Usestartingvariant for@starting-styletransitions
Usenot-*variant for:not()pseudo-class (e.g.,not-first:mb-4)
Useinertvariant for styling elements with theinertattribute
Usenth-*variants:nth-3:,nth-last-5:,nth-of-type-4:,nth-last-of-type-6:for targeting specific elements
Usein-*variant as a simpler alternative togroup-*without addinggroupclass
Useopenvariant to support:popover-openpseudo-class
Use**variant for targeting all descendants
Replace deprecatedbg-opacity-*utilities with color values using slash notation (e.g.,bg-black/50)
Replace deprecatedtext-opacity-*utilities with color values using slash notation (e.g.,text-black/50)
Replace deprecatedborder-opacity-*,divide-opacity-*and similar opacity utilities with color slash notation
Useshadow-xsinstead ofshadow-smandshadow-sminstead ofshadow
Usedrop-shadow-xsinstead ofdrop-shadow-smanddrop-shadow-sminstead ofdrop-shadow
Useblur-xsinstead ofblur-smandblur-sminstead ofblur
Userounded-xsinstead ofrounded-smandrounded-sminstead ofrounded
Useoutline-hiddeninstead ofoutline-nonefor...
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/components/xmpp/xmpp-account-management.tsxsrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/lib/routes/config.tssrc/app/(dashboard)/app/integrations/page.tsxsrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/components/integrations/integration-management.tsxsrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/tanstack-query.mdc)
**/*.{ts,tsx}: Create QueryClient using getQueryClient() function that automatically handles server/client isolation (new instance per server request, singleton on client)
Always use the centralized query key factory from src/lib/api/query-keys.ts instead of creating keys manually
Include all variables used in queryFn in the query key to ensure proper cache management and query invalidation
**/*.{ts,tsx}: Use explicit types for function parameters and return values when they enhance clarity
Preferunknownoveranywhen the type is genuinely unknown
Use const assertions (as const) for immutable values and literal types
Leverage TypeScript's type narrowing instead of type assertions
**/*.{ts,tsx}: TypeScript strict mode enabled
Prefer composition over inheritance
Use selective imports over barrel exports for performance
Write accessible, performant, type-safe code
Use explicit types for clarity, preferunknownoverany
Always await promises in async functions
Use@/authmodule for all authentication operations
Use@/dbfor all database operations
Use BetterAuth for all authentication
Validate all inputs with Zod schemas
Implement proper RBAC with permissions module
Use TypeScript paths for imports (configured in tsconfig.json)
Use TanStack Query for all server state management
Clear separation between client/server code following module boundaries
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/components/xmpp/xmpp-account-management.tsxsrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/lib/routes/config.tssrc/app/(dashboard)/app/integrations/page.tsxsrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/components/integrations/integration-management.tsxsrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{ts,tsx,js,jsx}: Use meaningful variable names instead of magic numbers - extract constants with descriptive names
Use arrow functions for callbacks and short functions
Preferfor...ofloops over.forEach()and indexedforloops
Use optional chaining (?.) and nullish coalescing (??) for safer property access
Prefer template literals over string concatenation
Use destructuring for object and array assignments
Useconstby default,letonly when reassignment is needed, nevervar
Alwaysawaitpromises in async functions - don't forget to use the return value
Useasync/awaitsyntax instead of promise chains for better readability
Handle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Use function components over class components
Call hooks at the top level only, never conditionally
Specify all dependencies in hook dependency arrays correctly
Use thekeyprop for elements in iterables (prefer unique IDs over array indices)
Nest children between opening and closing tags instead of passing as props in React components
Don't define components inside other components
Use semantic HTML and ARIA attributes for accessibility: provide meaningful alt text for images, use proper heading hierarchy, add labels for form inputs, include keyboard event handlers alongside mouse events, and use semantic elements instead of divs with roles
Removeconsole.log,debugger, andalertstatements from production code
ThrowErrorobjects with descriptive messages, not strings or other values
Usetry-catchblocks meaningfully - don't catch errors just to rethrow them
Prefer early returns over nested conditionals for error cases
Keep functions focused and under reasonable cognitive complexity limits
Extract complex conditions into well-named boolean variables
Use early returns to reduce nesting
Prefer simple conditionals over nested ternary operators
Group related code together and separate concerns
Add `...
Files:
next.config.tssrc/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/components/xmpp/xmpp-account-management.tsxsrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/lib/routes/config.tssrc/app/(dashboard)/app/integrations/page.tsxsrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/hooks/use-integration.tssrc/lib/integrations/core/constants.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/lib/integrations/core/registry.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.tssrc/components/integrations/integration-management.tsxsrc/hooks/use-xmpp-account.tssrc/env.tssrc/lib/integrations/xmpp/client.tssrc/app/api/integrations/route.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/lib/integrations/xmpp/config.tssrc/app/api/xmpp/accounts/route.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
src/lib/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Modules should import and use their own
keys()function, not directprocess.envaccess
Files:
src/lib/integrations/core/user-deletion.tssrc/lib/integrations/core/factory.tssrc/lib/api/index.tssrc/lib/integrations/xmpp/types.tssrc/lib/routes/config.tssrc/lib/integrations/core/base.tssrc/lib/db/schema/integrations/base.tssrc/lib/integrations/core/constants.tssrc/lib/integrations/core/registry.tssrc/lib/integrations/xmpp/index.tssrc/lib/integrations/xmpp/client.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/integrations/core/types.tssrc/lib/integrations/xmpp/implementation.tssrc/lib/auth/config.tssrc/lib/integrations/index.tssrc/lib/integrations/xmpp/config.tssrc/lib/api/xmpp.tssrc/lib/api/integrations.ts
**/*.{jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/sentry.mdc)
When creating custom spans for component actions with
Sentry.startSpan(), ensure thenameandopproperties are meaningful for the activities in the call, and attach attributes based on relevant information and metrics
Files:
src/components/xmpp/xmpp-account-management.tsxsrc/components/integrations/integration-card.tsxsrc/app/(dashboard)/app/integrations/page.tsxsrc/components/integrations/integration-management.tsxsrc/app/(dashboard)/app/integrations/integrations-content.tsx
{src/app/**,src/components/**}/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/tanstack-query.mdc)
{src/app/**,src/components/**}/*.{ts,tsx}: Use useUsers(), useUser(), useSessions(), useAdminStats(), useUpdateUser(), useDeleteUser(), useDeleteSession() hooks from src/hooks/use-admin.ts for standard queries
Use corresponding Suspense hooks (useUsersSuspense(), useUserSuspense(), etc.) from src/hooks/use-admin-suspense.ts when wrapping components in Suspense and Error Boundaries
Files:
src/components/xmpp/xmpp-account-management.tsxsrc/app/api/integrations/[integration]/accounts/route.tssrc/app/api/admin/users/[id]/route.tssrc/components/integrations/integration-card.tsxsrc/app/(dashboard)/app/integrations/page.tsxsrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/app/api/xmpp/accounts/[id]/route.tssrc/components/integrations/integration-management.tsxsrc/app/api/integrations/route.tssrc/app/(dashboard)/app/integrations/integrations-content.tsxsrc/app/api/xmpp/accounts/route.ts
**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
**/*.tsx: Use functional components and hooks
Use semantic HTML and ARIA attributes
Use shadcn/ui components from@/components/ui/*
Implement proper error boundaries
Use Suspense for loading states
Optimize images with next/image
Files:
src/components/xmpp/xmpp-account-management.tsxsrc/components/integrations/integration-card.tsxsrc/app/(dashboard)/app/integrations/page.tsxsrc/components/integrations/integration-management.tsxsrc/app/(dashboard)/app/integrations/integrations-content.tsx
locale/**/*
📄 CodeRabbit inference engine (AGENTS.md)
Internationalization handled via next-intl with locale files in
locale/directory
Files:
locale/en/routes.json
src/lib/api/query-keys.ts
📄 CodeRabbit inference engine (.cursor/rules/tanstack-query.mdc)
src/lib/api/query-keys.ts: Query keys must maintain consistent array order (array order matters) and use serializable values only (no functions or non-serializable objects)
All query keys must follow the factory pattern with structure: resource.all, resource.lists(), resource.list(filters?), resource.detail(id), resource.details()
Files:
src/lib/api/query-keys.ts
🧠 Learnings (38)
📚 Learning: 2026-01-15T06:15:18.973Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/sentry.mdc:0-0
Timestamp: 2026-01-15T06:15:18.973Z
Learning: Applies to {instrumentation-client.{js,ts},sentry.server.config.{js,ts},sentry.edge.config.{js,ts}} : In NextJS projects, perform client-side Sentry initialization in `instrumentation-client.(js|ts)`, server initialization in `sentry.server.config.ts`, and edge initialization in `sentry.edge.config.ts` - do not repeat initialization in other files
Applied to files:
next.config.ts
📚 Learning: 2026-01-15T06:15:18.973Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/sentry.mdc:0-0
Timestamp: 2026-01-15T06:15:18.973Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Import Sentry using `import * as Sentry from "sentry/nextjs"` when using logs in NextJS projects
Applied to files:
next.config.ts
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use Server Components by default; `use client` only when essential in frontend
Applied to files:
next.config.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{ts,tsx} : Use useUsers(), useUser(), useSessions(), useAdminStats(), useUpdateUser(), useDeleteUser(), useDeleteSession() hooks from src/hooks/use-admin.ts for standard queries
Applied to files:
src/components/xmpp/xmpp-account-management.tsxsrc/app/api/admin/users/[id]/route.tssrc/hooks/use-integration.tssrc/hooks/index.tssrc/hooks/use-xmpp-account.tssrc/lib/api/query-keys.tssrc/lib/db/schema/index.tssrc/lib/auth/config.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use selective imports over barrel exports for performance
Applied to files:
src/lib/api/index.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Avoid barrel files (index files that re-export everything)
Applied to files:
src/lib/api/index.tssrc/hooks/index.tssrc/lib/integrations/xmpp/index.ts
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/*route*.{ts,tsx,js,jsx} : Clean, non-duplicated REST routes in backend code
Applied to files:
src/app/api/admin/users/[id]/route.tssrc/lib/routes/config.tssrc/app/api/integrations/[integration]/accounts/[id]/route.tssrc/app/api/integrations/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/admin.ts : All API query functions must throw errors (not return undefined) and check response.ok for fetch() calls with proper error handling
Applied to files:
src/app/api/admin/users/[id]/route.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.tsx : Use functional components and hooks
Applied to files:
src/components/integrations/integration-card.tsxsrc/hooks/index.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Nest children between opening and closing tags instead of passing as props in React components
Applied to files:
src/components/integrations/integration-card.tsx
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to app/**/*.{ts,tsx} : Follow Next.js App Router conventions
Applied to files:
src/lib/routes/config.tssrc/app/api/integrations/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : Use HydrationBoundary component from TanStack Query to transfer server-prefetched data to client cache in Server Components
Applied to files:
src/app/(dashboard)/app/integrations/page.tsxsrc/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : In Server Components, create a QueryClient per request using getServerQueryClient(), prefetch data in parallel with Promise.all(), and dehydrate using dehydrate(queryClient)
Applied to files:
src/app/(dashboard)/app/integrations/page.tsxsrc/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{tsx} : Use useSuspenseQueries for parallel queries to avoid waterfalls and improve performance in Server Components and Client Components
Applied to files:
src/app/(dashboard)/app/integrations/page.tsxsrc/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : Perform auth checks using Better Auth's auth.api.getSession() in Server Components before prefetching data, and redirect if not authenticated
Applied to files:
src/app/(dashboard)/app/integrations/page.tsx
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/api/**/*.{ts,tsx,js,jsx} : API, DB, auth contracts, or tokens require mandatory review
Applied to files:
src/app/api/integrations/[integration]/accounts/[id]/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/hooks/use-admin.ts : Use queryClient.setQueryData() to update specific cache entries and queryClient.invalidateQueries() to invalidate related queries in mutation onSuccess handlers
Applied to files:
src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{tsx} : In regular query hooks, check isPending first, then isError, then use data to ensure TypeScript type narrowing works correctly
Applied to files:
src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to **/*.{ts,tsx} : Create QueryClient using getQueryClient() function that automatically handles server/client isolation (new instance per server request, singleton on client)
Applied to files:
src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Add new resources by creating query keys, types, client API functions, server query functions, regular hooks, and Suspense hooks following the established pattern
Applied to files:
src/hooks/use-integration.tssrc/lib/api/query-keys.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use TanStack Query for all server state management
Applied to files:
src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/**/*.{tsx} : Use fetchQuery for critical queries that must succeed (throws errors for 404/500), and prefetchQuery for optional data that can gracefully degrade
Applied to files:
src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/app/providers.tsx : Include ReactQueryDevtools in src/app/providers.tsx with NODE_ENV === 'development' check for development-only access
Applied to files:
src/hooks/use-integration.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to {src/app/**,src/components/**}/*.{ts,tsx} : Use corresponding Suspense hooks (useUsersSuspense(), useUserSuspense(), etc.) from src/hooks/use-admin-suspense.ts when wrapping components in Suspense and Error Boundaries
Applied to files:
src/hooks/index.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use `@/auth` module for all authentication operations
Applied to files:
src/hooks/index.tssrc/hooks/use-xmpp-account.tssrc/lib/db/schema/index.tssrc/app/api/xmpp/accounts/route.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript paths for imports (configured in tsconfig.json)
Applied to files:
src/hooks/use-xmpp-account.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to src/lib/**/*.ts : Modules should import and use their own `keys()` function, not direct `process.env` access
Applied to files:
src/env.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to **/*.{ts,tsx} : Always use the centralized query key factory from src/lib/api/query-keys.ts instead of creating keys manually
Applied to files:
src/env.tssrc/lib/api/query-keys.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to src/lib/*/keys.ts : Each lib module should export a `keys()` function that defines and validates its environment variables using Zod schemas
Applied to files:
src/env.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to app/**/*.tsx : Never expose API keys in client code
Applied to files:
src/env.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use `next/head` or App Router metadata API for head elements
Applied to files:
src/app/api/integrations/route.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/query-keys.ts : All query keys must follow the factory pattern with structure: resource.all, resource.lists(), resource.list(filters?), resource.detail(id), resource.details()
Applied to files:
src/lib/api/query-keys.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/query-keys.ts : Query keys must maintain consistent array order (array order matters) and use serializable values only (no functions or non-serializable objects)
Applied to files:
src/lib/api/query-keys.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Use BetterAuth for all authentication
Applied to files:
src/lib/db/schema/index.ts
📚 Learning: 2026-01-15T06:16:09.034Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/tanstack-query.mdc:0-0
Timestamp: 2026-01-15T06:16:09.034Z
Learning: Applies to src/lib/api/types.ts : TypeScript types for resources must be derived from database schema and include type interfaces for list filters (e.g., UserListFilters, PostListFilters)
Applied to files:
src/lib/integrations/core/types.ts
📚 Learning: 2026-01-15T06:16:30.014Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2026-01-15T06:16:30.014Z
Learning: Applies to **/*.{ts,tsx} : Use explicit types for function parameters and return values when they enhance clarity
Applied to files:
src/lib/integrations/core/types.ts
📚 Learning: 2025-12-18T18:18:05.202Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: .cursor/rules/anti-slop.mdc:0-0
Timestamp: 2025-12-18T18:18:05.202Z
Learning: Applies to **/config*.{ts,tsx,js,jsx} : ENV must be validated at startup in backend
Applied to files:
src/lib/integrations/xmpp/config.ts
📚 Learning: 2026-01-16T06:31:42.976Z
Learnt from: CR
Repo: allthingslinux/portal PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T06:31:42.976Z
Learning: Applies to **/*.{ts,tsx} : Clear separation between client/server code following module boundaries
Applied to files:
src/app/api/xmpp/accounts/route.ts
🧬 Code graph analysis (16)
src/lib/integrations/core/user-deletion.ts (3)
src/lib/integrations/index.ts (1)
registerIntegrations(10-17)src/lib/integrations/core/registry.ts (1)
getIntegrationRegistry(66-68)src/lib/db/schema/auth.ts (1)
account(48-70)
src/app/api/integrations/[integration]/accounts/route.ts (4)
src/app/api/integrations/route.ts (1)
dynamic(7-7)src/lib/api/utils.ts (3)
requireAuth(68-81)APIError(86-96)handleAPIError(102-124)src/lib/integrations/index.ts (1)
registerIntegrations(10-17)src/lib/integrations/core/registry.ts (1)
getIntegrationRegistry(66-68)
src/app/api/admin/users/[id]/route.ts (1)
src/lib/integrations/core/user-deletion.ts (1)
cleanupIntegrationAccounts(11-36)
src/components/integrations/integration-card.tsx (1)
src/components/ui/card.tsx (3)
Card(85-85)CardHeader(86-86)CardContent(91-91)
src/lib/routes/config.ts (1)
src/lib/integrations/core/types.ts (1)
IntegrationPublicInfo(17-22)
src/app/(dashboard)/app/integrations/page.tsx (2)
src/lib/api/hydration.ts (1)
getServerQueryClient(21-23)src/app/(dashboard)/app/integrations/integrations-content.tsx (1)
IntegrationsContent(17-150)
src/lib/integrations/core/base.ts (1)
src/lib/integrations/core/types.ts (4)
IntegrationAccount(7-15)IntegrationCreateInput(24-24)IntegrationUpdateInput(25-25)Integration(36-85)
src/app/api/integrations/[integration]/accounts/[id]/route.ts (5)
src/app/api/integrations/route.ts (2)
dynamic(7-7)GET(13-24)src/lib/api/utils.ts (3)
requireAuth(68-81)APIError(86-96)handleAPIError(102-124)src/lib/integrations/index.ts (1)
registerIntegrations(10-17)src/lib/integrations/core/registry.ts (1)
getIntegrationRegistry(66-68)src/lib/auth/check-role.ts (1)
isAdmin(46-49)
src/hooks/use-integration.ts (2)
src/lib/api/query-keys.ts (1)
queryKeys(13-103)src/lib/api/integrations.ts (6)
fetchIntegrations(17-31)fetchIntegrationAccount(36-56)fetchIntegrationAccountById(61-84)createIntegrationAccount(89-116)updateIntegrationAccount(121-152)deleteIntegrationAccount(157-177)
src/lib/integrations/core/constants.ts (1)
src/lib/integrations/core/types.ts (1)
IntegrationStatus(3-3)
src/lib/integrations/xmpp/client.ts (1)
src/lib/integrations/xmpp/config.ts (1)
validateXmppConfig(44-84)
src/app/api/integrations/route.ts (3)
src/lib/api/utils.ts (2)
requireAuth(68-81)handleAPIError(102-124)src/lib/integrations/index.ts (1)
registerIntegrations(10-17)src/lib/integrations/core/registry.ts (1)
getIntegrationRegistry(66-68)
src/lib/auth/config.ts (1)
src/lib/integrations/core/user-deletion.ts (1)
cleanupIntegrationAccounts(11-36)
src/app/(dashboard)/app/integrations/integrations-content.tsx (3)
src/hooks/use-integration.ts (1)
useIntegrations(18-24)src/components/integrations/integration-management.tsx (1)
IntegrationManagement(47-243)src/lib/integrations/xmpp/types.ts (1)
XmppAccount(14-24)
src/lib/api/xmpp.ts (2)
src/lib/integrations/xmpp/types.ts (3)
XmppAccount(14-24)CreateXmppAccountRequest(29-31)UpdateXmppAccountRequest(36-40)src/lib/api/integrations.ts (5)
fetchIntegrationAccount(36-56)fetchIntegrationAccountById(61-84)createIntegrationAccount(89-116)updateIntegrationAccount(121-152)deleteIntegrationAccount(157-177)
src/lib/api/integrations.ts (1)
src/lib/integrations/core/types.ts (1)
IntegrationPublicInfo(17-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Seer Code Review
- GitHub Check: Sourcery review
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| /** | ||
| * Validate XMPP configuration | ||
| * Called at module load time to catch configuration errors early | ||
| * Validate XMPP configuration lazily | ||
| * Only validates when actually needed (when XMPP operations are attempted) | ||
| * This prevents blocking the entire application if XMPP is not configured | ||
| */ | ||
| function validateXmppConfig(): void { | ||
| export function validateXmppConfig(): void { |
There was a problem hiding this comment.
Startup config validation is required for backend config modules.
Lazy validation means misconfigurations surface only at first use. Please ensure XMPP env validation runs during backend startup (for example in integration registration when XMPP is enabled), not only at runtime. Based on learnings, please validate env at startup in backend.
🤖 Prompt for AI Agents
In `@src/lib/integrations/xmpp/config.ts` around lines 39 - 44, The
validateXmppConfig function currently runs lazily; change startup flow so XMPP
config is validated during backend initialization: update the integration
registration/startup code that decides whether XMPP is enabled (e.g., the
function or flag like isXmppEnabled(), enableXmpp, or similar integration
registration routine) to call validateXmppConfig() immediately when XMPP is
enabled, and ensure validateXmppConfig throws on missing/invalid env so startup
fails fast; do not rely on first-use calls—invoke validateXmppConfig from the
integration registration/initialization path so misconfigurations surface at
startup.
- Added Sentry error capturing to improve error tracking during account operations. - Updated the JID copy functionality to include performance tracing with spans. - Introduced integration status labels for better user feedback on account statuses. - Enhanced request body validation in API routes to ensure proper error handling for invalid inputs.
- Deleted the XMPP account management routes, including creation, retrieval, update, and deletion functionalities. - This cleanup simplifies the codebase and aligns with recent refactoring efforts towards integration management.
- Introduced comprehensive documentation for the new integrations framework, detailing architecture, core components, database schema, type system, API routes, client-side API, and UI components. - This documentation serves as a guide for developers to implement and manage various integrations effectively, enhancing overall understanding and usability of the framework. - Removed the XMPP account management component and related hooks to streamline the codebase in favor of the new unified integrations approach.
Summary by Sourcery
Introduce a generic integrations framework with registry, API routes, database schema, and UI, and migrate the existing XMPP account management onto this integrations system.
New Features:
Enhancements:
Build:
Deployment:
Chores: