Skip to content

Commit 31fcc29

Browse files
committed
Create the backend API routes
1 parent 531a4b3 commit 31fcc29

8 files changed

Lines changed: 281 additions & 0 deletions

File tree

services/agora/src/api/api.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3679,6 +3679,39 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati
36793679
options: localVarRequestOptions,
36803680
};
36813681
},
3682+
/**
3683+
*
3684+
* @param {*} [options] Override http request option.
3685+
* @throws {RequiredError}
3686+
*/
3687+
apiV1ConversationImportCsvPost: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
3688+
const localVarPath = `/api/v1/conversation/import-csv`;
3689+
// use dummy base URL string because the URL constructor only accepts absolute URLs.
3690+
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
3691+
let baseOptions;
3692+
if (configuration) {
3693+
baseOptions = configuration.baseOptions;
3694+
}
3695+
3696+
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
3697+
const localVarHeaderParameter = {} as any;
3698+
const localVarQueryParameter = {} as any;
3699+
3700+
// authentication BearerAuth required
3701+
// http bearer authentication required
3702+
await setBearerAuthToObject(localVarHeaderParameter, configuration)
3703+
3704+
3705+
3706+
setSearchParams(localVarUrlObj, localVarQueryParameter);
3707+
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
3708+
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
3709+
3710+
return {
3711+
url: toPathString(localVarUrlObj),
3712+
options: localVarRequestOptions,
3713+
};
3714+
},
36823715
/**
36833716
*
36843717
* @param {ApiV1ConversationImportPostRequest} apiV1ConversationImportPostRequest
@@ -5258,6 +5291,17 @@ export const DefaultApiFp = function(configuration?: Configuration) {
52585291
const localVarOperationServerBasePath = operationServerMap['DefaultApi.apiV1ConversationGetPost']?.[localVarOperationServerIndex]?.url;
52595292
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
52605293
},
5294+
/**
5295+
*
5296+
* @param {*} [options] Override http request option.
5297+
* @throws {RequiredError}
5298+
*/
5299+
async apiV1ConversationImportCsvPost(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ApiV1ConversationCreatePost200Response>> {
5300+
const localVarAxiosArgs = await localVarAxiosParamCreator.apiV1ConversationImportCsvPost(options);
5301+
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
5302+
const localVarOperationServerBasePath = operationServerMap['DefaultApi.apiV1ConversationImportCsvPost']?.[localVarOperationServerIndex]?.url;
5303+
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
5304+
},
52615305
/**
52625306
*
52635307
* @param {ApiV1ConversationImportPostRequest} apiV1ConversationImportPostRequest
@@ -5851,6 +5895,14 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa
58515895
apiV1ConversationGetPost(apiV1ModerationConversationWithdrawPostRequest: ApiV1ModerationConversationWithdrawPostRequest, options?: RawAxiosRequestConfig): AxiosPromise<ApiV1ConversationGetPost200Response> {
58525896
return localVarFp.apiV1ConversationGetPost(apiV1ModerationConversationWithdrawPostRequest, options).then((request) => request(axios, basePath));
58535897
},
5898+
/**
5899+
*
5900+
* @param {*} [options] Override http request option.
5901+
* @throws {RequiredError}
5902+
*/
5903+
apiV1ConversationImportCsvPost(options?: RawAxiosRequestConfig): AxiosPromise<ApiV1ConversationCreatePost200Response> {
5904+
return localVarFp.apiV1ConversationImportCsvPost(options).then((request) => request(axios, basePath));
5905+
},
58545906
/**
58555907
*
58565908
* @param {ApiV1ConversationImportPostRequest} apiV1ConversationImportPostRequest
@@ -6372,6 +6424,16 @@ export class DefaultApi extends BaseAPI {
63726424
return DefaultApiFp(this.configuration).apiV1ConversationGetPost(apiV1ModerationConversationWithdrawPostRequest, options).then((request) => request(this.axios, this.basePath));
63736425
}
63746426

6427+
/**
6428+
*
6429+
* @param {*} [options] Override http request option.
6430+
* @throws {RequiredError}
6431+
* @memberof DefaultApi
6432+
*/
6433+
public apiV1ConversationImportCsvPost(options?: RawAxiosRequestConfig) {
6434+
return DefaultApiFp(this.configuration).apiV1ConversationImportCsvPost(options).then((request) => request(this.axios, this.basePath));
6435+
}
6436+
63756437
/**
63766438
*
63776439
* @param {ApiV1ConversationImportPostRequest} apiV1ConversationImportPostRequest

services/agora/src/shared/types/dto.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ export class Dto {
189189
conversationSlugId: z.string(),
190190
})
191191
.strict();
192+
static importCsvConversationRequest = z
193+
.object({
194+
postAsOrganization: z.string().optional(),
195+
indexConversationAt: z.string().datetime().optional(),
196+
isIndexed: z.boolean(),
197+
isLoginRequired: z.boolean(),
198+
})
199+
.strict();
200+
static importCsvConversationResponse = z
201+
.object({
202+
conversationSlugId: z.string(),
203+
})
204+
.strict();
192205
static getConversationRequest = z
193206
.object({
194207
conversationSlugId: zodSlugId,
@@ -546,6 +559,12 @@ export type ImportConversationRequest = z.infer<
546559
export type ImportConversationResponse = z.infer<
547560
typeof Dto.importConversationResponse
548561
>;
562+
export type ImportCsvConversationRequest = z.infer<
563+
typeof Dto.importCsvConversationRequest
564+
>;
565+
export type ImportCsvConversationResponse = z.infer<
566+
typeof Dto.importCsvConversationResponse
567+
>;
549568
export type GetConversationResponse = z.infer<
550569
typeof Dto.getConversationResponse
551570
>;

services/api/openapi-zkorum.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4704,6 +4704,31 @@
47044704
}
47054705
}
47064706
},
4707+
"/api/v1/conversation/import-csv": {
4708+
"post": {
4709+
"responses": {
4710+
"200": {
4711+
"description": "Default Response",
4712+
"content": {
4713+
"application/json": {
4714+
"schema": {
4715+
"type": "object",
4716+
"properties": {
4717+
"conversationSlugId": {
4718+
"type": "string"
4719+
}
4720+
},
4721+
"required": [
4722+
"conversationSlugId"
4723+
],
4724+
"additionalProperties": false
4725+
}
4726+
}
4727+
}
4728+
}
4729+
}
4730+
}
4731+
},
47074732
"/api/v1/conversation/get": {
47084733
"post": {
47094734
"requestBody": {

services/api/src/index.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from "@/shared/types/dto.js";
77
import fastifyAuth from "@fastify/auth";
88
import fastifyCors from "@fastify/cors";
9+
import fastifyMultipart from "@fastify/multipart";
910
import fastifySensible from "@fastify/sensible";
1011
import fastifySwagger from "@fastify/swagger";
1112
import * as ucans from "@ucans/ucans";
@@ -21,6 +22,7 @@ import fs from "fs";
2122
import { config, log, server } from "./app.js";
2223
import * as authService from "@/service/auth.js";
2324
import * as authUtilService from "@/service/authUtil.js";
25+
import * as csvImportService from "@/service/csvImport.js";
2426
import * as feedService from "@/service/feed.js";
2527
import * as postService from "@/service/post.js";
2628
// import * as p2pService from "@/service/p2p.js";
@@ -145,6 +147,14 @@ server.register(fastifyCors, {
145147
},
146148
});
147149

150+
// Register multipart plugin for file uploads
151+
server.register(fastifyMultipart, {
152+
limits: {
153+
fileSize: 50 * 1024 * 1024, // 50MB max per file
154+
files: 3,
155+
},
156+
});
157+
148158
// Add schema validator and serializer
149159
server.setValidatorCompiler(validatorCompiler);
150160
server.setSerializerCompiler(serializerCompiler);
@@ -1655,6 +1665,95 @@ server.after(() => {
16551665
});
16561666
},
16571667
});
1668+
server.withTypeProvider<ZodTypeProvider>().route({
1669+
method: "POST",
1670+
url: `/api/${apiVersion}/conversation/import-csv`,
1671+
schema: {
1672+
response: {
1673+
200: Dto.importCsvConversationResponse,
1674+
},
1675+
},
1676+
handler: async (request, reply) => {
1677+
const { didWrite, encodedUcan, deviceStatus } =
1678+
await verifyUcanAndKnownDeviceStatus(db, request, {
1679+
expectedKnownDeviceStatus: {
1680+
isLoggedIn: true,
1681+
isRegistered: true,
1682+
},
1683+
});
1684+
1685+
// Parse multipart request
1686+
const parts = request.parts();
1687+
const files: Record<string, string> = {};
1688+
let formFields: Record<string, string> = {};
1689+
1690+
for await (const part of parts) {
1691+
if (part.type === "file") {
1692+
// Read file content as string
1693+
const buffer = await part.toBuffer();
1694+
files[part.filename] = buffer.toString("utf-8");
1695+
} else {
1696+
// Parse form fields
1697+
formFields[part.fieldname] = part.value as string;
1698+
}
1699+
}
1700+
1701+
// Validate form fields using DTO
1702+
const parsedFields = Dto.importCsvConversationRequest.parse({
1703+
postAsOrganization: formFields.postAsOrganization || undefined,
1704+
indexConversationAt:
1705+
formFields.indexConversationAt || undefined,
1706+
isIndexed: formFields.isIndexed === "true",
1707+
isLoginRequired: formFields.isLoginRequired === "true",
1708+
});
1709+
1710+
// Check organization restriction (same as URL import)
1711+
if (
1712+
(parsedFields.postAsOrganization === undefined ||
1713+
parsedFields.postAsOrganization === "") &&
1714+
config.IS_ORG_IMPORT_ONLY
1715+
) {
1716+
throw server.httpErrors.forbidden(
1717+
"CSV import feature restricted to organizations",
1718+
);
1719+
}
1720+
1721+
// Verify organization membership if specified
1722+
if (
1723+
parsedFields.postAsOrganization !== undefined &&
1724+
parsedFields.postAsOrganization !== ""
1725+
) {
1726+
const organizationId =
1727+
await authUtilService.isUserPartOfOrganization({
1728+
db,
1729+
organizationName: parsedFields.postAsOrganization,
1730+
userId: deviceStatus.userId,
1731+
});
1732+
if (organizationId === undefined) {
1733+
throw server.httpErrors.forbidden(
1734+
`User '${deviceStatus.userId}' is not part of the organization: '${parsedFields.postAsOrganization}'`,
1735+
);
1736+
}
1737+
}
1738+
1739+
// Call CSV import service
1740+
const { conversationSlugId } =
1741+
await csvImportService.processCsvImport({
1742+
db,
1743+
voteBuffer,
1744+
files,
1745+
proof: encodedUcan,
1746+
didWrite,
1747+
authorId: deviceStatus.userId,
1748+
postAsOrganization: parsedFields.postAsOrganization,
1749+
indexConversationAt: parsedFields.indexConversationAt,
1750+
isLoginRequired: parsedFields.isLoginRequired,
1751+
isIndexed: parsedFields.isIndexed,
1752+
});
1753+
1754+
reply.send({ conversationSlugId });
1755+
},
1756+
});
16581757
server.withTypeProvider<ZodTypeProvider>().route({
16591758
method: "POST",
16601759
url: `/api/${apiVersion}/conversation/get`,

services/api/src/shared/types/dto.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ export class Dto {
189189
conversationSlugId: z.string(),
190190
})
191191
.strict();
192+
static importCsvConversationRequest = z
193+
.object({
194+
postAsOrganization: z.string().optional(),
195+
indexConversationAt: z.string().datetime().optional(),
196+
isIndexed: z.boolean(),
197+
isLoginRequired: z.boolean(),
198+
})
199+
.strict();
200+
static importCsvConversationResponse = z
201+
.object({
202+
conversationSlugId: z.string(),
203+
})
204+
.strict();
192205
static getConversationRequest = z
193206
.object({
194207
conversationSlugId: zodSlugId,
@@ -546,6 +559,12 @@ export type ImportConversationRequest = z.infer<
546559
export type ImportConversationResponse = z.infer<
547560
typeof Dto.importConversationResponse
548561
>;
562+
export type ImportCsvConversationRequest = z.infer<
563+
typeof Dto.importCsvConversationRequest
564+
>;
565+
export type ImportCsvConversationResponse = z.infer<
566+
typeof Dto.importCsvConversationResponse
567+
>;
549568
export type GetConversationResponse = z.infer<
550569
typeof Dto.getConversationResponse
551570
>;

services/load-testing/src/shared/types/dto.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ export class Dto {
189189
conversationSlugId: z.string(),
190190
})
191191
.strict();
192+
static importCsvConversationRequest = z
193+
.object({
194+
postAsOrganization: z.string().optional(),
195+
indexConversationAt: z.string().datetime().optional(),
196+
isIndexed: z.boolean(),
197+
isLoginRequired: z.boolean(),
198+
})
199+
.strict();
200+
static importCsvConversationResponse = z
201+
.object({
202+
conversationSlugId: z.string(),
203+
})
204+
.strict();
192205
static getConversationRequest = z
193206
.object({
194207
conversationSlugId: zodSlugId,
@@ -546,6 +559,12 @@ export type ImportConversationRequest = z.infer<
546559
export type ImportConversationResponse = z.infer<
547560
typeof Dto.importConversationResponse
548561
>;
562+
export type ImportCsvConversationRequest = z.infer<
563+
typeof Dto.importCsvConversationRequest
564+
>;
565+
export type ImportCsvConversationResponse = z.infer<
566+
typeof Dto.importCsvConversationResponse
567+
>;
549568
export type GetConversationResponse = z.infer<
550569
typeof Dto.getConversationResponse
551570
>;

services/math-updater/src/shared/types/dto.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,19 @@ export class Dto {
189189
conversationSlugId: z.string(),
190190
})
191191
.strict();
192+
static importCsvConversationRequest = z
193+
.object({
194+
postAsOrganization: z.string().optional(),
195+
indexConversationAt: z.string().datetime().optional(),
196+
isIndexed: z.boolean(),
197+
isLoginRequired: z.boolean(),
198+
})
199+
.strict();
200+
static importCsvConversationResponse = z
201+
.object({
202+
conversationSlugId: z.string(),
203+
})
204+
.strict();
192205
static getConversationRequest = z
193206
.object({
194207
conversationSlugId: zodSlugId,
@@ -546,6 +559,12 @@ export type ImportConversationRequest = z.infer<
546559
export type ImportConversationResponse = z.infer<
547560
typeof Dto.importConversationResponse
548561
>;
562+
export type ImportCsvConversationRequest = z.infer<
563+
typeof Dto.importCsvConversationRequest
564+
>;
565+
export type ImportCsvConversationResponse = z.infer<
566+
typeof Dto.importCsvConversationResponse
567+
>;
549568
export type GetConversationResponse = z.infer<
550569
typeof Dto.getConversationResponse
551570
>;

0 commit comments

Comments
 (0)