@carlonicora/nextjs-jsonapi 1.24.3 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/{BlockNoteEditor-OFSTXGZX.js → BlockNoteEditor-7WAWEZVW.js} +13 -13
  2. package/dist/{BlockNoteEditor-OFSTXGZX.js.map → BlockNoteEditor-7WAWEZVW.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-TJNLCNIP.mjs → BlockNoteEditor-UNVKGZ2G.mjs} +3 -3
  4. package/dist/billing/index.js +342 -342
  5. package/dist/billing/index.mjs +2 -2
  6. package/dist/{chunk-H5JZ5E7M.mjs → chunk-6BDOZDZ3.mjs} +1247 -54
  7. package/dist/chunk-6BDOZDZ3.mjs.map +1 -0
  8. package/dist/{chunk-EJALOG7L.js → chunk-JI6BDV7L.js} +1598 -405
  9. package/dist/chunk-JI6BDV7L.js.map +1 -0
  10. package/dist/{chunk-5U4NJJOF.mjs → chunk-LNBT2YPZ.mjs} +289 -2
  11. package/dist/chunk-LNBT2YPZ.mjs.map +1 -0
  12. package/dist/{chunk-NQVPCNRS.js → chunk-O3LLMGP7.js} +290 -3
  13. package/dist/chunk-O3LLMGP7.js.map +1 -0
  14. package/dist/client/index.d.mts +96 -1
  15. package/dist/client/index.d.ts +96 -1
  16. package/dist/client/index.js +9 -3
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/index.mjs +8 -2
  19. package/dist/components/index.d.mts +225 -1
  20. package/dist/components/index.d.ts +225 -1
  21. package/dist/components/index.js +25 -3
  22. package/dist/components/index.js.map +1 -1
  23. package/dist/components/index.mjs +24 -2
  24. package/dist/contexts/index.js +3 -3
  25. package/dist/contexts/index.mjs +2 -2
  26. package/dist/core/index.d.mts +108 -1
  27. package/dist/core/index.d.ts +108 -1
  28. package/dist/core/index.js +14 -2
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.mjs +13 -1
  31. package/dist/index.d.mts +2 -1
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +14 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +13 -1
  36. package/dist/oauth.interface-DsZ5ecSX.d.mts +119 -0
  37. package/dist/oauth.interface-vL7za9Bz.d.ts +119 -0
  38. package/dist/server/index.js +3 -3
  39. package/dist/server/index.mjs +1 -1
  40. package/package.json +3 -2
  41. package/src/client/index.ts +1 -0
  42. package/src/components/index.ts +1 -0
  43. package/src/core/index.ts +3 -0
  44. package/src/core/registry/ModuleRegistry.ts +2 -0
  45. package/src/features/index.ts +1 -0
  46. package/src/features/oauth/atoms/index.ts +1 -0
  47. package/src/features/oauth/atoms/oauth.atoms.ts +131 -0
  48. package/src/features/oauth/components/OAuthClientCard.tsx +105 -0
  49. package/src/features/oauth/components/OAuthClientDetail.tsx +269 -0
  50. package/src/features/oauth/components/OAuthClientForm.tsx +212 -0
  51. package/src/features/oauth/components/OAuthClientList.tsx +127 -0
  52. package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +127 -0
  53. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +152 -0
  54. package/src/features/oauth/components/OAuthScopeSelector.tsx +123 -0
  55. package/src/features/oauth/components/consent/OAuthConsentActions.tsx +41 -0
  56. package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +51 -0
  57. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +142 -0
  58. package/src/features/oauth/components/consent/OAuthScopeList.tsx +72 -0
  59. package/src/features/oauth/components/consent/index.ts +4 -0
  60. package/src/features/oauth/components/index.ts +8 -0
  61. package/src/features/oauth/data/index.ts +2 -0
  62. package/src/features/oauth/data/oauth.service.ts +191 -0
  63. package/src/features/oauth/data/oauth.ts +87 -0
  64. package/src/features/oauth/hooks/index.ts +3 -0
  65. package/src/features/oauth/hooks/useOAuthClient.ts +161 -0
  66. package/src/features/oauth/hooks/useOAuthClients.ts +111 -0
  67. package/src/features/oauth/hooks/useOAuthConsent.ts +125 -0
  68. package/src/features/oauth/index.ts +6 -0
  69. package/src/features/oauth/interfaces/index.ts +1 -0
  70. package/src/features/oauth/interfaces/oauth.interface.ts +175 -0
  71. package/src/features/oauth/oauth.module.ts +9 -0
  72. package/dist/chunk-5U4NJJOF.mjs.map +0 -1
  73. package/dist/chunk-EJALOG7L.js.map +0 -1
  74. package/dist/chunk-H5JZ5E7M.mjs.map +0 -1
  75. package/dist/chunk-NQVPCNRS.js.map +0 -1
  76. /package/dist/{BlockNoteEditor-TJNLCNIP.mjs.map → BlockNoteEditor-UNVKGZ2G.mjs.map} +0 -0
