Skip to content

Commit c4fbcc2

Browse files
authored
Merge pull request #42 from t3rr11/bugfix/fix-various-bugs-from-user-testing
FIX/FEAT: Fixing a bunch of bugs found during user testing
2 parents 9a8a819 + ae94253 commit c4fbcc2

141 files changed

Lines changed: 2843 additions & 853 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"devDependencies": {
2525
"@types/node": "^25.6.0",
2626
"@types/node-fetch": "^2.6.13",
27+
"selfsigned": "^5.5.0",
2728
"tsx": "^4.21.0",
2829
"typescript": "^6.0.3",
2930
"vitest": "^4.1.4"

apps/backend/src/controllers/collection.controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FastifyPluginAsync } from 'fastify';
22
import { CollectionService } from '../services/collection.service';
33
import { OpenApiService } from '../services/openapi.service';
4-
import type { AuthConfig } from '../models/proxy';
4+
import type { AuthConfig, BodyType, FormDataEntry } from '../models/proxy';
55
import type { SyncApplyBody } from '../models/openapi-sync';
66

77
interface Options {
@@ -45,13 +45,13 @@ const collectionController: FastifyPluginAsync<Options> = async (server, opts) =
4545

4646
server.post<{
4747
Params: { id: string };
48-
Body: { name: string; method: string; url: string; headers?: Record<string, string>; body?: string; auth?: AuthConfig; folderId?: string };
48+
Body: { name: string; method: string; url: string; headers?: Record<string, string>; body?: string; bodyType?: BodyType; formDataEntries?: FormDataEntry[]; auth?: AuthConfig; folderId?: string };
4949
}>('/collections/:id/requests', async (request, reply) => {
50-
const { name, method, url, headers, body, auth, folderId } = request.body;
50+
const { name, method, url, headers, body, bodyType, formDataEntries, auth, folderId } = request.body;
5151
if (!name || !method || !url) {
5252
return reply.code(400).send({ error: 'Name, method, and URL are required' });
5353
}
54-
const saved = await collectionService.addRequest(request.params.id, { name, method, url, headers, body, auth, folderId });
54+
const saved = await collectionService.addRequest(request.params.id, { name, method, url, headers, body, bodyType, formDataEntries, auth, folderId });
5555
return reply.code(201).send(saved);
5656
});
5757

apps/backend/src/controllers/oauth.controller.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,16 @@ const oauthController: FastifyPluginAsync<Options> = async (server, opts) => {
8888
return reply.code(204).send();
8989
});
9090

