Skip to content
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

Merged
merged 12 commits into from
Jan 23, 2024
23 changes: 23 additions & 0 deletions migrations/1704208353171-add-modify-user-table-column.ts
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 {
Copy link
Member

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로 확인)
우선 마이그레이션 통합 작업을 진행할테니 놔두셔도 됩니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@rrgks6221
이거 클래스명 수동으로 변경하나요? 아니면 생성할때 명령어가 있는지 궁금합니다~

Copy link
Member

Choose a reason for hiding this comment

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

저는 README 에 있는 방법대로 하는데 그러면 클레스명이 파일명과 같아져요


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`);
}
}
22 changes: 22 additions & 0 deletions migrations/1704441168745-add-enum-type-in-loginType.ts
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';
Copy link
Member

Choose a reason for hiding this comment

The 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';
`);
}
}
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member

Choose a reason for hiding this comment

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

http client를 어떤걸 사용할 지 내부적으로 한번 정해야겠군요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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",
Expand Down
2 changes: 2 additions & 0 deletions src/apis/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AuthModule } from './auth/auth.module';
import { MajorModule } from './major/major.module';
import { NoticePostsModule } from './notice-posts/notice-posts.module';
import { UsersModule } from './users/users.module';
import { AuthSocialModule } from './auth/social/auth-social.module';

@Module({
imports: [
Expand All @@ -18,6 +19,7 @@ import { UsersModule } from './users/users.module';
FreePostsModule,
FreePostCommentsModule,
FreePostReplyCommentsModule,
AuthSocialModule,
],
})
export class ApiModule {}
1 change: 1 addition & 0 deletions src/apis/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { AuthService } from './services/auth.service';
],
controllers: [AuthController],
providers: [AuthService, JwtModuleOptionsFactory, JwtStrategy],
exports: [AuthService]
})
export class AuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
Expand Down
22 changes: 22 additions & 0 deletions src/apis/auth/social/auth-social.module.ts
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 { }
38 changes: 38 additions & 0 deletions src/apis/auth/social/controllers/auth-social.controller.ts
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);
}
}
94 changes: 94 additions & 0 deletions src/apis/auth/social/controllers/auth-social.swagger.ts
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,
]),
);
},
}
17 changes: 17 additions & 0 deletions src/apis/auth/social/dto/auth-registration.dto.ts
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;
}
81 changes: 81 additions & 0 deletions src/apis/auth/social/dto/auth-social.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 | null;

@ApiProperty({
description: 'email',
format: 'email',
})
@IsEmail()
@IsNullable()
Copy link
Collaborator

Choose a reason for hiding this comment

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

IsNullable 데코레이터는 null이 아닌 경우에만 유효성 검사를 진행하게 만드는 데코레이터이고 타입만 봤을땐 email, name, role은 null이 들어오면 안되는 것 같은데 이렇게 했을 때 null이 들어오면 그냥 통과될 것 같습니다.
의도하신 거라면 타입에 null도 추가해주셔야 할 것 같아용

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

role을 제외하고 우선 null허용으로 작업하고 추후에 기획이 수정되면 다시 재작업하도록 하겠습니다!

email: string | null;

@ApiProperty({
description: 'role',
enum: UserRole,
})
@IsEnum(UserRole)
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 | null;
}

export class SignInRequestBodyDto extends CheckRegistrationRequestBodyDto { }
25 changes: 25 additions & 0 deletions src/apis/auth/social/service/auth-registration.service.ts
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;
}
}
Loading