@@ -0,0 +1,119 @@
1
+ import { A as ApiDataInterface } from './ApiDataInterface-DPP8s46n.js';
2
+
3
+ /**
4
+ * OAuth client application interface
5
+ * Represents a registered OAuth application that can request access tokens
6
+ */
7
+ interface OAuthClientInterface extends ApiDataInterface {
8
+ /** The public client identifier (UUID format) */
9
+ get clientId(): string;
10
+ /** Human-readable application name */
11
+ get name(): string;
12
+ /** Optional description of the application */
13
+ get description(): string | undefined;
14
+ /** Array of allowed redirect URIs (exact match validation) */
15
+ get redirectUris(): string[];
16
+ /** Array of scopes this client can request */
17
+ get allowedScopes(): string[];
18
+ /** Supported grant types (authorization_code, client_credentials, refresh_token) */
19
+ get allowedGrantTypes(): string[];
20
+ /** True for server-side apps (can keep secret secure), false for mobile/desktop apps */
21
+ get isConfidential(): boolean;
22
+ /** Whether the client is currently active */
23
+ get isActive(): boolean;
24
+ /** When the client was created */
25
+ get createdAt(): Date;
26
+ /** When the client was last updated */
27
+ get updatedAt(): Date;
28
+ }
29
+ /**
30
+ * Input type for OAuth client CRUD operations
31
+ */
32
+ type OAuthClientInput = {
33
+ id?: string;
34
+ name?: string;
35
+ description?: string;
36
+ redirectUris?: string[];
37
+ allowedScopes?: string[];
38
+ allowedGrantTypes?: string[];
39
+ isConfidential?: boolean;
40
+ isActive?: boolean;
41
+ };
42
+ /**
43
+ * Request body for creating a new OAuth client
44
+ */
45
+ interface OAuthClientCreateRequest {
46
+ /** Required: Human-readable application name */
47
+ name: string;
48
+ /** Optional: Description of the application */
49
+ description?: string;
50
+ /** Required: At least one redirect URI */
51
+ redirectUris: string[];
52
+ /** Required: Array of scopes the client needs */
53
+ allowedScopes: string[];
54
+ /** Optional: Grant types (defaults to authorization_code + refresh_token) */
55
+ allowedGrantTypes?: string[];
56
+ /** Required: Whether this is a confidential client */
57
+ isConfidential: boolean;
58
+ }
59
+ /**
60
+ * Response when creating a client (includes one-time secret)
61
+ */
62
+ interface OAuthClientCreateResponse {
63
+ client: OAuthClientInterface;
64
+ /** Only returned on creation - must be saved immediately */
65
+ clientSecret?: string;
66
+ }
67
+ /**
68
+ * Parameters for the OAuth authorization consent flow
69
+ * Passed via URL query parameters to the consent page
70
+ */
71
+ interface OAuthConsentRequest {
72
+ /** The client_id requesting authorization */
73
+ clientId: string;
74
+ /** Where to redirect after authorization */
75
+ redirectUri: string;
76
+ /** Space-separated list of requested scopes */
77
+ scope: string;
78
+ /** CSRF protection token (passed back on redirect) */
79
+ state?: string;
80
+ /** PKCE code challenge (required for public clients) */
81
+ codeChallenge?: string;
82
+ /** PKCE method: 'S256' (recommended) or 'plain' */
83
+ codeChallengeMethod?: string;
84
+ }
85
+ /**
86
+ * Scope information for display in consent screen
87
+ */
88
+ interface OAuthScopeInfo {
89
+ /** The scope identifier (e.g., 'photographs:read') */
90
+ scope: string;
91
+ /** Human-readable scope name */
92
+ name: string;
93
+ /** Description of what this scope allows */
94
+ description: string;
95
+ /** Optional icon identifier */
96
+ icon?: string;
97
+ }
98
+ /**
99
+ * Client info returned for consent screen display
100
+ */
101
+ interface OAuthConsentInfo {
102
+ client: OAuthClientInterface;
103
+ scopes: OAuthScopeInfo[];
104
+ }
105
+ /**
106
+ * Default scope display configuration
107
+ * Maps scope identifiers to human-readable info
108
+ */
109
+ declare const OAUTH_SCOPE_DISPLAY: Record<string, OAuthScopeInfo>;
110
+ /**
111
+ * Available scopes list for the scope selector
112
+ */
113
+ declare const AVAILABLE_OAUTH_SCOPES: OAuthScopeInfo[];
114
+ /**
115
+ * Default grant types for new clients
116
+ */
117
+ declare const DEFAULT_GRANT_TYPES: string[];
118
+
119
+ export { AVAILABLE_OAUTH_SCOPES as A, DEFAULT_GRANT_TYPES as D, type OAuthClientInterface as O, type OAuthClientInput as a, type OAuthClientCreateRequest as b, type OAuthClientCreateResponse as c, type OAuthConsentRequest as d, type OAuthScopeInfo as e, type OAuthConsentInfo as f, OAUTH_SCOPE_DISPLAY as g };
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunkNQVPCNRSjs = require('../chunk-NQVPCNRS.js');
18
+ var _chunkO3LLMGP7js = require('../chunk-O3LLMGP7.js');
19
19
  require('../chunk-LXKSUWAV.js');
