-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 관리자 기능 개선 및 추가 #459
feat: 관리자 기능 개선 및 추가 #459
Changes from all commits
ffa55d4
1ec8215
5d69992
3e4d3e1
37dabdd
52cc21e
7266274
fa1820b
47d78f7
90a8543
4c56926
b97c1d1
b6ca049
13bd3b9
e8480d2
efcb945
7590068
c6f2853
88e35a8
a6cc1dc
248d4d1
b2a3df2
505c401
de561e5
f4f55c5
adee23c
e47c450
a23dc5e
2dfa285
2f1e61b
c6bb957
ccc39d1
3867ee2
d2207e1
5fa2731
2d6b0a1
344fc4b
3c718e0
d2465ad
161a2fa
6e7872b
75fb598
d5fe165
30de966
eeabb97
1131de1
8ed1672
e2e38ef
961ea6d
010158e
bbf13c5
bee5b9d
d4c1f44
73084ac
428f374
1d0fc52
07d4d38
9273238
3159da8
aa7cad2
7f16514
02cfa73
4bf4f28
912bb01
7ddbca4
b97b954
b1b2ab8
499a338
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,52 @@ | ||||||||||||||||
import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||||||||||||||
|
||||||||||||||||
export class AddTrigramUser1730047331380 implements MigrationInterface { | ||||||||||||||||
name = 'AddTrigramUser1730047331380'; | ||||||||||||||||
|
||||||||||||||||
public async up(queryRunner: QueryRunner): Promise<void> { | ||||||||||||||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`); | ||||||||||||||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`); | ||||||||||||||||
|
||||||||||||||||
// Add unaccented_email column (no generated expression) | ||||||||||||||||
await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT`); | ||||||||||||||||
|
||||||||||||||||
// Populate unaccented_email initially | ||||||||||||||||
await queryRunner.query(`UPDATE "user" SET "unaccented_email" = unaccent(email)`); | ||||||||||||||||
|
||||||||||||||||
Comment on lines
+11
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Simplify by using a generated column instead of triggers. Consider using a generated column for Apply this diff to modify the migration: -await queryRunner.query(`ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT`);
-
-// Populate unaccented_email initially
-await queryRunner.query(`UPDATE "user" SET "unaccented_email" = unaccent(email)`);
-
-// Create trigger function for unaccented_email updates
-await queryRunner.query(`
- CREATE OR REPLACE FUNCTION update_unaccented_email()
- RETURNS TRIGGER AS $$
- BEGIN
- NEW.unaccented_email = unaccent(NEW.email);
- RETURN NEW;
- END;
- $$ LANGUAGE plpgsql;
-`);
-
-// Apply trigger to update unaccented_email on email change
-await queryRunner.query(`
- CREATE TRIGGER trigger_update_unaccented_email
- BEFORE INSERT OR UPDATE OF email ON "user"
- FOR EACH ROW EXECUTE FUNCTION update_unaccented_email();
-`);
+await queryRunner.query(`
+ ALTER TABLE "user" ADD COLUMN "unaccented_email" TEXT GENERATED ALWAYS AS (unaccent(email)) STORED
+`); Don't forget to adjust the -await queryRunner.query(`DROP TRIGGER trigger_update_unaccented_email ON "user"`);
-await queryRunner.query(`DROP FUNCTION update_unaccented_email`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "unaccented_email"`); 📝 Committable suggestion
Suggested change
|
||||||||||||||||
// Create trigger function for unaccented_email updates | ||||||||||||||||
await queryRunner.query(` | ||||||||||||||||
CREATE OR REPLACE FUNCTION update_unaccented_email() | ||||||||||||||||
RETURNS TRIGGER AS $$ | ||||||||||||||||
BEGIN | ||||||||||||||||
NEW.unaccented_email = unaccent(NEW.email); | ||||||||||||||||
RETURN NEW; | ||||||||||||||||
END; | ||||||||||||||||
$$ LANGUAGE plpgsql; | ||||||||||||||||
`); | ||||||||||||||||
|
||||||||||||||||
// Apply trigger to update unaccented_email on email change | ||||||||||||||||
await queryRunner.query(` | ||||||||||||||||
CREATE TRIGGER trigger_update_unaccented_email | ||||||||||||||||
BEFORE INSERT OR UPDATE OF email ON "user" | ||||||||||||||||
FOR EACH ROW EXECUTE FUNCTION update_unaccented_email(); | ||||||||||||||||
`); | ||||||||||||||||
|
||||||||||||||||
// Create index for trigram search on unaccented_email | ||||||||||||||||
await queryRunner.query( | ||||||||||||||||
`CREATE INDEX "IDX_USER_UNACCENTED_EMAIL" ON "user" USING gin (unaccented_email gin_trgm_ops)`, | ||||||||||||||||
); | ||||||||||||||||
|
||||||||||||||||
// Optional: Create index for text search if required | ||||||||||||||||
await queryRunner.query( | ||||||||||||||||
`CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (to_tsvector('simple', coalesce(email, '')))`, | ||||||||||||||||
); | ||||||||||||||||
} | ||||||||||||||||
|
||||||||||||||||
public async down(queryRunner: QueryRunner): Promise<void> { | ||||||||||||||||
await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`); | ||||||||||||||||
await queryRunner.query(`DROP INDEX "IDX_USER_UNACCENTED_EMAIL"`); | ||||||||||||||||
await queryRunner.query(`DROP TRIGGER trigger_update_unaccented_email ON "user"`); | ||||||||||||||||
await queryRunner.query(`DROP FUNCTION update_unaccented_email`); | ||||||||||||||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "unaccented_email"`); | ||||||||||||||||
} | ||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,30 @@ | ||||||||||||||||||||||||||||||||
import { MigrationInterface, QueryRunner } from 'typeorm'; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
export class AddVectorUser1730048056148 implements MigrationInterface { | ||||||||||||||||||||||||||||||||
name = 'AddVectorUser1730048056148'; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public async up(queryRunner: QueryRunner): Promise<void> { | ||||||||||||||||||||||||||||||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Add search_vector column for full-text search on email | ||||||||||||||||||||||||||||||||
await queryRunner.query( | ||||||||||||||||||||||||||||||||
`ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', coalesce(email, ''))) STORED`, | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
Comment on lines
+9
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider using a more robust text search configuration. The current implementation uses the 'simple' text search configuration, which:
Consider using 'english' or another language-specific configuration if the emails contain natural language content. - `ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('simple', coalesce(email, ''))) STORED`,
+ `ALTER TABLE "user" ADD COLUMN IF NOT EXISTS "search_vector" TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', coalesce(email, ''))) STORED`, 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
// Check and create index if not exists | ||||||||||||||||||||||||||||||||
await queryRunner.query(` | ||||||||||||||||||||||||||||||||
DO $$ | ||||||||||||||||||||||||||||||||
BEGIN | ||||||||||||||||||||||||||||||||
IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'IDX_USER_SEARCH_VECTOR') THEN | ||||||||||||||||||||||||||||||||
CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector); | ||||||||||||||||||||||||||||||||
END IF; | ||||||||||||||||||||||||||||||||
END | ||||||||||||||||||||||||||||||||
$$; | ||||||||||||||||||||||||||||||||
`); | ||||||||||||||||||||||||||||||||
Comment on lines
+15
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Simplify index creation using CREATE INDEX IF NOT EXISTS. The current DO block implementation is unnecessarily complex. PostgreSQL supports CREATE INDEX IF NOT EXISTS directly. - await queryRunner.query(`
- DO $$
- BEGIN
- IF NOT EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'IDX_USER_SEARCH_VECTOR') THEN
- CREATE INDEX "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector);
- END IF;
- END
- $$;
- `);
+ await queryRunner.query(
+ `CREATE INDEX IF NOT EXISTS "IDX_USER_SEARCH_VECTOR" ON "user" USING gin (search_vector)`,
+ ); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+6
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add transaction control to ensure atomic migration. The migration should be wrapped in a transaction to ensure all changes are applied atomically. public async up(queryRunner: QueryRunner): Promise<void> {
+ await queryRunner.startTransaction();
+ try {
// ... existing migration code ...
+ await queryRunner.commitTransaction();
+ } catch (err) {
+ await queryRunner.rollbackTransaction();
+ throw err;
+ }
}
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
public async down(queryRunner: QueryRunner): Promise<void> { | ||||||||||||||||||||||||||||||||
await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`); | ||||||||||||||||||||||||||||||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "search_vector"`); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
Comment on lines
+26
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enhance down method robustness. The current down method implementation could fail if objects don't exist and lacks transaction control. public async down(queryRunner: QueryRunner): Promise<void> {
- await queryRunner.query(`DROP INDEX "IDX_USER_SEARCH_VECTOR"`);
- await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "search_vector"`);
+ await queryRunner.startTransaction();
+ try {
+ await queryRunner.query(`DROP INDEX IF EXISTS "IDX_USER_SEARCH_VECTOR"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN IF EXISTS "search_vector"`);
+ await queryRunner.commitTransaction();
+ } catch (err) {
+ await queryRunner.rollbackTransaction();
+ throw err;
+ }
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -67,6 +67,7 @@ import { CreateThemeReqDto } from './dto/request/createThemeReq.dto'; | |||||||||||||||||||||||||
import { CreateItemReqDto } from './dto/request/createItemReq.dto'; | ||||||||||||||||||||||||||
import { ItemsResDto } from '../home/dto/response/itemsRes.dto'; | ||||||||||||||||||||||||||
import { ThemesResDto } from '../home/dto/response/themesRes.dto'; | ||||||||||||||||||||||||||
import { SummaryEssaysResDto } from '../essay/dto/response/SummaryEssaysRes.dto'; | ||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct the import path to match the file name The import statement at line 70 may have an incorrect file name in the import path. Ensure that the file name matches the actual file in the directory. If the file is named -import { SummaryEssaysResDto } from '../essay/dto/response/SummaryEssaysRes.dto';
+import { SummaryEssaysResDto } from '../essay/dto/response/SummaryEssaysResDto';
|
||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@ApiTags('Admin-auth') | ||||||||||||||||||||||||||
@Controller('admin-auth') | ||||||||||||||||||||||||||
|
@@ -436,6 +437,8 @@ export class AdminInfoController { | |||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**쿼리 파라미터:** | ||||||||||||||||||||||||||
- \`activated\`: 어드민의 활성 상태 (true 또는 false, 선택적) | ||||||||||||||||||||||||||
- \`page\`: 페이지 번호 (기본값: 1) | ||||||||||||||||||||||||||
- \`limit\`: 한 페이지에 표시할 신고 수 (기본값: 10) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**동작 과정:** | ||||||||||||||||||||||||||
1. 선택적 쿼리 파라미터 \`activated\`를 기반으로 어드민을 조회합니다. | ||||||||||||||||||||||||||
|
@@ -447,8 +450,14 @@ export class AdminInfoController { | |||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
@ApiResponse({ status: 200, type: AdminsResDto }) | ||||||||||||||||||||||||||
@ApiQuery({ name: 'activated', required: false }) | ||||||||||||||||||||||||||
async getAdmins(@Query('activated', OptionalBoolPipe) activated?: boolean) { | ||||||||||||||||||||||||||
return this.adminService.getAdmins(activated); | ||||||||||||||||||||||||||
@ApiQuery({ name: 'page', required: false }) | ||||||||||||||||||||||||||
@ApiQuery({ name: 'limit', required: false }) | ||||||||||||||||||||||||||
async getAdmins( | ||||||||||||||||||||||||||
@Query('page', new PagingParseIntPipe(1)) page: number, | ||||||||||||||||||||||||||
@Query('limit', new PagingParseIntPipe(10)) limit: number, | ||||||||||||||||||||||||||
@Query('activated', OptionalBoolPipe) activated?: boolean, | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
return this.adminService.getAdmins(page, limit, activated); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Get('inactive') | ||||||||||||||||||||||||||
|
@@ -470,6 +479,18 @@ export class AdminInfoController { | |||||||||||||||||||||||||
return this.adminService.getInactiveAdmins(); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Get('my') | ||||||||||||||||||||||||||
@ApiOperation({ | ||||||||||||||||||||||||||
summary: '어드민 본인조회', | ||||||||||||||||||||||||||
description: ` | ||||||||||||||||||||||||||
어드민 자신의 상세 정보를 조회합니다. | ||||||||||||||||||||||||||
`, | ||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
@ApiResponse({ status: 200, type: AdminResDto }) | ||||||||||||||||||||||||||
async getMyAdmin(@Req() req: ExpressRequest) { | ||||||||||||||||||||||||||
return this.adminService.getAdmin(req.user.id); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Get(':adminId') | ||||||||||||||||||||||||||
@ApiOperation({ | ||||||||||||||||||||||||||
summary: '어드민 상세조회', | ||||||||||||||||||||||||||
|
@@ -1056,25 +1077,26 @@ export class AdminManagementController { | |||||||||||||||||||||||||
return this.adminService.getUser(userId); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Get('users/search/:email') | ||||||||||||||||||||||||||
@Get('users/search/:keyword') | ||||||||||||||||||||||||||
@ApiOperation({ | ||||||||||||||||||||||||||
summary: '유저 검색 (이메일)', | ||||||||||||||||||||||||||
description: ` | ||||||||||||||||||||||||||
관리자 권한으로 이메일로 유저를 검색합니다.. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**경로 파라미터:** | ||||||||||||||||||||||||||
- \`email\`: 조회할 유저의 고유 이메일 | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**동작 과정:** | ||||||||||||||||||||||||||
1. 해당 유저의 상세 정보를 조회합니다. | ||||||||||||||||||||||||||
- \`keyword\`: 검색할 이메일 | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**주의 사항:** | ||||||||||||||||||||||||||
- 관리자 권한이 필요합니다. | ||||||||||||||||||||||||||
`, | ||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
@ApiResponse({ status: 200, type: UserDetailResDto }) | ||||||||||||||||||||||||||
async searchUser(@Param('email') email: string) { | ||||||||||||||||||||||||||
return this.adminService.searchUser(email); | ||||||||||||||||||||||||||
async searchUsers( | ||||||||||||||||||||||||||
@Param('keyword') email: string, | ||||||||||||||||||||||||||
@Query('page', new PagingParseIntPipe(1)) page: number, | ||||||||||||||||||||||||||
@Query('limit', new PagingParseIntPipe(10)) limit: number, | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
Comment on lines
+1094
to
+1098
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Rename the parameter for clarity and consistency In the async searchUsers(
- @Param('keyword') email: string,
+ @Param('keyword') keyword: string,
@Query('page', new PagingParseIntPipe(1)) page: number,
@Query('limit', new PagingParseIntPipe(10)) limit: number,
) {
- return this.adminService.searchUsers(email, page, limit);
+ return this.adminService.searchUsers(keyword, page, limit);
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||
return this.adminService.searchUsers(email, page, limit); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Put('users/:userId') | ||||||||||||||||||||||||||
|
@@ -1105,6 +1127,42 @@ export class AdminManagementController { | |||||||||||||||||||||||||
return this.adminService.updateUser(req.user.id, userId, data); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Get('essays/search/:keyword') | ||||||||||||||||||||||||||
@ApiOperation({ | ||||||||||||||||||||||||||
summary: '에세이 검색', | ||||||||||||||||||||||||||
description: ` | ||||||||||||||||||||||||||
키워드를 기반으로 에세이를 검색합니다. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**쿼리 파라미터:** | ||||||||||||||||||||||||||
- \`keyword\`: 검색할 키워드(필수) | ||||||||||||||||||||||||||
- \`page\`: 페이지 번호 (기본값: 1) | ||||||||||||||||||||||||||
- \`limit\`: 한 페이지에 보여줄 에세이 수 (기본값: 10) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**동작 과정:** | ||||||||||||||||||||||||||
1. 주어진 키워드를 제목 또는 내용에서 검색합니다. | ||||||||||||||||||||||||||
2. 검색된 결과에서 페이징 처리를 합니다. | ||||||||||||||||||||||||||
3. 결과는 제목 또는 내용에 키워드가 포함된 에세이의 슬라이스된 내용을 반환합니다. | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
**주의 사항:** | ||||||||||||||||||||||||||
- 검색 키워드는 URL 인코딩된 문자열이어야 합니다. | ||||||||||||||||||||||||||
- 응답에는 제목 또는 본문에 키워드가 포함된 에세이만 포함됩니다. | ||||||||||||||||||||||||||
`, | ||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
@ApiQuery({ name: 'keyword', required: true }) | ||||||||||||||||||||||||||
@ApiQuery({ name: 'page', required: false }) | ||||||||||||||||||||||||||
@ApiQuery({ name: 'limit', required: false }) | ||||||||||||||||||||||||||
@ApiResponse({ | ||||||||||||||||||||||||||
status: 200, | ||||||||||||||||||||||||||
type: SummaryEssaysResDto, | ||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||
async searchEssays( | ||||||||||||||||||||||||||
@Query('keyword') keyword: string, | ||||||||||||||||||||||||||
@Query('page', new PagingParseIntPipe(1)) page: number, | ||||||||||||||||||||||||||
@Query('limit', new PagingParseIntPipe(10)) limit: number, | ||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||
return this.adminService.searchEssays(keyword, page, limit); | ||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
@Get('essays') | ||||||||||||||||||||||||||
@ApiOperation({ | ||||||||||||||||||||||||||
summary: '에세이 리스트 조회', | ||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -67,6 +67,8 @@ import { CreateItemReqDto } from './dto/request/createItemReq.dto'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Item } from '../../entities/item.entity'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ItemResDto } from '../home/dto/response/itemRes.dto'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ThemeResDto } from '../home/dto/response/themeRes.dto'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { UserResDto } from '../user/dto/response/userRes.dto'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { EssayService } from '../essay/essay.service'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@Injectable() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export class AdminService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -80,6 +82,7 @@ export class AdminService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly alertService: AlertService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly geulroquisService: GeulroquisService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly geulroquisRepository: GeulroquisRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly essayService: EssayService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly nicknameService: NicknameService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly mailService: MailService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private readonly awsService: AwsService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -506,10 +509,17 @@ export class AdminService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { users: userDtos, totalPage, page, total }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async searchUser(email: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const user = await this.userRepository.findUserByEmail(email); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async searchUsers(keyword: string, page: number, limit: number) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return this.utilsService.transformToDto(UserDetailResDto, user); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const totalPage: number = Math.ceil(total / limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const usersDto = this.utilsService.transformToDto(UserResDto, users); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { usersDto, total, page, totalPage }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+512
to
+519
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix inconsistent property naming in return object. The method returns Apply this fix: - return { usersDto, total, page, totalPage };
+ return { users: usersDto, total, page, totalPage }; 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async searchEssays(keyword: string, page: number, limit: number) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return await this.essayService.searchEssays(keyword, page, limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+512
to
+522
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation for pagination parameters. The pagination parameters ( Add validation at the start of both methods: async searchUsers(keyword: string, page: number, limit: number) {
+ if (page < 1 || limit < 1 || limit > 100) {
+ throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+ }
const { users, total } = await this.userRepository.searchUsers(keyword, page, limit);
// ...
}
async searchEssays(keyword: string, page: number, limit: number) {
+ if (page < 1 || limit < 1 || limit > 100) {
+ throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+ }
return await this.essayService.searchEssays(keyword, page, limit);
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async getUser(userId: number) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -552,15 +562,7 @@ export class AdminService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { essays, total } = await this.essayRepository.findFullEssays(page, limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const totalPage: number = Math.ceil(total / limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const data = essays.map((essay) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
...essay, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
authorId: essay.author.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
storyId: essay.story?.id ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
reportCount: essay?.reports ? essay.reports.length : null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
reviewCount: essay?.createdDate ? essay.reviews.length : null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
})); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const essaysDto = this.utilsService.transformToDto(EssayInfoResDto, data); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const essaysDto = this.utilsService.transformToDto(EssayInfoResDto, essays); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { essays: essaysDto, total, page, totalPage }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+565
to
567
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation for pagination parameters in getFullEssays. Consistent with other paginated methods, add validation for pagination parameters. Add validation: async getFullEssays(page: number, limit: number) {
+ if (page < 1 || limit < 1 || limit > 100) {
+ throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+ }
const { essays, total } = await this.essayRepository.findFullEssays(page, limit);
// ...
}
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -649,11 +651,14 @@ export class AdminService { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return !admin ? null : admin; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async getAdmins(activated?: boolean) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const admins = await this.adminRepository.findAdmins(activated); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async getAdmins(page: number, limit: number, activated?: boolean) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { admins, total } = await this.adminRepository.findAdmins(activated, page, limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const totalPage: number = Math.ceil(total / limit); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const adminsDto = this.utilsService.transformToDto(AdminResDto, admins); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { admins: adminsDto }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { admins: adminsDto, total, page, totalPage }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+654
to
+661
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation for pagination parameters in getAdmins. Similar to the search methods, pagination parameters should be validated. Add validation: async getAdmins(page: number, limit: number, activated?: boolean) {
+ if (page < 1 || limit < 1 || limit > 100) {
+ throw new HttpException('Invalid pagination parameters', HttpStatus.BAD_REQUEST);
+ }
const { admins, total } = await this.adminRepository.findAdmins(activated, page, limit);
// ...
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async getAdmin(adminId: number) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,25 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { AdminResDto } from './adminRes.dto'; | ||
import { Expose } from 'class-transformer'; | ||
import { IsNumber } from 'class-validator'; | ||
|
||
export class AdminsResDto { | ||
@ApiProperty({ type: [AdminResDto] }) | ||
@Expose() | ||
admins: AdminResDto[]; | ||
|
||
@ApiProperty() | ||
@IsNumber() | ||
@Expose() | ||
page: number; | ||
|
||
@ApiProperty() | ||
@IsNumber() | ||
@Expose() | ||
total: number; | ||
|
||
@ApiProperty() | ||
@IsNumber() | ||
@Expose() | ||
totalPage: number; | ||
Comment on lines
+11
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider adding min/max validation constraints While the number validation is good, consider adding more specific constraints:
Here's how you can enhance the validation: @ApiProperty()
- @IsNumber()
+ @IsNumber()
+ @Min(1)
@Expose()
page: number;
@ApiProperty()
- @IsNumber()
+ @IsNumber()
+ @Min(0)
@Expose()
total: number;
@ApiProperty()
- @IsNumber()
+ @IsNumber()
+ @Min(0)
@Expose()
totalPage: number; Don't forget to add the import: import { IsNumber, Min } from 'class-validator'; |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sync connection pool settings between configurations.
The connection pool settings in
dataSourceOptions
are inconsistent withTypeormConfig
. This could lead to different behavior when using direct DataSource vs NestJS TypeORM module.Apply this diff to sync the settings:
Also applies to: 65-68