-
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
Social signup / signin #120
Changes from 11 commits
dcfc13d
1d21ec4
ab7f221
8b9b09a
fbf5764
09d7a75
ae79d09
75847dd
44a1115
4a6803d
d0194be
3e93c5f
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,23 @@ | ||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm" | ||
|
||
export class Migrations1704208353171 implements MigrationInterface { | ||
|
||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.addColumn( | ||
'user', | ||
new TableColumn({ | ||
name: 'sns_id', | ||
type: 'varchar', | ||
length: '255', | ||
isNullable: true, | ||
comment: '소셜 아이디', | ||
}), | ||
); | ||
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "name" VARCHAR(20) NULL`); | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.dropColumn('user', 'sns_id'); | ||
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "name" VARCHAR(20) NOT NULL`); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { MigrationInterface, QueryRunner } from "typeorm" | ||
|
||
export class Migrations1704441168745 implements MigrationInterface { | ||
|
||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(` | ||
ALTER TABLE user MODIFY COLUMN login_type ENUM('email', 'KAKAO', 'GOOGLE', 'NAVER') NOT NULL DEFAULT 'email'; | ||
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. email 로그인은 사용하지 않기로 했으니 없어도 되지만 현재 email 값을 사용하는 raw가 있을 수 있기 때문에 놔두시고 migration 통합 때 제가 없애도록 하겠습니다. |
||
`); | ||
await queryRunner.query(` | ||
ALTER TABLE user_history MODIFY COLUMN login_type ENUM('email', 'KAKAO', 'GOOGLE', 'NAVER') NOT NULL DEFAULT 'email'; | ||
`); | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query(` | ||
ALTER TABLE user MODIFY COLUMN login_type ENUM('email') NOT NULL DEFAULT 'email'; | ||
`); | ||
await queryRunner.query(` | ||
ALTER TABLE user_history MODIFY COLUMN login_type ENUM('email') NOT NULL DEFAULT 'email'; | ||
`); | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,7 @@ | |
"@types/express": "^4.17.17", | ||
"@types/jest": "^29.5.2", | ||
"@types/node": "^20.3.1", | ||
"@types/node-fetch": "^2.6.9", | ||
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. http client를 어떤걸 사용할 지 내부적으로 한번 정해야겠군요 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. 일단은 유지하겠습니다 |
||
"@types/passport-jwt": "^3.0.13", | ||
"@types/supertest": "^2.0.12", | ||
"@typescript-eslint/eslint-plugin": "^6.0.0", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Module } from "@nestjs/common"; | ||
import { AuthSocialController } from "./controllers/auth-social.controller"; | ||
import { AuthSocialService } from "./service/auth-social.service"; | ||
import { UsersModule } from "@src/apis/users/users.module"; | ||
import { AuthRegistrationService } from "./service/auth-registration.service"; | ||
import { AuthService } from "../services/auth.service"; | ||
import { EncryptionModule } from "@src/libs/encryption/encryption.module"; | ||
import { JwtModule } from "@nestjs/jwt"; | ||
import { JwtModuleOptionsFactory } from "../jwt/jwt-module-options.factory"; | ||
import { AuthModule } from "../auth.module"; | ||
|
||
@Module({ | ||
imports: [ | ||
UsersModule, | ||
AuthModule, | ||
], | ||
controllers: [AuthSocialController], | ||
providers: [AuthSocialService, AuthRegistrationService], | ||
exports: [AuthSocialService] | ||
}) | ||
|
||
export class AuthSocialModule { } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Body, Controller, Post } from "@nestjs/common"; | ||
import { ApiTags } from "@nestjs/swagger"; | ||
import { AuthSocialService } from "../service/auth-social.service"; | ||
import { SignInRequestBodyDto, SignUpRequestBodyDto } from "../dto/auth-social.dto"; | ||
import { CheckRegistrationRequestBodyDto } from "../dto/auth-registration.dto"; | ||
import { AuthRegistrationService } from "../service/auth-registration.service"; | ||
import { ApiAuthSocial } from "./auth-social.swagger"; | ||
|
||
/** | ||
* author: changhoon oh | ||
* @todo https://eslint.org/docs/latest/rules/no-return-await | ||
*/ | ||
@ApiTags('auth-social') | ||
@Controller('auth/social') | ||
export class AuthSocialController { | ||
constructor( | ||
private readonly authSocialService: AuthSocialService, | ||
private readonly authRegistrationService: AuthRegistrationService | ||
) { } | ||
|
||
@ApiAuthSocial.CheckRegistration({ summary: '소셜 유저 프로필 유무 조회' }) | ||
@Post('check-registration') | ||
async checkRegistration(@Body() checkRegistrationRequestBodyDto: CheckRegistrationRequestBodyDto): Promise<boolean> { | ||
return await this.authRegistrationService.isUserRegistered(checkRegistrationRequestBodyDto) | ||
} | ||
|
||
@ApiAuthSocial.SignUp({ summary: '소셜 회원가입' }) | ||
@Post('signup') | ||
async signUp(@Body() signUpRequestBodyDto: SignUpRequestBodyDto) { | ||
return await this.authSocialService.signUp(signUpRequestBodyDto); | ||
} | ||
|
||
@ApiAuthSocial.SignIn({ summary: '소셜 로그인' }) | ||
@Post('signin') | ||
async signIn(@Body() signInRequestBodyDto: SignInRequestBodyDto) { | ||
return await this.authSocialService.signIn(signInRequestBodyDto); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { ApiOperator } from "@src/types/type"; | ||
import { AuthSocialController } from "./auth-social.controller"; | ||
import { OperationObject } from "@nestjs/swagger/dist/interfaces/open-api-spec.interface"; | ||
import { HttpStatus, applyDecorators } from "@nestjs/common"; | ||
import { ApiCreatedResponse, ApiOperation } from "@nestjs/swagger"; | ||
import { DetailResponseDto } from "@src/interceptors/success-interceptor/dto/detail-response.dto"; | ||
import { UserDto } from "@src/apis/users/dto/user.dto"; | ||
import { ValidationError } from "class-validator"; | ||
import { HttpException } from "@src/http-exceptions/exceptions/http.exception"; | ||
import { COMMON_ERROR_CODE } from "@src/constants/error/common/common-error-code.constant"; | ||
import { USER_ERROR_CODE } from "@src/constants/error/users/user-error-code.constant"; | ||
import { AUTH_ERROR_CODE } from "@src/constants/error/auth/auth-error-code.constant"; | ||
|
||
export const ApiAuthSocial: ApiOperator<keyof AuthSocialController> = { | ||
CheckRegistration: ( | ||
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> & | ||
Partial<OperationObject>, | ||
): PropertyDecorator => { | ||
return applyDecorators( | ||
ApiOperation({ | ||
operationId: 'CheckRegistration', | ||
...apiOperationOptions, | ||
}), | ||
ApiCreatedResponse({ | ||
type: Boolean | ||
}), | ||
) | ||
}, | ||
SignUp: ( | ||
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> & | ||
Partial<OperationObject>, | ||
): PropertyDecorator => { | ||
return applyDecorators( | ||
ApiOperation({ | ||
operationId: 'Signup', | ||
...apiOperationOptions, | ||
}), | ||
DetailResponseDto.swaggerBuilder(HttpStatus.CREATED, 'user', UserDto), | ||
HttpException.swaggerBuilder( | ||
HttpStatus.BAD_REQUEST, | ||
[COMMON_ERROR_CODE.INVALID_REQUEST_PARAMETER], | ||
{ | ||
description: | ||
'해당 필드는 request parameter 가 잘못된 경우에만 리턴됩니다.', | ||
type: ValidationError, | ||
}, | ||
), | ||
HttpException.swaggerBuilder(HttpStatus.CONFLICT, [ | ||
USER_ERROR_CODE.ALREADY_EXIST_USER_EMAIL, | ||
USER_ERROR_CODE.ALREADY_EXIST_USER_PHONE_NUMBER, | ||
]), | ||
HttpException.swaggerBuilder(HttpStatus.INTERNAL_SERVER_ERROR, [ | ||
COMMON_ERROR_CODE.SERVER_ERROR, | ||
]), | ||
); | ||
}, | ||
SignIn: ( | ||
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> & | ||
Partial<OperationObject>, | ||
): PropertyDecorator => { | ||
return applyDecorators( | ||
ApiOperation({ | ||
operationId: 'AuthSignIn', | ||
...apiOperationOptions, | ||
}), | ||
ApiCreatedResponse({ | ||
schema: { | ||
properties: { | ||
accessToken: { | ||
description: 'access token', | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
}), | ||
HttpException.swaggerBuilder( | ||
HttpStatus.BAD_REQUEST, | ||
[ | ||
COMMON_ERROR_CODE.INVALID_REQUEST_PARAMETER, | ||
AUTH_ERROR_CODE.ACCOUNT_NOT_FOUND, | ||
AUTH_ERROR_CODE.DIFFERENT_ACCOUNT_INFORMATION, | ||
], | ||
{ | ||
description: | ||
'해당 필드는 request parameter 가 잘못된 경우에만 리턴됩니다.', | ||
type: ValidationError, | ||
}, | ||
), | ||
HttpException.swaggerBuilder(HttpStatus.INTERNAL_SERVER_ERROR, [ | ||
COMMON_ERROR_CODE.SERVER_ERROR, | ||
]), | ||
); | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { ApiProperty } from "@nestjs/swagger"; | ||
import { UserLoginType } from "@src/apis/users/constants/user.enum"; | ||
import { IsEnum, IsString } from "class-validator"; | ||
|
||
export class CheckRegistrationRequestBodyDto { | ||
@ApiProperty({ | ||
description: '로그인 타입', | ||
enum: UserLoginType, | ||
enumName: 'UserLoginType' | ||
}) | ||
@IsEnum(UserLoginType) | ||
loginType: UserLoginType; | ||
|
||
@ApiProperty({ description: 'SNS 토큰' }) | ||
@IsString() | ||
snsToken: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { ApiProperty } from "@nestjs/swagger"; | ||
import { UserGender, UserLoginType, UserRole } from "@src/apis/users/constants/user.enum"; | ||
import { PHONE_NUMBER_REGEXP } from "@src/constants/regexp.constant"; | ||
import { IsNullable } from "@src/decorators/validators/is-nullable.decorator"; | ||
import { IsEmail, IsEnum, IsInt, IsString, Length, Matches, Max, Min } from "class-validator"; | ||
import { CheckRegistrationRequestBodyDto } from "./auth-registration.dto"; | ||
import { USER_GRADE, USER_NAME_LENGTH } from "@src/apis/users/constants/user.constant"; | ||
|
||
export class SignUpRequestBodyDto extends CheckRegistrationRequestBodyDto { | ||
@ApiProperty({ | ||
description: 'name', | ||
minLength: USER_NAME_LENGTH.MIN, | ||
maxLength: USER_NAME_LENGTH.MAX, | ||
}) | ||
@Length(USER_NAME_LENGTH.MIN, USER_NAME_LENGTH.MAX) | ||
@IsNullable() | ||
name: string; | ||
|
||
@ApiProperty({ | ||
description: 'email', | ||
format: 'email', | ||
}) | ||
@IsEmail() | ||
@IsNullable() | ||
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. IsNullable 데코레이터는 null이 아닌 경우에만 유효성 검사를 진행하게 만드는 데코레이터이고 타입만 봤을땐 email, name, role은 null이 들어오면 안되는 것 같은데 이렇게 했을 때 null이 들어오면 그냥 통과될 것 같습니다. 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. role을 제외하고 우선 null허용으로 작업하고 추후에 기획이 수정되면 다시 재작업하도록 하겠습니다! |
||
email: string; | ||
|
||
@ApiProperty({ | ||
description: 'role', | ||
enum: UserRole, | ||
}) | ||
@IsEnum(UserRole) | ||
@IsNullable() | ||
role: UserRole = UserRole.Student; | ||
|
||
@ApiProperty({ | ||
description: 'phone number', | ||
example: '010-0000-0000', | ||
type: () => String, | ||
nullable: true, | ||
pattern: String(PHONE_NUMBER_REGEXP), | ||
}) | ||
@Matches(PHONE_NUMBER_REGEXP) | ||
@IsNullable() | ||
phoneNumber: string | null; | ||
|
||
@ApiProperty({ | ||
description: 'grade 0은 졸업생', | ||
type: () => Number, | ||
nullable: true, | ||
minimum: USER_GRADE.MIN, | ||
maximum: USER_GRADE.MAX, | ||
}) | ||
@Min(USER_GRADE.MIN) | ||
@Max(USER_GRADE.MAX) | ||
@IsNullable() | ||
grade: number | null; | ||
|
||
@ApiProperty({ | ||
description: 'gender', | ||
enum: UserGender, | ||
nullable: true, | ||
}) | ||
@IsEnum(UserGender) | ||
@IsNullable() | ||
gender: UserGender | null; | ||
|
||
@ApiProperty({ | ||
description: 'url 이 아닌 profile path', | ||
type: () => String, | ||
nullable: true, | ||
example: 'user_image.jpg', | ||
}) | ||
@IsString() | ||
@IsNullable() | ||
profilePath: string | null; | ||
|
||
@IsInt() | ||
@IsNullable() | ||
majorId: number; | ||
} | ||
|
||
export class SignInRequestBodyDto extends CheckRegistrationRequestBodyDto { } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { Injectable } from "@nestjs/common"; | ||
import { UsersService } from "@src/apis/users/services/users.service"; | ||
import { getSnsProfile } from "../../util/getSnsProfile"; | ||
import { CheckRegistrationRequestBodyDto } from "../dto/auth-registration.dto"; | ||
|
||
@Injectable() | ||
export class AuthRegistrationService { | ||
constructor(private readonly usersService: UsersService) { } | ||
|
||
async isUserRegistered(checkRegistrationRequestBodyDto: CheckRegistrationRequestBodyDto): Promise<boolean> { | ||
const { loginType, snsToken } = checkRegistrationRequestBodyDto; | ||
const snsProfile = await getSnsProfile(loginType, snsToken); | ||
|
||
if (snsProfile) { | ||
const user = await this.usersService.findOneBy({ | ||
loginType: checkRegistrationRequestBodyDto.loginType, | ||
snsId: snsProfile.snsId | ||
}); | ||
|
||
return !!user; | ||
} | ||
|
||
return false; | ||
} | ||
} |
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.
마이그레이션을 실행할 때 클레스명을 기준으로 raw가 쌓이기떄문에 클레스명은 의미있는 이름이 들어가는게 좋습니다.(현재까지 있는 migrations raw 확인 또는 npm run migrate:show로 확인)
우선 마이그레이션 통합 작업을 진행할테니 놔두셔도 됩니다.
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.
@rrgks6221
이거 클래스명 수동으로 변경하나요? 아니면 생성할때 명령어가 있는지 궁금합니다~
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.
저는 README 에 있는 방법대로 하는데 그러면 클레스명이 파일명과 같아져요