20
20
  require('../chunk-IBS6NI7D.js');
21
21
 
@@ -93,7 +93,7 @@ var ServerSession = class {
93
93
  if (!rawModules) return false;
94
94
  const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
95
95
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
96
- return _chunkNQVPCNRSjs.checkPermissionsFromServer.call(void 0, {
96
+ return _chunkO3LLMGP7js.checkPermissionsFromServer.call(void 0, {
97
97
  module: params.module,
98
98
  action: params.action,
99
99
  data: params.data,
@@ -303,5 +303,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
303
303
 
304
304
 
305
305
 
306
- exports.ServerAuthService = _chunkNQVPCNRSjs.AuthService; exports.ServerCompanyService = _chunkNQVPCNRSjs.CompanyService; exports.ServerContentService = _chunkNQVPCNRSjs.ContentService; exports.ServerFeatureService = _chunkNQVPCNRSjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkNQVPCNRSjs.NotificationService; exports.ServerPushService = _chunkNQVPCNRSjs.PushService; exports.ServerRoleService = _chunkNQVPCNRSjs.RoleService; exports.ServerS3Service = _chunkNQVPCNRSjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkNQVPCNRSjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
306
+ exports.ServerAuthService = _chunkO3LLMGP7js.AuthService; exports.ServerCompanyService = _chunkO3LLMGP7js.CompanyService; exports.ServerContentService = _chunkO3LLMGP7js.ContentService; exports.ServerFeatureService = _chunkO3LLMGP7js.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkO3LLMGP7js.NotificationService; exports.ServerPushService = _chunkO3LLMGP7js.PushService; exports.ServerRoleService = _chunkO3LLMGP7js.RoleService; exports.ServerS3Service = _chunkO3LLMGP7js.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkO3LLMGP7js.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
307
307
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-5U4NJJOF.mjs";
18
+ } from "../chunk-LNBT2YPZ.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.24.3",
3
+ "version": "1.25.1",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -101,7 +101,6 @@
101
101
  },
102
102
  "dependencies": {
103
103
  "@base-ui/react": "^1.0.0",
104
- "class-variance-authority": "^0.7.1",
105
104
  "@blocknote/core": "^0.45.0",
106
105
  "@blocknote/react": "^0.45.0",
107
106
  "@blocknote/shadcn": "^0.45.0",
@@ -111,6 +110,7 @@
111
110
  "@hookform/resolvers": "^5.2.2",
112
111
  "@radix-ui/react-label": "^2.1.8",
113
112
  "@radix-ui/react-slot": "^1.2.4",
113
+ "class-variance-authority": "^0.7.1",
114
114
  "clsx": "^2.1.1",
115
115
  "cmdk": "^1.1.1",
116
116
  "commander": "^14.0.2",
@@ -120,6 +120,7 @@
120
120
  "embla-carousel-react": "^8.6.0",
121
121
  "input-otp": "^1.4.2",
122
122
  "jotai": "^2.16.1",
123
+ "jotai-family": "^1.0.1",
123
124
  "lucide-react": "^0.562.0",
124
125
  "next-themes": "^0.4.6",
125
126
  "pako": "^2.1.0",
@@ -24,6 +24,7 @@ import { registerTableGenerator } from "../hooks";
24
24
  export * from "../features/content/hooks";
25
25
  export * from "../features/role/hooks";
26
26
  export * from "../features/user/hooks";
27
+ export * from "../features/oauth/hooks";
27
28
 
28
29
  registerTableGenerator("roles", useRoleTableStructure);
29
30
  registerTableGenerator("users", useUserTableStructure);
@@ -17,6 +17,7 @@ export * from "../features/feature/components";
17
17
  export * from "../features/notification/components";
18
18
  export * from "../features/role/components";
19
19
  export * from "../features/user/components";
20
+ export * from "../features/oauth/components";
20
21
 
21
22
  // shadcn/ui components (merged from /shadcnui entry point)
22
23
  export * from "../shadcnui";
package/src/core/index.ts CHANGED
@@ -58,3 +58,6 @@ export * from "../features/search";
58
58
  export * from "../features/user/author.module";
59
59
  export * from "../features/user/data";
60
60
  export * from "../features/user/user.module";
61
+ export * from "../features/oauth/oauth.module";
62
+ export * from "../features/oauth/data";
63
+ export * from "../features/oauth/interfaces";
@@ -26,6 +26,8 @@ export interface FoundationModuleDefinitions {
26
26
  StripeProduct: ModuleWithPermissions;
27
27
  StripePrice: ModuleWithPermissions;
28
28
  StripeUsage: ModuleWithPermissions;
29
+ // OAuth modules
30
+ OAuth: ModuleWithPermissions;
29
31
  }
30
32
 
31
33
  // App-specific modules - apps will augment this interface ONLY
@@ -17,3 +17,4 @@ export * from "./role";
17
17
  export * from "./s3";
18
18
  export * from "./search";
19
19
  export * from "./user";
20
+ export * from "./oauth";
@@ -0,0 +1 @@
1
+ export * from "./oauth.atoms";
@@ -0,0 +1,131 @@
1
+ import { atom } from "jotai";
2
+ import { atomFamily } from "jotai-family";
3
+ import { OAuthClientInterface } from "../interfaces/oauth.interface";
4
+
5
+ // ==========================================
6
+ // OAUTH CLIENTS STATE
7
+ // ==========================================
8
+
9
+ /**
10
+ * Primary store for OAuth clients
11
+ * Populated by useOAuthClients hook
12
+ */
13
+ export const oauthClientsAtom = atom<OAuthClientInterface[]>([]);
14
+
15
+ /**
16
+ * Loading state for OAuth clients list
17
+ */
18
+ export const oauthClientsLoadingAtom = atom<boolean>(false);
19
+
20
+ /**
21
+ * Error state for OAuth client operations
22
+ */
23
+ export const oauthClientsErrorAtom = atom<Error | null>(null);
24
+
25
+ /**
26
+ * Derived atom family for getting a single client by ID
27
+ * Usage: const client = useAtomValue(oauthClientByIdAtom(clientId))
28
+ *
29
+ * @param clientId - The client ID (not the internal id, but the clientId field)
30
+ * @returns The client if found, undefined otherwise
31
+ */
32
+ export const oauthClientByIdAtom = atomFamily((clientId: string) =>
33
+ atom((get) => {
34
+ const clients = get(oauthClientsAtom);
35
+ return clients.find((c) => c.clientId === clientId || c.id === clientId);
36
+ }),
37
+ );
38
+
39
+ // ==========================================
40
+ // ONE-TIME SECRET DISPLAY STATE
41
+ // ==========================================
42
+
43
+ /**
44
+ * Stores the client secret for one-time display
45
+ * Set after createClient or regenerateSecret, cleared after user acknowledges
46
+ */
47
+ export const oauthNewClientSecretAtom = atom<string | null>(null);
48
+
49
+ /**
50
+ * The client ID associated with the new secret
51
+ * Used to know which client the secret belongs to
52
+ */
53
+ export const oauthNewClientIdAtom = atom<string | null>(null);
54
+
55
+ // ==========================================
56
+ // CONSENT FLOW STATE
57
+ // ==========================================
58
+
59
+ /**
60
+ * Loading state for consent screen data fetch
61
+ */
62
+ export const oauthConsentLoadingAtom = atom<boolean>(false);
63
+
64
+ /**
65
+ * Error state for consent flow
66
+ */
67
+ export const oauthConsentErrorAtom = atom<Error | null>(null);
68
+
69
+ // ==========================================
70
+ // WRITE ATOMS (for actions)
71
+ // ==========================================
72
+
73
+ /**
74
+ * Write atom to set a new client secret and associated client ID
75
+ */
76
+ export const setNewClientSecretAtom = atom(null, (get, set, value: { clientId: string; secret: string } | null) => {
77
+ if (value === null) {
78
+ set(oauthNewClientSecretAtom, null);
79
+ set(oauthNewClientIdAtom, null);
80
+ } else {
81
+ set(oauthNewClientSecretAtom, value.secret);
82
+ set(oauthNewClientIdAtom, value.clientId);
83
+ }
84
+ });
85
+
86
+ /**
87
+ * Write atom to clear the new client secret (after user acknowledges)
88
+ */
89
+ export const clearNewClientSecretAtom = atom(null, (get, set) => {
90
+ set(oauthNewClientSecretAtom, null);
91
+ set(oauthNewClientIdAtom, null);
92
+ });
93
+
94
+ /**
95
+ * Write atom to update the clients list
96
+ */
97
+ export const setOAuthClientsAtom = atom(null, (get, set, clients: OAuthClientInterface[]) => {
98
+ set(oauthClientsAtom, clients);
99
+ });
100
+
101
+ /**
102
+ * Write atom to add a client to the list
103
+ */
104
+ export const addOAuthClientAtom = atom(null, (get, set, client: OAuthClientInterface) => {
105
+ const clients = get(oauthClientsAtom);
106
+ set(oauthClientsAtom, [...clients, client]);
107
+ });
108
+
109
+ /**
110
+ * Write atom to update a client in the list
111
+ */
112
+ export const updateOAuthClientAtom = atom(null, (get, set, updatedClient: OAuthClientInterface) => {
113
+ const clients = get(oauthClientsAtom);
114
+ const index = clients.findIndex((c) => c.id === updatedClient.id || c.clientId === updatedClient.clientId);
115
+ if (index !== -1) {
116
+ const newClients = [...clients];
117
+ newClients[index] = updatedClient;
118
+ set(oauthClientsAtom, newClients);
119
+ }
120
+ });
121
+
122
+ /**
123
+ * Write atom to remove a client from the list
124
+ */
125
+ export const removeOAuthClientAtom = atom(null, (get, set, clientId: string) => {
126
+ const clients = get(oauthClientsAtom);
127
+ set(
128
+ oauthClientsAtom,
129
+ clients.filter((c) => c.id !== clientId && c.clientId !== clientId),
130
+ );
131
+ });
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { formatDistanceToNow } from "date-fns";
4
+ import { Key, MoreVertical, Pencil, Trash2 } from "lucide-react";
5
+ import {
6
+ Badge,
7
+ Button,
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuTrigger,
17
+ } from "../../../shadcnui";
18
+ import { OAuthClientInterface } from "../interfaces/oauth.interface";
19
+
20
+ export interface OAuthClientCardProps {
21
+ /** The OAuth client to display */
22
+ client: OAuthClientInterface;
23
+ /** Called when card is clicked */
24
+ onClick?: () => void;
25
+ /** Called when edit is clicked */
26
+ onEdit?: () => void;
27
+ /** Called when delete is clicked */
28
+ onDelete?: () => void;
29
+ }
30
+
31
+ /**
32
+ * Card component for displaying an OAuth client in a list
33
+ */
34
+ export function OAuthClientCard({
35
+ client,
36
+ onClick,
37
+ onEdit,
38
+ onDelete,
39
+ }: OAuthClientCardProps) {
40
+ // Truncate client ID for display
41
+ const truncatedId = client.clientId.length > 12
42
+ ? `${client.clientId.slice(0, 8)}...${client.clientId.slice(-4)}`
43
+ : client.clientId;
44
+
45
+ const createdAgo = client.createdAt
46
+ ? formatDistanceToNow(new Date(client.createdAt), { addSuffix: true })
47
+ : "Unknown";
48
+
49
+ return (
50
+ <Card
51
+ className={`cursor-pointer transition-colors hover:bg-accent/50 ${!client.isActive ? "opacity-60" : ""}`}
52
+ onClick={onClick}
53
+ >
54
+ <CardHeader className="pb-2">
55
+ <div className="flex items-start justify-between">
56
+ <div className="flex items-center gap-2">
57
+ <Key className="h-5 w-5 text-muted-foreground" />
58
+ <CardTitle className="text-lg">{client.name}</CardTitle>
59
+ </div>
60
+ <div className="flex items-center gap-2">
61
+ <Badge variant={client.isActive ? "default" : "secondary"}>
62
+ {client.isActive ? "Active" : "Inactive"}
63
+ </Badge>
64
+ {(onEdit || onDelete) && (
65
+ <DropdownMenu>
66
+ <DropdownMenuTrigger onClick={(e) => e.stopPropagation()}>
67
+ <Button render={<div />} nativeButton={false} variant="ghost" size="icon" className="h-8 w-8">
68
+ <MoreVertical className="h-4 w-4" />
69
+ </Button>
70
+ </DropdownMenuTrigger>
71
+ <DropdownMenuContent align="end">
72
+ {onEdit && (
73
+ <DropdownMenuItem onClick={(e) => { e.stopPropagation(); onEdit(); }}>
74
+ <Pencil className="h-4 w-4 mr-2" />
75
+ Edit
76
+ </DropdownMenuItem>
77
+ )}
78
+ {onDelete && (
79
+ <DropdownMenuItem
80
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
81
+ className="text-destructive"
82
+ >
83
+ <Trash2 className="h-4 w-4 mr-2" />
84
+ Delete
85
+ </DropdownMenuItem>
86
+ )}
87
+ </DropdownMenuContent>
88
+ </DropdownMenu>
89
+ )}
90
+ </div>
91
+ </div>
92
+ {client.description && (
93
+ <CardDescription className="line-clamp-2">{client.description}</CardDescription>
94
+ )}
95
+ </CardHeader>
96
+ <CardContent>
97
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
98
+ <span className="font-mono">{truncatedId}</span>
99
+ <span>Created {createdAgo}</span>
100
+ <span>{client.isConfidential ? "Confidential" : "Public"}</span>
101
+ </div>
102
+ </CardContent>
103
+ </Card>
104
+ );
105
+ }