Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
8ce1d5f
chore(deps): update dependency @testing-library/jest-dom to ^6.7.0 (#…
renovate[bot] Aug 14, 2025
1ed3596
fix(deps): update dependency pino to ^9.9.0 (#1954)
renovate[bot] Aug 14, 2025
ac564b2
fix(deps): update dependency i18next to ^25.3.5 (#1959)
renovate[bot] Aug 14, 2025
9d9df20
chore(deps): update dependency @babel/core to ^7.28.3 (#1961)
renovate[bot] Aug 14, 2025
8180dac
fix(deps): update dependency i18next to ^25.3.6 (#1962)
renovate[bot] Aug 14, 2025
2cedaeb
fix(deps): update tanstack-query monorepo to ^5.85.3 (#1960)
renovate[bot] Aug 15, 2025
a9fd083
chore(deps): update dependency @types/nodemailer to ^6.4.18 (#1963)
renovate[bot] Aug 15, 2025
4a4b436
fix(deps): update turbo monorepo to ^2.5.6 (#1964)
renovate[bot] Aug 15, 2025
a254a39
chore(deps): update dependency @types/nodemailer to v7 (#1965)
renovate[bot] Aug 16, 2025
981b2d7
chore(deps): update swc monorepo (#1917)
renovate[bot] Aug 17, 2025
81a6302
fix(deps): update dependency lucide-react to ^0.540.0 (#1966)
renovate[bot] Aug 18, 2025
2c1d113
fix(deps): update dependency typeorm to ^0.3.26 (#1967)
renovate[bot] Aug 18, 2025
5477552
fix(deps): update typescript-eslint monorepo to ^8.40.0 (#1968)
renovate[bot] Aug 19, 2025
936bde6
fix(deps): update nextjs monorepo to ^15.4.7 (#1969)
renovate[bot] Aug 19, 2025
58dbe9a
fix(deps): update tanstack-query monorepo to ^5.85.5 (#1970)
renovate[bot] Aug 19, 2025
6bc97d3
chore(deps): update pnpm to v10.15.0 (#1971)
renovate[bot] Aug 20, 2025
f7c6541
implement presigned url download
jihun Aug 20, 2025
0a1962c
minor change
jihun Aug 20, 2025
769f5a1
fix(deps): update dependency zustand to ^5.0.8 (#1972)
renovate[bot] Aug 20, 2025
4e6149d
fix(deps): update dependency joi to ^18.0.1 (#1975)
renovate[bot] Aug 20, 2025
113d3ac
chore(deps): update dependency @types/nodemailer to ^7.0.1 (#1977)
renovate[bot] Aug 21, 2025
96ad81a
chore(deps): update dependency @playwright/test to ^1.55.0 (#1976)
renovate[bot] Aug 21, 2025
0c295a8
chore(deps): update dependency @testing-library/jest-dom to ^6.8.0 (#…
renovate[bot] Aug 21, 2025
3cefcf6
chore(deps): update dependency @swc/core to ^1.13.4 (#1982)
renovate[bot] Aug 21, 2025
504c37e
fix(deps): update aws-sdk-js-v3 monorepo to ^3.873.0 (#1978)
renovate[bot] Aug 22, 2025
8e8a92f
fix(deps): update dependency i18next to ^25.4.0 (#1980)
renovate[bot] Aug 22, 2025
d7d5a9d
fix(deps): update dependency react-i18next to ^15.7.1 (#1981)
renovate[bot] Aug 22, 2025
b86df22
fix(deps): update dependency lucide-react to ^0.541.0 (#1983)
renovate[bot] Aug 22, 2025
5b2fb4c
chore(deps): update dependency @types/react to ^19.1.11 (#1984)
renovate[bot] Aug 22, 2025
223866b
chore(deps): update dependency eslint to ^9.34.0 (#1986)
renovate[bot] Aug 23, 2025
5bd7c6a
fix(deps): update dependency nuqs to ^2.5.0 (#1985)
renovate[bot] Aug 23, 2025
6b96f15
fix(deps): update dependency zod to ^4.1.0 (#1987)
renovate[bot] Aug 23, 2025
2e252a7
fix(deps): update dependency i18next to ^25.4.1 (#1988)
renovate[bot] Aug 24, 2025
8b291b4
fix(deps): update dependency react-i18next to ^15.7.2 (#1989)
renovate[bot] Aug 24, 2025
dc2fe06
chore(deps): update dependency ts-loader to ^9.5.4 (#1990)
renovate[bot] Aug 24, 2025
89070c8
chore(deps): update dependency @swc/core to ^1.13.5 (#1992)
renovate[bot] Aug 24, 2025
7fb72c8
fix(deps): update dependency i18next to ^25.4.2 (#1994)
renovate[bot] Aug 24, 2025
e0bc78d
fix(deps): update dependency zod to ^4.1.1 (#1991)
renovate[bot] Aug 25, 2025
e2fc731
change imageDownloadSecurity to enablePresignedUrlDownload
jihun Aug 25, 2025
7f293c0
fix(deps): update dependency nuqs to ^2.5.1 (#1995)
renovate[bot] Aug 25, 2025
d093f3c
fix(deps): update typescript-eslint monorepo to ^8.41.0 (#1996)
renovate[bot] Aug 26, 2025
2e71ed0
update guide text
jihun Aug 26, 2025
bf433a0
fix(deps): update dependency zod to ^4.1.3 (#1997)
renovate[bot] Aug 26, 2025
80cdba6
chore(deps): update dependency @types/react-dom to ^19.1.8 (#1998)
renovate[bot] Aug 26, 2025
a4b8115
feat: implement presigned URL download functionality for images
chiol Aug 27, 2025
08042aa
feat: make enablePresignedUrlDownload optional in API types and updat…
chiol Aug 27, 2025
e559297
fix(deps): update aws-sdk-js-v3 monorepo to ^3.876.0 (#2000)
renovate[bot] Aug 27, 2025
7f9710d
fix ai field race condition bug
jihun Aug 27, 2025
aad347f
fix(deps): update dependency lucide-react to ^0.542.0 (#1999)
renovate[bot] Aug 27, 2025
28e3da2
chore(deps): update jest monorepo to ^30.1.0 (#2002)
renovate[bot] Aug 27, 2025
db6f36a
fix(deps): update dependency dayjs to ^1.11.14 (#2003)
renovate[bot] Aug 27, 2025
988c69d
chore(deps): update dependency @types/react to ^19.1.12 (#2004)
renovate[bot] Aug 27, 2025
f4f43af
feat: add relative class to SwiperSlide components for improved styling
chiol Aug 28, 2025
9476928
chore(deps): update jest monorepo to ^30.1.1 (#2005)
renovate[bot] Aug 28, 2025
3b40f80
Merge pull request #2001 from line/fix/fix-ai-field-race-condition-bug
jihun Aug 28, 2025
c377eeb
fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^…
renovate[bot] Aug 28, 2025
9b35ce4
fix(deps): update dependency dayjs to ^1.11.15 (#2008)
renovate[bot] Aug 28, 2025
adb50b2
feat: enhance feedback UI with image previews and improve dialog inte…
chiol Aug 29, 2025
6bada64
fix update query bug
jihun Aug 29, 2025
16f6cb1
feat: update image container styles with background color for better …
chiol Aug 29, 2025
d06e988
feat: enhance image preview functionality with improved URL handling …
chiol Aug 29, 2025
36a51a1
Merge pull request #1974 from line/feat/implement-presigned-url-download
jihun Aug 29, 2025
4e4fae0
feat: add FeedbackImage component and integrate it into ImagePreviewB…
chiol Aug 29, 2025
d079840
chore(deps): update dependency @types/react-dom to ^19.1.9 (#2009)
renovate[bot] Aug 31, 2025
fe0dffc
chore(deps): update dependency jest-fixed-jsdom to ^0.0.10 (#2012)
renovate[bot] Aug 31, 2025
d08e5f5
fix(deps): update dependency dayjs to ^1.11.18 (#2013)
renovate[bot] Aug 31, 2025
4352fcf
fix(deps): update dependency nodemailer to ^7.0.6 (#2014)
renovate[bot] Aug 31, 2025
6cd7faf
feat: improve image key extraction logic in FeedbackImage component
chiol Sep 1, 2025
3ec4113
feat: add onClick handler to FeedbackImage for opening images in a ne…
chiol Sep 1, 2025
96a15d8
chore(deps): update node.js to v22.19.0 (#1913)
renovate[bot] Sep 1, 2025
7c4700b
chore(deps): update jest monorepo to ^30.1.2 (#2016)
renovate[bot] Sep 1, 2025
dd499e8
fix(deps): update dependency mysql2 to ^3.14.4 (#2017)
renovate[bot] Sep 1, 2025
a29f8bf
fix(deps): update dependency nuqs to ^2.5.2 (#2015)
renovate[bot] Sep 1, 2025
ed54985
chore(deps): update pnpm to v10.15.1 (#2019)
renovate[bot] Sep 1, 2025
69ef27d
fix(deps): update dependency react-i18next to ^15.7.3 (#2018)
renovate[bot] Sep 1, 2025
783fd4b
fix(deps): update dependency immer to ^10.1.3 (#2021)
renovate[bot] Sep 2, 2025
cab7e95
fix(deps): update dependency zod to ^4.1.5 (#2007)
renovate[bot] Sep 2, 2025
dfc3e71
fix(deps): update aws-sdk-js-v3 monorepo to ^3.879.0 (#2022)
renovate[bot] Sep 2, 2025
0e57021
fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^…
renovate[bot] Sep 2, 2025
e912272
fix(deps): update tanstack-query monorepo to ^5.85.8 (#2020)
renovate[bot] Sep 2, 2025
eb8887d
fix(deps): update typescript-eslint monorepo to ^8.42.0 (#2025)
renovate[bot] Sep 2, 2025
edfc4e7
chore(deps): update dependency jest to ^30.1.3 (#2026)
renovate[bot] Sep 2, 2025
e40909d
fix(deps): update tanstack-query monorepo to ^5.85.9 (#2027)
renovate[bot] Sep 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.18.0
22.19.0
1 change: 1 addition & 0 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ To enable image uploads directly to the server, you must configure the image sto
- `endpoint`: The endpoint URL for the storage service.
- `region`: The region your storage service is located in.
- `bucket`: The name of the bucket where images will be stored.
- `enablePresignedUrlDownload`: Enable the setting to enhance download security by using the pre-signed URL feature supported by AWS S3.

Depending on your use case and the desired level of access, you may need to adjust the permissions of your S3 bucket. If your application requires that the images be publicly accessible, configure your S3 bucket's policy to allow public reads.

Expand Down
26 changes: 13 additions & 13 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
},
"prettier": "@ufb/prettier-config",
"dependencies": {
"@aws-sdk/client-s3": "^3.864.0",
"@aws-sdk/s3-request-presigner": "^3.864.0",
"@aws-sdk/client-s3": "^3.879.0",
"@aws-sdk/s3-request-presigner": "^3.879.0",
"@fastify/multipart": "^9.0.3",
"@fastify/static": "^8.2.0",
"@nestjs-modules/mailer": "^2.0.2",
Expand Down Expand Up @@ -58,14 +58,14 @@
"exceljs": "^4.4.0",
"fast-csv": "^5.0.5",
"fastify": "^5.4.0",
"joi": "^18.0.0",
"joi": "^18.0.1",
"luxon": "^3.7.1",
"magic-bytes.js": "^1.12.1",
"mysql2": "^3.14.3",
"mysql2": "^3.14.4",
"nestjs-cls": "^6.0.1",
"nestjs-pino": "^4.4.0",
"nestjs-typeorm-paginate": "^4.1.0",
"nodemailer": "^7.0.5",
"nodemailer": "^7.0.6",
"passport": "^0.7.0",
"passport-custom": "^1.1.1",
"passport-jwt": "^4.0.1",
Expand All @@ -76,7 +76,7 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"source-map-support": "^0.5.21",
"typeorm": "^0.3.25",
"typeorm": "^0.3.26",
"typeorm-naming-strategies": "^4.1.0",
"typeorm-transactional": "^0.5.0",
"uuid": "^11.1.0"
Expand All @@ -86,29 +86,29 @@
"@nestjs/cli": "^11.0.10",
"@nestjs/schematics": "^11.0.7",
"@nestjs/testing": "^11.1.6",
"@swc-node/jest": "^1.8.13",
"@swc-node/jest": "^1.9.1",
"@swc/cli": "0.7.8",
"@swc/core": "^1.4.16",
"@swc/core": "^1.13.5",
"@swc/helpers": "^0.5.17",
"@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.3",
"@types/jest": "^30.0.0",
"@types/luxon": "^3.7.1",
"@types/node": "22.17.0",
"@types/nodemailer": "^6.4.17",
"@types/node": "22.18.0",
"@types/nodemailer": "^7.0.1",
"@types/passport-jwt": "*",
"@types/supertest": "^6.0.3",
"@typescript-eslint/parser": "^8.39.1",
"@typescript-eslint/parser": "^8.42.0",
"@ufb/eslint-config": "workspace:*",
"@ufb/prettier-config": "workspace:*",
"@ufb/tsconfig": "workspace:*",
"eslint": "catalog:",
"jest": "^30.0.5",
"jest": "^30.1.3",
"mockdate": "^3.0.5",
"prettier": "catalog:",
"supertest": "^7.1.4",
"ts-jest": "^29.4.1",
"ts-loader": "^9.5.2",
"ts-loader": "^9.5.4",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "catalog:"
Expand Down
42 changes: 42 additions & 0 deletions apps/api/src/domains/admin/channel/channel/channel.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* under the License.
*/
import {
BadRequestException,
Body,
Controller,
Delete,
Expand All @@ -30,6 +31,7 @@ import {
ApiCreatedResponse,
ApiOkResponse,
ApiParam,
ApiQuery,
ApiTags,
} from '@nestjs/swagger';

Expand Down Expand Up @@ -145,6 +147,7 @@ export class ChannelController {
secretAccessKey,
endpoint,
region,
bucket,
}: ImageUploadUrlTestRequestDto,
) {
return {
Expand All @@ -153,7 +156,46 @@ export class ChannelController {
secretAccessKey,
endpoint,
region,
bucket,
}),
};
}

@ApiParam({ name: 'projectId', type: Number })
@ApiParam({ name: 'channelId', type: Number })
@ApiQuery({
name: 'imageKey',
type: String,
required: true,
description: 'Image Key for the pre-signed url download',
example: 'test-image-key.jpg',
})
@ApiOkResponse({ type: String })
@UseGuards(JwtAuthGuard)
@Get('/:channelId/image-download-url')
async getImageDownloadUrl(
@Param('projectId', ParseIntPipe) projectId: number,
@Param('channelId', ParseIntPipe) channelId: number,
@Query('imageKey') imageKey: string,
) {
if (!imageKey) {
throw new BadRequestException('imageKey is required in query parameter');
}
const channel = await this.channelService.findById({ channelId });
if (channel.project.id !== projectId) {
throw new BadRequestException('Invalid channel id');
}
if (!channel.imageConfig) {
throw new BadRequestException('No image config in this channel');
}

return await this.channelService.createImageDownloadUrl({
accessKeyId: channel.imageConfig.accessKeyId,
secretAccessKey: channel.imageConfig.secretAccessKey,
endpoint: channel.imageConfig.endpoint,
region: channel.imageConfig.region,
bucket: channel.imageConfig.bucket,
imageKey,
});
}
Comment on lines +176 to +200
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Scope imageKey to a per-channel prefix to prevent cross-object access within shared buckets.

If the bucket is shared across channels/projects, allowing arbitrary imageKey enables presigning unrelated objects. Validate that imageKey starts with a channel/project-scoped prefix (e.g., projects/${projectId}/channels/${channelId}/). If you don’t have a prefix today, consider adding one to your image config and enforce it here.

I can propose a small schema addition (e.g., downloadKeyPrefix) to imageConfig and wire validation here—want me to open a follow-up?

🤖 Prompt for AI Agents
In apps/api/src/domains/admin/channel/channel/channel.controller.ts around lines
176-200, the current implementation accepts any imageKey and can presign objects
outside the channel scope when buckets are shared; validate and scope imageKey
before generating a download URL by requiring it to start with an explicit
channel/project prefix (e.g., `projects/${projectId}/channels/${channelId}/`) or
with a configured imageConfig.downloadKeyPrefix if present, and throw
BadRequestException when the key does not match; if your imageConfig lacks a
downloadKeyPrefix, add that field to the schema and persist appropriate prefixes
for existing channels, then use the validated/scoped key when calling
createImageDownloadUrl so only channel-scoped objects can be presigned.

}
26 changes: 23 additions & 3 deletions apps/api/src/domains/admin/channel/channel/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
* under the License.
*/
import {
ListBucketsCommand,
GetObjectCommand,
ListObjectsCommand,
PutObjectCommand,
S3Client,
} from '@aws-sdk/client-s3';
Expand All @@ -26,6 +27,7 @@ import { Transactional } from 'typeorm-transactional';
import { OpensearchRepository } from '@/common/repositories';
import { ProjectService } from '@/domains/admin/project/project/project.service';
import type {
CreateImageDownloadUrlDto,
CreateImageUploadUrlDto,
ImageUploadUrlTestDto,
} from '../../feedback/dtos';
Expand Down Expand Up @@ -130,16 +132,34 @@ export class ChannelService {
return await getSignedUrl(s3, command, { expiresIn: 60 * 60 });
}

async createImageDownloadUrl(dto: CreateImageDownloadUrlDto) {
const { accessKeyId, secretAccessKey, endpoint, region, bucket, imageKey } =
dto;

const s3 = new S3Client({
credentials: { accessKeyId, secretAccessKey },
endpoint,
region,
});

const command = new GetObjectCommand({
Bucket: bucket,
Key: imageKey,
});

return await getSignedUrl(s3, command, { expiresIn: 60 });
}
Comment on lines +135 to +151
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Presigned download URL OK; consider configurable expiry and endpoint path-style.

Make expiry configurable and support path-style for MinIO/compatible endpoints via config.

-    const s3 = new S3Client({
-      credentials: { accessKeyId, secretAccessKey },
-      endpoint,
-      region,
-    });
+    const s3 = new S3Client({
+      credentials: { accessKeyId, secretAccessKey },
+      endpoint,
+      region,
+      // e.g., set via env S3_FORCE_PATH_STYLE=true for non-AWS endpoints
+      forcePathStyle: Boolean(this.configService.get('s3.forcePathStyle')),
+    });
@@
-    return await getSignedUrl(s3, command, { expiresIn: 60 });
+    const expiresIn =
+      Number(this.configService.get('s3.downloadUrlExpiresInSeconds')) || 60;
+    return await getSignedUrl(s3, command, { expiresIn });

Additionally, the existing upload presign above includes ContentType and ACL headers that can cause issues/security exposure:

  • ContentType='image/*' will not match clients sending 'image/png' and will break the signature.
  • ACL='public-read' makes uploaded images public.

Recommend removing both unless explicitly required by policy.

-    const command = new PutObjectCommand({
-      Bucket: bucket,
-      Key: `${projectId}_${channelId}_${Date.now()}.${extension}`,
-      ContentType: 'image/*',
-      ACL: 'public-read',
-    });
+    const command = new PutObjectCommand({
+      Bucket: bucket,
+      Key: `${projectId}_${channelId}_${Date.now()}.${extension}`,
+    });


async isValidImageConfig(dto: ImageUploadUrlTestDto) {
const { accessKeyId, secretAccessKey, endpoint, region } = dto;
const { accessKeyId, secretAccessKey, endpoint, region, bucket } = dto;

const s3 = new S3Client({
credentials: { accessKeyId, secretAccessKey },
endpoint,
region,
});

const command = new ListBucketsCommand({});
const command = new ListObjectsCommand({ Bucket: bucket });

try {
await s3.send(command);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* under the License.
*/
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { IsBoolean, IsString } from 'class-validator';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validation bug: optional field will fail without @IsOptional and TS optional

enablePresignedUrlDownload is documented as not required but will fail validation when omitted. Make it truly optional.

-import { IsBoolean, IsString } from 'class-validator';
+import { IsBoolean, IsOptional, IsString } from 'class-validator';
@@
-  @ApiProperty({ required: false })
-  @IsBoolean()
-  enablePresignedUrlDownload: boolean;
+  @ApiProperty({ required: false })
+  @IsOptional()
+  @IsBoolean()
+  enablePresignedUrlDownload?: boolean;

Also applies to: 44-47

🤖 Prompt for AI Agents
In
apps/api/src/domains/admin/channel/channel/dtos/requests/image-config-request.dto.ts
around line 17 (and also apply the same change to the properties at lines
44-47), the boolean field enablePresignedUrlDownload is documented as optional
but lacks @IsOptional and a TS optional marker, so validation fails when
omitted; update the import line to include IsOptional, annotate the
enablePresignedUrlDownload property with @IsOptional(), and make the property
optional in TypeScript (use the ? suffix and boolean type) for both occurrences
so class-validator will allow the field to be omitted.


export class ImageConfigRequestDto {
@ApiProperty()
Expand All @@ -40,4 +40,8 @@ export class ImageConfigRequestDto {
@ApiProperty({ nullable: true, type: [String] })
@IsString({ each: true })
domainWhiteList: string[];

@ApiProperty({ required: false })
@IsBoolean()
enablePresignedUrlDownload: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class FindChannelByIdResponseDto {
description: string;

@Expose()
@ApiProperty()
@ApiProperty({ required: false })
imageConfig: ImageConfigResponseDto;

@Expose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ export class ImageConfigResponseDto {
@Expose()
@ApiProperty()
domainWhiteList: string[];

@Expose()
@ApiProperty({ required: false, type: 'boolean' })
enablePresignedUrlDownload: boolean | undefined;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright 2025 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

export class CreateImageDownloadUrlDto {
accessKeyId: string;
secretAccessKey: string;
endpoint: string;
region: string;
bucket: string;
imageKey: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export class ImageUploadUrlTestDto {
secretAccessKey: string;
endpoint: string;
region: string;
bucket: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation and Swagger metadata for the new bucket field (and consider marking secrets writeOnly).

To keep DTOs consistent and avoid leaking secrets in logs/docs, annotate fields. At minimum, add decorators for bucket; optionally add writeOnly for secretAccessKey across this DTO.

Apply locally within this file:

 export class ImageUploadUrlTestDto {
   accessKeyId: string;
   secretAccessKey: string;
   endpoint: string;
   region: string;
-  bucket: string;
+  // S3 bucket name for the test upload
+  bucket: string;
 }

And add these imports and decorators (outside the shown range) for stronger typing and API docs:

import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty } from 'class-validator';

export class ImageUploadUrlTestDto {
  @ApiProperty({ description: 'S3 access key ID', writeOnly: true })
  @IsString() @IsNotEmpty()
  accessKeyId: string;

  @ApiProperty({ description: 'S3 secret access key', writeOnly: true })
  @IsString() @IsNotEmpty()
  secretAccessKey: string;

  @ApiProperty({ description: 'S3-compatible endpoint (https://...)' })
  @IsString() @IsNotEmpty()
  endpoint: string;

  @ApiProperty({ description: 'AWS region (e.g., ap-northeast-2)' })
  @IsString() @IsNotEmpty()
  region: string;

  @ApiProperty({ description: 'Bucket name' })
  @IsString() @IsNotEmpty()
  bucket: string;
}

Also ensure request logging (if any) masks secretAccessKey.

🤖 Prompt for AI Agents
In apps/api/src/domains/admin/feedback/dtos/image-upload-url-test.dto.ts around
line 22, the new bucket field lacks validation and Swagger metadata; update the
DTO to import ApiProperty from @nestjs/swagger and IsString, IsNotEmpty from
class-validator, then annotate bucket with @ApiProperty({ description: 'Bucket
name' }) and @IsString() @IsNotEmpty(); also add the suggested decorators for
accessKeyId, secretAccessKey (mark secretAccessKey writeOnly: true), endpoint
and region as shown in the review comment so all fields have proper validation
and documentation, and ensure any request logging masks secretAccessKey.

}
1 change: 1 addition & 0 deletions apps/api/src/domains/admin/feedback/dtos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
CreateFeedbackOSDto,
} from './create-feedback.dto';
export { CreateImageUploadUrlDto } from './create-image-upload-url.dto';
export { CreateImageDownloadUrlDto } from './create-image-download-url.dto';
export { FindFeedbacksByChannelIdDto } from './find-feedbacks-by-channel-id.dto';
export {
UpdateFeedbackDto,
Expand Down
57 changes: 33 additions & 24 deletions apps/api/src/domains/admin/feedback/feedback.mysql.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,37 +551,46 @@ export class FeedbackMySQLService {
async updateFeedback(dto: UpdateFeedbackMySQLDto) {
const { feedbackId, data } = dto;

let query = `JSON_SET(IFNULL(feedbacks.data,'{}'), `;
const parameters: Record<string, any> = {};

if (Object.keys(data).length === 0) {
query = 'data';
} else {
Object.entries(data).forEach(([fieldKey, value], index) => {
query += `'$.${fieldKey}', `;
if (Array.isArray(value)) {
const arrayParams = value
.map((v, i) => {
const paramName = `value${index}_${i}`;
parameters[paramName] = v as string | number;
return `:${paramName}`;
})
.join(', ');
query += `JSON_ARRAY(${arrayParams})`;
} else {
const paramName = `value${index}`;
parameters[paramName] = value as string | number;
query += `:${paramName}`;
}

if (index + 1 !== Object.entries(data).length) {
query += ', ';
}
});

query += ')';
}

await this.feedbackRepository
.createQueryBuilder('feedbacks')
.update('feedbacks')
.set({
data: () => {
if (Object.keys(data).length === 0) {
return 'data';
}
let query = `JSON_SET(IFNULL(feedbacks.data,'{}'), `;
for (const [index, fieldKey] of Object.entries(Object.keys(data))) {
query += `'$.${fieldKey}',
${
Array.isArray(data[fieldKey]) ?
data[fieldKey].length === 0 ?
'JSON_ARRAY()'
: 'JSON_ARRAY("' + data[fieldKey].join('","') + '")'
: `:${fieldKey}`
}`;

if (parseInt(index) + 1 !== Object.entries(data).length) {
query += ',';
}
}
query += `)`;

return query;
},
data: () => query,
updatedAt: () => `'${DateTime.utc().toFormat('yyyy-MM-dd HH:mm:ss')}'`,
})
.where('id = :feedbackId', { feedbackId })
.setParameters(data)
.setParameters(parameters)
.execute();
}

Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/domains/admin/project/ai/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ export class AIService {
fields: FieldEntity[],
isAutoProcess = false,
): Promise<boolean> {
feedback = structuredClone(feedback);

const integration = await this.aiIntegrationsRepo.findOne({
where: {
project: {
Expand Down
4 changes: 2 additions & 2 deletions apps/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"test:e2e": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.54.2",
"@playwright/test": "^1.55.0",
"axios": "^1.11.0",
"mysql2": "^3.14.3"
"mysql2": "^3.14.4"
}
}
Loading