66} from "@/shared/types/dto.js" ;
77import fastifyAuth from "@fastify/auth" ;
88import fastifyCors from "@fastify/cors" ;
9+ import fastifyMultipart from "@fastify/multipart" ;
910import fastifySensible from "@fastify/sensible" ;
1011import fastifySwagger from "@fastify/swagger" ;
1112import * as ucans from "@ucans/ucans" ;
@@ -21,6 +22,7 @@ import fs from "fs";
2122import { config , log , server } from "./app.js" ;
2223import * as authService from "@/service/auth.js" ;
2324import * as authUtilService from "@/service/authUtil.js" ;
25+ import * as csvImportService from "@/service/csvImport.js" ;
2426import * as feedService from "@/service/feed.js" ;
2527import * 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
149159server . setValidatorCompiler ( validatorCompiler ) ;
150160server . 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` ,
0 commit comments