Skip to content

Commit f7d2175

Browse files
authored
Social signup / signin (#120)
2 parents b0c6844 + 3e93c5f commit f7d2175

20 files changed

+549
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"
2+
3+
export class Migrations1704208353171 implements MigrationInterface {
4+
5+
public async up(queryRunner: QueryRunner): Promise<void> {
6+
await queryRunner.addColumn(
7+
'user',
8+
new TableColumn({
9+
name: 'sns_id',
10+
type: 'varchar',
11+
length: '255',
12+
isNullable: true,
13+
comment: '소셜 아이디',
14+
}),
15+
);
16+
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "name" VARCHAR(20) NULL`);
17+
}
18+
19+
public async down(queryRunner: QueryRunner): Promise<void> {
20+
await queryRunner.dropColumn('user', 'sns_id');
21+
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "name" VARCHAR(20) NOT NULL`);
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm"
2+
3+
export class Migrations1704441168745 implements MigrationInterface {
4+
5+
public async up(queryRunner: QueryRunner): Promise<void> {
6+
await queryRunner.query(`
7+
ALTER TABLE user MODIFY COLUMN login_type ENUM('email', 'KAKAO', 'GOOGLE', 'NAVER') NOT NULL DEFAULT 'email';
8+
`);
9+
await queryRunner.query(`
10+
ALTER TABLE user_history MODIFY COLUMN login_type ENUM('email', 'KAKAO', 'GOOGLE', 'NAVER') NOT NULL DEFAULT 'email';
11+
`);
12+
}
13+
14+
public async down(queryRunner: QueryRunner): Promise<void> {
15+
await queryRunner.query(`
16+
ALTER TABLE user MODIFY COLUMN login_type ENUM('email') NOT NULL DEFAULT 'email';
17+
`);
18+
await queryRunner.query(`
19+
ALTER TABLE user_history MODIFY COLUMN login_type ENUM('email') NOT NULL DEFAULT 'email';
20+
`);
21+
}
22+
}

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"@types/express": "^4.17.17",
6868
"@types/jest": "^29.5.2",
6969
"@types/node": "^20.3.1",
70+
"@types/node-fetch": "^2.6.9",
7071
"@types/passport-jwt": "^3.0.13",
7172
"@types/supertest": "^2.0.12",
7273
"@typescript-eslint/eslint-plugin": "^6.0.0",

src/apis/api.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AuthModule } from './auth/auth.module';
77
import { MajorModule } from './major/major.module';
88
import { NoticePostsModule } from './notice-posts/notice-posts.module';
99
import { UsersModule } from './users/users.module';
10+
import { AuthSocialModule } from './auth/social/auth-social.module';
1011

1112
@Module({
1213
imports: [
@@ -18,6 +19,7 @@ import { UsersModule } from './users/users.module';
1819
FreePostsModule,
1920
FreePostCommentsModule,
2021
FreePostReplyCommentsModule,
22+
AuthSocialModule,
2123
],
2224
})
2325
export class ApiModule {}

src/apis/auth/auth.module.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { AuthService } from './services/auth.service';
2525
],
2626
controllers: [AuthController],
2727
providers: [AuthService, JwtModuleOptionsFactory, JwtStrategy],
28+
exports: [AuthService]
2829
})
2930
export class AuthModule implements NestModule {
3031
configure(consumer: MiddlewareConsumer) {
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Module } from "@nestjs/common";
2+
import { AuthSocialController } from "./controllers/auth-social.controller";
3+
import { AuthSocialService } from "./service/auth-social.service";
4+
import { UsersModule } from "@src/apis/users/users.module";
5+
import { AuthRegistrationService } from "./service/auth-registration.service";
6+
import { AuthService } from "../services/auth.service";
7+
import { EncryptionModule } from "@src/libs/encryption/encryption.module";
8+
import { JwtModule } from "@nestjs/jwt";
9+
import { JwtModuleOptionsFactory } from "../jwt/jwt-module-options.factory";
10+
import { AuthModule } from "../auth.module";
11+
12+
@Module({
13+
imports: [
14+
UsersModule,
15+
AuthModule,
16+
],
17+
controllers: [AuthSocialController],
18+
providers: [AuthSocialService, AuthRegistrationService],
19+
exports: [AuthSocialService]
20+
})
21+
22+
export class AuthSocialModule { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Body, Controller, Post } from "@nestjs/common";
2+
import { ApiTags } from "@nestjs/swagger";
3+
import { AuthSocialService } from "../service/auth-social.service";
4+
import { SignInRequestBodyDto, SignUpRequestBodyDto } from "../dto/auth-social.dto";
5+
import { CheckRegistrationRequestBodyDto } from "../dto/auth-registration.dto";
6+
import { AuthRegistrationService } from "../service/auth-registration.service";
7+
import { ApiAuthSocial } from "./auth-social.swagger";
8+
9+
/**
10+
* author: changhoon oh
11+
* @todo https://eslint.org/docs/latest/rules/no-return-await
12+
*/
13+
@ApiTags('auth-social')
14+
@Controller('auth/social')
15+
export class AuthSocialController {
16+
constructor(
17+
private readonly authSocialService: AuthSocialService,
18+
private readonly authRegistrationService: AuthRegistrationService
19+
) { }
20+
21+
@ApiAuthSocial.CheckRegistration({ summary: '소셜 유저 프로필 유무 조회' })
22+
@Post('check-registration')
23+
async checkRegistration(@Body() checkRegistrationRequestBodyDto: CheckRegistrationRequestBodyDto): Promise<boolean> {
24+
return await this.authRegistrationService.isUserRegistered(checkRegistrationRequestBodyDto)
25+
}
26+
27+
@ApiAuthSocial.SignUp({ summary: '소셜 회원가입' })
28+
@Post('signup')
29+
async signUp(@Body() signUpRequestBodyDto: SignUpRequestBodyDto) {
30+
return await this.authSocialService.signUp(signUpRequestBodyDto);
31+
}
32+
33+
@ApiAuthSocial.SignIn({ summary: '소셜 로그인' })
34+
@Post('signin')
35+
async signIn(@Body() signInRequestBodyDto: SignInRequestBodyDto) {
36+
return await this.authSocialService.signIn(signInRequestBodyDto);
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { ApiOperator } from "@src/types/type";
2+
import { AuthSocialController } from "./auth-social.controller";
3+
import { OperationObject } from "@nestjs/swagger/dist/interfaces/open-api-spec.interface";
4+
import { HttpStatus, applyDecorators } from "@nestjs/common";
5+
import { ApiCreatedResponse, ApiOperation } from "@nestjs/swagger";
6+
import { DetailResponseDto } from "@src/interceptors/success-interceptor/dto/detail-response.dto";
7+
import { UserDto } from "@src/apis/users/dto/user.dto";
8+
import { ValidationError } from "class-validator";
9+
import { HttpException } from "@src/http-exceptions/exceptions/http.exception";
10+
import { COMMON_ERROR_CODE } from "@src/constants/error/common/common-error-code.constant";
11+
import { USER_ERROR_CODE } from "@src/constants/error/users/user-error-code.constant";
12+
import { AUTH_ERROR_CODE } from "@src/constants/error/auth/auth-error-code.constant";
13+
14+
export const ApiAuthSocial: ApiOperator<keyof AuthSocialController> = {
15+
CheckRegistration: (
16+
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
17+
Partial<OperationObject>,
18+
): PropertyDecorator => {
19+
return applyDecorators(
20+
ApiOperation({
21+
operationId: 'CheckRegistration',
22+
...apiOperationOptions,
23+
}),
24+
ApiCreatedResponse({
25+
type: Boolean
26+
}),
27+
)
28+
},
29+
SignUp: (
30+
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
31+
Partial<OperationObject>,
32+
): PropertyDecorator => {
33+
return applyDecorators(
34+
ApiOperation({
35+
operationId: 'Signup',
36+
...apiOperationOptions,
37+
}),
38+
DetailResponseDto.swaggerBuilder(HttpStatus.CREATED, 'user', UserDto),
39+
HttpException.swaggerBuilder(
40+
HttpStatus.BAD_REQUEST,
41+
[COMMON_ERROR_CODE.INVALID_REQUEST_PARAMETER],
42+
{
43+
description:
44+
'해당 필드는 request parameter 가 잘못된 경우에만 리턴됩니다.',
45+
type: ValidationError,
46+
},
47+
),
48+
HttpException.swaggerBuilder(HttpStatus.CONFLICT, [
49+
USER_ERROR_CODE.ALREADY_EXIST_USER_EMAIL,
50+
USER_ERROR_CODE.ALREADY_EXIST_USER_PHONE_NUMBER,
51+
]),
52+
HttpException.swaggerBuilder(HttpStatus.INTERNAL_SERVER_ERROR, [
53+
COMMON_ERROR_CODE.SERVER_ERROR,
54+
]),
55+
);
56+
},
57+
SignIn: (
58+
apiOperationOptions: Required<Pick<Partial<OperationObject>, 'summary'>> &
59+
Partial<OperationObject>,
60+
): PropertyDecorator => {
61+
return applyDecorators(
62+
ApiOperation({
63+
operationId: 'AuthSignIn',
64+
...apiOperationOptions,
65+
}),
66+
ApiCreatedResponse({
67+
schema: {
68+
properties: {
69+
accessToken: {
70+
description: 'access token',
71+
type: 'string',
72+
},
73+
},
74+
},
75+
}),
76+
HttpException.swaggerBuilder(
77+
HttpStatus.BAD_REQUEST,
78+
[
79+
COMMON_ERROR_CODE.INVALID_REQUEST_PARAMETER,
80+
AUTH_ERROR_CODE.ACCOUNT_NOT_FOUND,
81+
AUTH_ERROR_CODE.DIFFERENT_ACCOUNT_INFORMATION,
82+
],
83+
{
84+
description:
85+
'해당 필드는 request parameter 가 잘못된 경우에만 리턴됩니다.',
86+
type: ValidationError,
87+
},
88+
),
89+
HttpException.swaggerBuilder(HttpStatus.INTERNAL_SERVER_ERROR, [
90+
COMMON_ERROR_CODE.SERVER_ERROR,
91+
]),
92+
);
93+
},
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
import { UserLoginType } from "@src/apis/users/constants/user.enum";
3+
import { IsEnum, IsString } from "class-validator";
4+
5+
export class CheckRegistrationRequestBodyDto {
6+
@ApiProperty({
7+
description: '로그인 타입',
8+
enum: UserLoginType,
9+
enumName: 'UserLoginType'
10+
})
11+
@IsEnum(UserLoginType)
12+
loginType: UserLoginType;
13+
14+
@ApiProperty({ description: 'SNS 토큰' })
15+
@IsString()
16+
snsToken: string;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
import { UserGender, UserLoginType, UserRole } from "@src/apis/users/constants/user.enum";
3+
import { PHONE_NUMBER_REGEXP } from "@src/constants/regexp.constant";
4+
import { IsNullable } from "@src/decorators/validators/is-nullable.decorator";
5+
import { IsEmail, IsEnum, IsInt, IsString, Length, Matches, Max, Min } from "class-validator";
6+
import { CheckRegistrationRequestBodyDto } from "./auth-registration.dto";
7+
import { USER_GRADE, USER_NAME_LENGTH } from "@src/apis/users/constants/user.constant";
8+
9+
export class SignUpRequestBodyDto extends CheckRegistrationRequestBodyDto {
10+
@ApiProperty({
11+
description: 'name',
12+
minLength: USER_NAME_LENGTH.MIN,
13+
maxLength: USER_NAME_LENGTH.MAX,
14+
})
15+
@Length(USER_NAME_LENGTH.MIN, USER_NAME_LENGTH.MAX)
16+
@IsNullable()
17+
name: string | null;
18+
19+
@ApiProperty({
20+
description: 'email',
21+
format: 'email',
22+
})
23+
@IsEmail()
24+
@IsNullable()
25+
email: string | null;
26+
27+
@ApiProperty({
28+
description: 'role',
29+
enum: UserRole,
30+
})
31+
@IsEnum(UserRole)
32+
role: UserRole = UserRole.Student;
33+
34+
@ApiProperty({
35+
description: 'phone number',
36+
example: '010-0000-0000',
37+
type: () => String,
38+
nullable: true,
39+
pattern: String(PHONE_NUMBER_REGEXP),
40+
})
41+
@Matches(PHONE_NUMBER_REGEXP)
42+
@IsNullable()
43+
phoneNumber: string | null;
44+
45+
@ApiProperty({
46+
description: 'grade 0은 졸업생',
47+
type: () => Number,
48+
nullable: true,
49+
minimum: USER_GRADE.MIN,
50+
maximum: USER_GRADE.MAX,
51+
})
52+
@Min(USER_GRADE.MIN)
53+
@Max(USER_GRADE.MAX)
54+
@IsNullable()
55+
grade: number | null;
56+
57+
@ApiProperty({
58+
description: 'gender',
59+
enum: UserGender,
60+
nullable: true,
61+
})
62+
@IsEnum(UserGender)
63+
@IsNullable()
64+
gender: UserGender | null;
65+
66+
@ApiProperty({
67+
description: 'url 이 아닌 profile path',
68+
type: () => String,
69+
nullable: true,
70+
example: 'user_image.jpg',
71+
})
72+
@IsString()
73+
@IsNullable()
74+
profilePath: string | null;
75+
76+
@IsInt()
77+
@IsNullable()
78+
majorId: number | null;
79+
}
80+
81+
export class SignInRequestBodyDto extends CheckRegistrationRequestBodyDto { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { UsersService } from "@src/apis/users/services/users.service";
3+
import { getSnsProfile } from "../../util/getSnsProfile";
4+
import { CheckRegistrationRequestBodyDto } from "../dto/auth-registration.dto";
5+
6+
@Injectable()
7+
export class AuthRegistrationService {
8+
constructor(private readonly usersService: UsersService) { }
9+
10+
async isUserRegistered(checkRegistrationRequestBodyDto: CheckRegistrationRequestBodyDto): Promise<boolean> {
11+
const { loginType, snsToken } = checkRegistrationRequestBodyDto;
12+
const snsProfile = await getSnsProfile(loginType, snsToken);
13+
14+
if (snsProfile) {
15+
const user = await this.usersService.findOneBy({
16+
loginType: checkRegistrationRequestBodyDto.loginType,
17+
snsId: snsProfile.snsId
18+
});
19+
20+
return !!user;
21+
}
22+
23+
return false;
24+
}
25+
}

0 commit comments

Comments
 (0)