91-
server.post<{ Body: { configId: string; code: string; codeVerifier?: string; redirectUri: string } }>(
91+
server.post<{ Body: { configId: string; code: string; codeVerifier?: string; redirectUri: string; insecureTls?: boolean } }>(
9292
'/oauth/token',
9393
async (request, reply) => {
94-
const { configId, code, codeVerifier, redirectUri } = request.body;
94+
const { configId, code, codeVerifier, redirectUri, insecureTls } = request.body;
9595
if (!configId || !code || !redirectUri) {
9696
return reply.code(400).send({ error: 'Missing required fields: configId, code, redirectUri' });
9797
}
9898

9999
try {
100-
const result = await oauthService.exchangeToken({ configId, code, codeVerifier, redirectUri });
100+
const result = await oauthService.exchangeToken({ configId, code, codeVerifier, redirectUri, insecureTls });
101101
return reply.code(200).send(result);
102102
} catch (error) {
103103
if (axios.isAxiosError(error)) {
@@ -111,16 +111,16 @@ const oauthController: FastifyPluginAsync<Options> = async (server, opts) => {
111111
},
112112
);
113113

114-
server.post<{ Body: { configId: string; refreshToken: string } }>(
114+
server.post<{ Body: { configId: string; refreshToken: string; insecureTls?: boolean } }>(
115115
'/oauth/refresh',
116116
async (request, reply) => {
117-
const { configId, refreshToken } = request.body;
117+
const { configId, refreshToken, insecureTls } = request.body;
118118
if (!configId || !refreshToken) {
119119
return reply.code(400).send({ error: 'Missing required fields: configId, refreshToken' });
120120
}
121121

122122
try {
123-
const result = await oauthService.refreshToken({ configId, refreshToken });
123+
const result = await oauthService.refreshToken({ configId, refreshToken, insecureTls });
124124
return reply.code(200).send(result);
125125
} catch (error) {
126126
if (axios.isAxiosError(error)) {
@@ -134,16 +134,16 @@ const oauthController: FastifyPluginAsync<Options> = async (server, opts) => {
134134
},
135135
);
136136

137-
server.post<{ Body: { configId: string; token: string; tokenTypeHint?: string } }>(
137+
server.post<{ Body: { configId: string; token: string; tokenTypeHint?: string; insecureTls?: boolean } }>(
138138
'/oauth/revoke',
139139
async (request, reply) => {
140-
const { configId, token, tokenTypeHint } = request.body;
140+
const { configId, token, tokenTypeHint, insecureTls } = request.body;
141141
if (!configId || !token) {
142142
return reply.code(400).send({ error: 'Missing required fields: configId, token' });
143143
}
144144

145145
try {
146-
const result = await oauthService.revokeToken({ configId, token, tokenTypeHint });
146+
const result = await oauthService.revokeToken({ configId, token, tokenTypeHint, insecureTls });
147147
return reply.code(200).send(result);
148148
} catch (error) {
149149
if (axios.isAxiosError(error)) {
@@ -157,14 +157,14 @@ const oauthController: FastifyPluginAsync<Options> = async (server, opts) => {
157157
},
158158
);
159159

160-
server.post<{ Body: { configId: string } }>('/oauth/client-credentials', async (request, reply) => {
161-
const { configId } = request.body;
160+
server.post<{ Body: { configId: string; insecureTls?: boolean } }>('/oauth/client-credentials', async (request, reply) => {
161+
const { configId, insecureTls } = request.body;
162162
if (!configId) {
163163
return reply.code(400).send({ error: 'Missing required field: configId' });
164164
}
165165

166166
try {
167-
const result = await oauthService.clientCredentials(configId);
167+
const result = await oauthService.clientCredentials({ configId, insecureTls });
168168
return reply.code(200).send(result);
169169
} catch (error) {
170170
if (axios.isAxiosError(error)) {
@@ -177,16 +177,16 @@ const oauthController: FastifyPluginAsync<Options> = async (server, opts) => {
177177
}
178178
});
179179

180-
server.post<{ Body: { configId: string; username: string; password: string } }>(
180+
server.post<{ Body: { configId: string; username: string; password: string; insecureTls?: boolean } }>(
181181
'/oauth/password',
182182
async (request, reply) => {
183-
const { configId, username, password } = request.body;
183+
const { configId, username, password, insecureTls } = request.body;
184184
if (!configId || !username || !password) {
185185
return reply.code(400).send({ error: 'Missing required fields: configId, username, password' });
186186
}
187187

188188
try {
189-
const result = await oauthService.passwordFlow({ configId, username, password });
189+
const result = await oauthService.passwordFlow({ configId, username, password, insecureTls });
190190
return reply.code(200).send(result);
191191
} catch (error) {
192192
if (axios.isAxiosError(error)) {

apps/backend/src/dtos/oauth.dto.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,38 @@ export const tokenExchangeSchema = z.object({
2121
code: z.string().min(1, 'Authorization code is required'),
2222
codeVerifier: z.string().optional(),
2323
redirectUri: z.string().min(1, 'Redirect URI is required'),
24+
insecureTls: z.boolean().optional(),
2425
});
2526

2627
export const tokenRefreshSchema = z.object({
2728
configId: z.string().min(1, 'Config ID is required'),
2829
refreshToken: z.string().min(1, 'Refresh token is required'),
30+
insecureTls: z.boolean().optional(),
2931
});
3032

3133
export const tokenRevokeSchema = z.object({
3234
configId: z.string().min(1, 'Config ID is required'),
3335
token: z.string().min(1, 'Token is required'),
3436
tokenTypeHint: z.string().optional(),
37+
insecureTls: z.boolean().optional(),
38+
});
39+
40+
export const clientCredentialsSchema = z.object({
41+
configId: z.string().min(1, 'Config ID is required'),
42+
insecureTls: z.boolean().optional(),
43+
});
44+
45+
export const passwordFlowSchema = z.object({
46+
configId: z.string().min(1, 'Config ID is required'),
47+
username: z.string().min(1, 'Username is required'),
48+
password: z.string().min(1, 'Password is required'),
49+
insecureTls: z.boolean().optional(),
3550
});
3651

3752
export type CreateOAuthConfigDto = z.infer<typeof createOAuthConfigSchema>;
3853
export type UpdateOAuthConfigDto = z.infer<typeof updateOAuthConfigSchema>;
3954
export type TokenExchangeDto = z.infer<typeof tokenExchangeSchema>;
4055
export type TokenRefreshDto = z.infer<typeof tokenRefreshSchema>;
4156
export type TokenRevokeDto = z.infer<typeof tokenRevokeSchema>;
57+
export type ClientCredentialsDto = z.infer<typeof clientCredentialsSchema>;
58+
export type PasswordFlowDto = z.infer<typeof passwordFlowSchema>;

apps/backend/src/dtos/proxy.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const proxyRequestSchema = z.object({
99
bodyType: z.enum(['json', 'form-data', 'x-www-form-urlencoded']).optional(),
1010
formDataEntries: z.array(formDataEntrySchema).optional(),
1111
auth: authConfigSchema.optional(),
12+
insecureTls: z.boolean().optional(),
1213
});
1314

1415
/** Stream request has the same shape as a regular proxy request */

apps/backend/src/models/collection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@ export interface Collection {
1919
id: string;
2020
name: string;
2121
description?: string;
22+
isSystem?: boolean;
2223
folders: Folder[];
2324
requests: SavedRequest[];
2425
openApiSpec?: OpenApiSpecLink;
2526
createdAt: number;
2627
updatedAt: number;
2728
}
2829

30+
/** Stable id of the system-managed catch-all collection. */
31+
export const UNCATEGORIZED_COLLECTION_ID = 'uncategorized';
32+
2933
export interface SavedRequest {
3034
id: string;
3135
name: string;

apps/backend/src/models/proxy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ export interface ProxyRequest {
5353
bodyType?: BodyType;
5454
formDataEntries?: FormDataEntry[];
5555
auth?: AuthConfig;
56+
/**
57+
* When true, ignore TLS certificate errors (e.g. self-signed certs in
58+
* chain). Driven by a global user setting on the frontend.
59+
*/
60+
insecureTls?: boolean;
5661
}
5762

5863
export interface ProxyResponse {

apps/backend/src/services/collection.service.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { CollectionRepository } from '../repositories/collection.repository';
22
import { AppError } from '../errors/app-error';
33
import type { Collection, SavedRequest, Folder } from '../models/collection';
4-
import type { AuthConfig } from '../models/proxy';
4+
import { UNCATEGORIZED_COLLECTION_ID } from '../models/collection';
5+
import type { AuthConfig, BodyType, FormDataEntry } from '../models/proxy';
56

67
export interface MoveRequestParams {
78
sourceCollectionId: string;
@@ -21,6 +22,28 @@ export interface MoveFolderParams {
2122
export class CollectionService {
2223
constructor(private readonly repo: CollectionRepository) {}
2324

25+
/**
26+
* Idempotently ensure the system-managed "Uncategorized" collection exists.
27+
* Used as a catch-all when users save a request without picking a collection.
28+
*/
29+
async ensureUncategorizedCollection(): Promise<Collection> {
30+
const existing = await this.repo.getById(UNCATEGORIZED_COLLECTION_ID);
31+
if (existing) return existing;
32+
33+
const now = Date.now();
34+
const collection: Collection = {
35+
id: UNCATEGORIZED_COLLECTION_ID,
36+
name: 'Uncategorized',
37+
description: 'Saved requests that do not belong to a specific collection.',
38+
isSystem: true,
39+
folders: [],
40+
requests: [],
41+
createdAt: now,
42+
updatedAt: now,
43+
};
44+
return this.repo.create(collection);
45+
}
46+
2447
async getAll(): Promise<Collection[]> {
2548
return this.repo.getAll();
2649
}
@@ -47,6 +70,9 @@ export class CollectionService {
4770
}
4871

4972
async update(id: string, updates: Partial<Pick<Collection, 'name' | 'description'>>): Promise<Collection> {
73+
if (id === UNCATEGORIZED_COLLECTION_ID) {
74+
throw AppError.badRequest('The Uncategorized collection cannot be renamed.');
75+
}
5076
const collection = await this.repo.update(id, updates);
5177
if (!collection) {
5278
throw AppError.notFound('Collection not found');
@@ -55,6 +81,9 @@ export class CollectionService {
5581
}
5682

5783
async delete(id: string): Promise<void> {
84+
if (id === UNCATEGORIZED_COLLECTION_ID) {
85+
throw AppError.badRequest('The Uncategorized collection cannot be deleted.');
86+
}
5887
const deleted = await this.repo.delete(id);
5988
if (!deleted) {
6089
throw AppError.notFound('Collection not found');
@@ -69,17 +98,25 @@ export class CollectionService {
6998
url: string;
7099
headers?: Record<string, string>;
71100
body?: string;
101+
bodyType?: BodyType;
102+
formDataEntries?: FormDataEntry[];
72103
auth?: AuthConfig;
73104
folderId?: string;
74105
},
75106
): Promise<SavedRequest> {
107+
if (collectionId === UNCATEGORIZED_COLLECTION_ID) {
108+
await this.ensureUncategorizedCollection();
109+
}
110+
76111
const newRequest: SavedRequest = {
77112
id: `req-${Date.now()}-${Math.random().toString(36).substring(7)}`,
78113
name: data.name.trim(),
79114
method: data.method.toUpperCase(),
80115
url: data.url.trim(),
81116
headers: data.headers,
82117
body: data.body,
118+
bodyType: data.bodyType,
119+
formDataEntries: data.formDataEntries,
83120
auth: data.auth,
84121
folderId: data.folderId,
85122
collectionId,

apps/backend/src/services/environment.service.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { EnvironmentRepository } from '../repositories/environment.repository';
2-
import { substituteInRequest } from '../utils/variable-substitution';
2+
import { substituteInAuth, substituteInRequest } from '../utils/variable-substitution';
33
import { AppError } from '../errors/app-error';
44
import type { Environment, EnvironmentsData } from '../models/environment';
5-
import type { ProxyRequest } from '../models/proxy';
5+
import type { AuthConfig, ProxyRequest } from '../models/proxy';
66

77
export class EnvironmentService {
88
constructor(private readonly repo: EnvironmentRepository) {}
@@ -46,4 +46,9 @@ export class EnvironmentService {
4646
const active = this.repo.getActive();
4747
return substituteInRequest(partialRequest, active);
4848
}
49+
50+
substituteInAuth(auth: AuthConfig | undefined): AuthConfig | undefined {
51+
const active = this.repo.getActive();
52+
return substituteInAuth(auth, active);
53+
}
4954
}

0 commit comments

Comments
 (0)