Skip to content

Commit bb4cffe

Browse files
committed
Image uploading for moderators
1 parent db13d1a commit bb4cffe

File tree

5 files changed

+124
-7
lines changed

5 files changed

+124
-7
lines changed

src/app.module.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import * as redisStore from "cache-manager-redis-store";
3535
import { MetaController } from "./rest/meta/meta.controller";
3636
import { HttpCacheInterceptor } from "./utils/cache-key-track";
3737
import { GetReportsAvailableQuery } from "./gateway/queries/GetReportsAvailable/get-reports-available.query";
38-
import { MulterModule } from "@nestjs/platform-express";
3938
import { ScheduleModule } from "@nestjs/schedule";
4039
import { MainService } from "./main.service";
4140
import { Entities } from "./db.config";
@@ -119,6 +118,8 @@ import * as TelegramBot from "node-telegram-bot-api";
119118
import { TelegramNotificationService } from "./rest/notification/telegram-notification.service";
120119
import { NewTicketMessageCreatedHandler } from "./rest/notification/event-handler/new-ticket-message-created.handler";
121120
import { MetricsService } from "./metrics.service";
121+
import { StorageController } from "./rest/storage/storage.controller";
122+
import { StorageMapper } from "./rest/storage/storage.mapper";
122123

123124
const OPENAPI_GENERATED: Provider[] = [
124125
{
@@ -240,9 +241,6 @@ const OPENAPI_GENERATED: Provider[] = [
240241
rootPath: join(__dirname, "./upload"),
241242
serveRoot: "/static/",
242243
}),
243-
MulterModule.register({
244-
dest: "./dist/upload",
245-
}),
246244
JwtModule.registerAsync({
247245
useFactory(config: ConfigService): JwtModuleOptions {
248246
return {
@@ -304,6 +302,8 @@ const OPENAPI_GENERATED: Provider[] = [
304302
NotificationController,
305303
FeedbackController,
306304
AdminFeedbackController,
305+
306+
StorageController,
307307
],
308308
providers: [
309309
...OPENAPI_GENERATED,
@@ -354,6 +354,7 @@ const OPENAPI_GENERATED: Provider[] = [
354354
LobbyMapper,
355355
NotificationMapper,
356356
FeedbackMapper,
357+
StorageMapper,
357358

358359
UserRepository,
359360
UserCreatedHandler,

src/main.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ async function bootstrap() {
3737
.addBearerAuth()
3838
.build();
3939

40-
const document = SwaggerModule.createDocument(app, options, {
41-
deepScanRoutes: true,
42-
});
40+
const document = SwaggerModule.createDocument(app, options);
4341
SwaggerModule.setup("api", app, document);
4442

4543
app.connectMicroservice({
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Query,
6+
UploadedFile,
7+
UseInterceptors,
8+
} from "@nestjs/common";
9+
import { ReqLoggingInterceptor } from "../../middleware/req-logging.interceptor";
10+
import { ApiBody, ApiConsumes, ApiQuery, ApiTags } from "@nestjs/swagger";
11+
import { ModeratorGuard, WithUser } from "../../utils/decorator/with-user";
12+
import { FileInterceptor } from "@nestjs/platform-express";
13+
import { InjectS3, S3 } from "nestjs-s3";
14+
import {
15+
ListObjectsV2CommandInput,
16+
PutObjectCommandInput,
17+
} from "@aws-sdk/client-s3";
18+
import { ConfigService } from "@nestjs/config";
19+
import { UploadedImageDto, UploadedImagePageDto } from "./storage.dto";
20+
import { StorageMapper } from "./storage.mapper";
21+
22+
interface IFile {
23+
fieldname: string;
24+
originalname: string;
25+
encoding: string;
26+
mimetype: string;
27+
buffer: Buffer;
28+
size: number;
29+
}
30+
31+
@UseInterceptors(ReqLoggingInterceptor)
32+
@Controller("storage")
33+
@ApiTags("storage")
34+
export class StorageController {
35+
constructor(
36+
@InjectS3() private readonly s3: S3,
37+
private readonly config: ConfigService,
38+
private readonly mapper: StorageMapper,
39+
) {}
40+
41+
@ApiBody({
42+
schema: {
43+
type: "object",
44+
properties: {
45+
file: {
46+
type: "string",
47+
format: "binary",
48+
},
49+
},
50+
},
51+
})
52+
@ApiConsumes("multipart/form-data")
53+
@ModeratorGuard()
54+
@WithUser()
55+
@UseInterceptors(FileInterceptor("file"))
56+
@Post("upload")
57+
public async uploadImage(
58+
@UploadedFile() file: IFile,
59+
): Promise<UploadedImageDto> {
60+
const Key = this.config.get("s3.uploadPrefix") + file.originalname;
61+
62+
const putObjectCommandInput: PutObjectCommandInput = {
63+
Bucket: this.config.get("s3.bucket"),
64+
Key,
65+
Body: file.buffer,
66+
ContentType: file.mimetype,
67+
ACL: "public-read",
68+
69+
Metadata: {
70+
originalName: file.originalname,
71+
},
72+
};
73+
74+
await this.s3.putObject(putObjectCommandInput);
75+
76+
return this.mapper.mapS3Item(Key);
77+
}
78+
79+
@ApiQuery({
80+
name: "ctoken",
81+
required: false,
82+
})
83+
@Get()
84+
public async getUploadedFiles(
85+
@Query("ctoken") ctoken?: string,
86+
): Promise<UploadedImagePageDto> {
87+
const response = await this.s3.listObjectsV2({
88+
Bucket: this.config.get("s3.bucket"),
89+
Prefix: this.config.get("s3.uploadPrefix"),
90+
ContinuationToken: ctoken,
91+
} satisfies ListObjectsV2CommandInput);
92+
93+
return {
94+
items: response.Contents.map((it) => it.Key).map(this.mapper.mapS3Item),
95+
ctoken: response.ContinuationToken,
96+
};
97+
}
98+
}

src/rest/storage/storage.dto.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export class UploadedImageDto {
2+
url: string;
3+
}
4+
5+
export class UploadedImagePageDto {
6+
items: UploadedImageDto[];
7+
ctoken?: string;
8+
}

src/rest/storage/storage.mapper.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { ConfigService } from "@nestjs/config";
3+
import { UploadedImageDto } from "./storage.dto";
4+
5+
@Injectable()
6+
export class StorageMapper {
7+
constructor(private readonly config: ConfigService) {}
8+
9+
public mapS3Item = (key: string): UploadedImageDto => ({
10+
url: `${this.config.get("api.s3root")}/${this.config.get("s3.bucket")}/${key}`,
11+
});
12+
}

0 commit comments

Comments
 (0)