Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { AuthenticationError } from 'apollo-server-express';
2
3import { ErrorType, CoopError } from '../../utils/errors.js';
4import { logErrorJson } from '../../utils/logging.js';
5import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js';
6
7/** Context shape required by rotateWebhookSigningKey (avoids importing resolvers). */
8type RotateWebhookSigningKeyContext = {
9 getUser: () => {
10 orgId: string;
11 getPermissions: () => readonly string[];
12 } | null | undefined;
13 dataSources: {
14 orgAPI: { rotateWebhookSigningKey: (orgId: string) => Promise<string> };
15 };
16};
17
18const typeDefs = /* GraphQL */ `
19 type ApiKey {
20 id: ID!
21 name: String!
22 description: String
23 isActive: Boolean!
24 createdAt: String!
25 lastUsedAt: String
26 createdBy: String
27 }
28
29 type RotateApiKeySuccessResponse {
30 apiKey: String!
31 record: ApiKey!
32 }
33
34 type RotateApiKeyError implements Error {
35 title: String!
36 status: Int!
37 type: [String!]!
38 pointer: String
39 detail: String
40 requestId: String
41 }
42
43 union RotateApiKeyResponse = RotateApiKeySuccessResponse | RotateApiKeyError
44
45 input RotateApiKeyInput {
46 name: String!
47 description: String
48 }
49
50 type RotateWebhookSigningKeySuccessResponse {
51 publicSigningKey: String!
52 }
53
54 type RotateWebhookSigningKeyError implements Error {
55 title: String!
56 status: Int!
57 type: [String!]!
58 pointer: String
59 detail: String
60 requestId: String
61 }
62
63 union RotateWebhookSigningKeyResponse =
64 RotateWebhookSigningKeySuccessResponse
65 | RotateWebhookSigningKeyError
66
67 type Query {
68 apiKey: String!
69 }
70
71 type Mutation {
72 rotateApiKey(input: RotateApiKeyInput!): RotateApiKeyResponse!
73 rotateWebhookSigningKey: RotateWebhookSigningKeyResponse!
74 }
75`;
76
77const Query: any = {
78 async apiKey(_: any, __: any, context: any) {
79 const user = context.getUser();
80 if (!user || !user.orgId) {
81 throw new AuthenticationError('User must be authenticated');
82 }
83
84 const apiKeyRecord = await context.services.ApiKeyService.getActiveApiKeyForOrg(user.orgId);
85 if (!apiKeyRecord) {
86 return process.env.NODE_ENV !== 'production' ? '' : '';
87 }
88 // Return a message indicating the key exists but is hidden for security
89 return 'API key exists (hidden for security)';
90 },
91};
92
93const Mutation: any = {
94 async rotateApiKey(_: any, { input }: any, context: any) {
95 const user = context.getUser();
96 if (!user || !user.orgId) {
97 throw new AuthenticationError('User must be authenticated');
98 }
99 if (!user.getPermissions().includes('MANAGE_ORG')) {
100 throw new AuthenticationError(
101 'User does not have permission to rotate the API key',
102 );
103 }
104
105 try {
106 const { apiKey, record } = await context.services.ApiKeyService.rotateApiKey(
107 user.orgId,
108 input.name,
109 input.description || null,
110 user.id
111 );
112
113 return gqlSuccessResult(
114 {
115 apiKey,
116 record: {
117 id: record.id,
118 name: record.name,
119 description: record.description,
120 isActive: record.isActive,
121 createdAt: record.createdAt.toISOString(),
122 lastUsedAt: record.lastUsedAt?.toISOString() || null,
123 createdBy: record.createdBy,
124 },
125 },
126 'RotateApiKeySuccessResponse'
127 );
128 } catch (error) {
129 // Resolvers do not receive a request-scoped logger; use logErrorJson for structured server-side logging.
130 // eslint-disable-next-line no-restricted-syntax -- see comment above
131 logErrorJson({ message: 'Failed to rotate API key', error });
132 return gqlErrorResult(
133 new CoopError({
134 status: 500,
135 type: [ErrorType.InternalServerError],
136 title: 'Failed to rotate API key',
137 detail: 'An error occurred while rotating the API key',
138 name: 'InternalServerError',
139 shouldErrorSpan: true,
140 }),
141 );
142 }
143 },
144 async rotateWebhookSigningKey(
145 _: unknown,
146 __: Record<string, never>,
147 context: RotateWebhookSigningKeyContext,
148 ) {
149 const user = context.getUser();
150 if (!user || !user.orgId) {
151 throw new AuthenticationError('User must be authenticated');
152 }
153 if (!user.getPermissions().includes('MANAGE_ORG')) {
154 throw new AuthenticationError(
155 'User does not have permission to rotate the webhook signing key',
156 );
157 }
158
159 try {
160 const publicSigningKey =
161 await context.dataSources.orgAPI.rotateWebhookSigningKey(user.orgId);
162 return gqlSuccessResult(
163 { publicSigningKey },
164 'RotateWebhookSigningKeySuccessResponse',
165 );
166 } catch (error) {
167 // Resolvers do not receive a request-scoped logger; use logErrorJson for structured server-side logging.
168 // eslint-disable-next-line no-restricted-syntax -- see comment above
169 logErrorJson({
170 message: 'Failed to rotate webhook signing key',
171 error,
172 });
173 return gqlErrorResult(
174 new CoopError({
175 status: 500,
176 type: [ErrorType.InternalServerError],
177 title: 'Failed to rotate webhook signing key',
178 detail: 'An error occurred while rotating the webhook signing key',
179 name: 'InternalServerError',
180 shouldErrorSpan: true,
181 }),
182 );
183 }
184 },
185};
186
187export const resolvers = { Query, Mutation };
188export { typeDefs, Query, Mutation };