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

[BE] 사용자 공유 상태 변경 API 및 공유 링크 조회 API 추가 #194

Merged
merged 8 commits into from
Nov 30, 2023
12 changes: 0 additions & 12 deletions packages/server/src/app.controller.ts

This file was deleted.

4 changes: 0 additions & 4 deletions packages/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { BoardModule } from './board/board.module';
import { TypeOrmModule } from '@nestjs/typeorm';
Expand All @@ -17,7 +15,5 @@ import { StarModule } from './star/star.module';
MongooseModule.forRoot(mongooseConfig.uri),
StarModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
8 changes: 0 additions & 8 deletions packages/server/src/app.service.ts

This file was deleted.

38 changes: 35 additions & 3 deletions packages/server/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
UnauthorizedException,
Param,
NotFoundException,
BadRequestException,
Patch,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { SignUpUserDto } from './dto/signup-user.dto';
Expand All @@ -22,7 +24,7 @@ import { Response } from 'express';
import { ApiTags } from '@nestjs/swagger';
import { JwtEnum } from './enums/jwt.enum';
import { CookieAuthGuard } from './cookie-auth.guard';
import { UserEnum } from './enums/user.enum';
import { UserEnum, UserShareStatus } from './enums/user.enum';
import { SignUpSwaggerDecorator } from './decorators/swagger/sign-up-swagger.decorator';
import { SignInSwaggerDecorator } from './decorators/swagger/sign-in-swagger.decorator';
import { SignOutSwaggerDecorator } from './decorators/swagger/sign-out-swagger.decorator';
Expand All @@ -31,6 +33,12 @@ import { IsAvailableNicknameSwaggerDecorator } from './decorators/swagger/is-ava
import { SignInWithOAuthSwaggerDecorator } from './decorators/swagger/sign-in-with-oauth-swagger.decorator';
import { SignUpWithOAuthSwaggerDecorator } from './decorators/swagger/sign-up-with-oauth-swagger.decorator';
import { OAuthCallbackSwaggerDecorator } from './decorators/swagger/oauth-callback-swagger.decorator';
import { SearchUserSwaggerDecorator } from './decorators/swagger/search-user-swagger.decorator';
import { GetUser } from './decorators/get-user.decorator';
import { UserDataDto } from './dto/user-data.dto';
import { StatusValidationPipe } from './pipes/StatusValidationPipe';
import { ChangeStatusSwaggerDecorator } from './decorators/swagger/change-status-swagger.decorator';
import { GetShareLinkSwaggerDecorator } from './decorators/swagger/get-share-link-swagger.decorator';

@Controller('auth')
@ApiTags('인증/인가 API')
Expand Down Expand Up @@ -71,8 +79,11 @@ export class AuthController {
@Get('signout')
@UseGuards(CookieAuthGuard)
@SignOutSwaggerDecorator()
async signOut(@Res({ passthrough: true }) res: Response, @Req() req) {
await this.authService.signOut(req.user);
async signOut(
@Res({ passthrough: true }) res: Response,
@GetUser() userData: UserDataDto,
) {
await this.authService.signOut(userData);
res.clearCookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME, {
path: '/',
httpOnly: true,
Expand Down Expand Up @@ -184,7 +195,28 @@ export class AuthController {
}

@Get('search')
@SearchUserSwaggerDecorator()
searchUser(@Query('nickname') nickname: string) {
if (!nickname) {
throw new BadRequestException('검색할 닉네임을 입력해주세요.');
}
return this.authService.searchUser(nickname);
}

@Patch('status')
@UseGuards(CookieAuthGuard)
@ChangeStatusSwaggerDecorator()
changeStatus(
@GetUser() userData: UserDataDto,
@Body('status', StatusValidationPipe) status: UserShareStatus,
) {
return this.authService.changeStatus(userData, status);
}

@Get('sharelink')
@UseGuards(CookieAuthGuard)
@GetShareLinkSwaggerDecorator()
getShareLink(@GetUser() userData: UserDataDto) {
return this.authService.getShareLink(userData);
}
}
3 changes: 2 additions & 1 deletion packages/server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import { JwtModule } from '@nestjs/jwt';
import { jwtConfig } from '../config/jwt.config';
import { RedisRepository } from './redis.repository';
import { CookieAuthGuard } from './cookie-auth.guard';
import { ShareLink } from './entities/share_link.entity';

@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register(jwtConfig),
TypeOrmModule.forFeature([User]),
TypeOrmModule.forFeature([User, ShareLink]),
],
controllers: [AuthController],
providers: [AuthService, CookieAuthGuard, RedisRepository],
Expand Down
49 changes: 47 additions & 2 deletions packages/server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,24 @@ import * as bcrypt from 'bcryptjs';
import { SignInUserDto } from './dto/signin-user.dto';
import { JwtService } from '@nestjs/jwt';
import { RedisRepository } from './redis.repository';
import { UserEnum } from './enums/user.enum';
import { UserEnum, UserShareStatus } from './enums/user.enum';
import { JwtEnum } from './enums/jwt.enum';
import {
createJwt,
getOAuthAccessToken,
getOAuthUserData,
} from '../utils/auth.util';
import { v4 as uuid } from 'uuid';
import { UserDataDto } from './dto/user-data.dto';
import { ShareLink } from './entities/share_link.entity';

@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(ShareLink)
private readonly shareLinkRepository: Repository<ShareLink>,
private readonly jwtService: JwtService,
private readonly redisRepository: RedisRepository,
) {}
Expand Down Expand Up @@ -77,7 +81,7 @@ export class AuthService {
return { accessToken, refreshToken };
}

async signOut(user: Partial<User>) {
async signOut(user: UserDataDto) {
this.redisRepository.del(user.username);
}

Expand Down Expand Up @@ -200,4 +204,45 @@ export class AuthService {
.getMany();
return users;
}

async changeStatus(userData: UserDataDto, status: UserShareStatus) {
const user = await this.userRepository.findOneBy({ id: userData.userId });

if (!user) {
throw new NotFoundException('해당 유저를 찾을 수 없습니다.');
}

if (user.status === status) {
throw new BadRequestException('이미 해당 상태입니다.');
}

user.status = status;
const updatedUser = await this.userRepository.save(user);

updatedUser.password = undefined;
return updatedUser;
}

async getShareLink(userData: UserDataDto) {
if (userData.status === UserShareStatus.PRIVATE) {
qkrwogk marked this conversation as resolved.
Show resolved Hide resolved
throw new BadRequestException('비공개 상태입니다.');
}

const foundLink = await this.shareLinkRepository.findOneBy({
user: userData.userId,
});

if (foundLink) {
return foundLink;
}

const newLink = this.shareLinkRepository.create({
user: userData.userId,
link: uuid(),
});

const savedLink = await this.shareLinkRepository.save(newLink);
savedLink.user = undefined;
return savedLink;
}
}
9 changes: 5 additions & 4 deletions packages/server/src/auth/cookie-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ export class CookieAuthGuard extends AuthGuard('jwt') {

const accessToken = request.cookies['accessToken'];
try {
const { userId, username, nickname } =
const { userId, username, nickname, status } =
this.jwtService.verify(accessToken);

request.user = { userId, username, nickname };
request.user = { userId, username, nickname, status };
return true;
} catch (error) {}

const refreshToken = request.cookies['refreshToken'];
try {
const { userId, username, nickname } =
const { userId, username, nickname, status } =
this.jwtService.verify(refreshToken);
request.user = { userId, username, nickname };
request.user = { userId, username, nickname, status };
} catch (error) {
response.clearCookie(JwtEnum.ACCESS_TOKEN_COOKIE_NAME);
response.clearCookie(JwtEnum.REFRESH_TOKEN_COOKIE_NAME);
Expand All @@ -62,6 +62,7 @@ export class CookieAuthGuard extends AuthGuard('jwt') {
id: request.user.userId,
username: request.user.username,
nickname: request.user.nickname,
status: request.user.status,
},
JwtEnum.ACCESS_TOKEN_TYPE,
this.jwtService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
} from '@nestjs/swagger';

const apiOperation = {
summary: '사용자 공개 상태 변경',
description: '사용자 공개 상태를 변경합니다.',
};

const apiOkResponse = {
status: 200,
description: '공개 상태 변경 성공',
};

const apiBadRequestResponse = {
status: 400,
description:
'status값을 지정해주지 않았거나 올바르지 않은 status값 요청으로 공개 상태 변경 실패',
};

const apiNotFoundResponse = {
status: 404,
description: '사용자를 찾을 수 없어 공개 상태 변경 실패',
};

export const ChangeStatusSwaggerDecorator = () => {
return applyDecorators(
ApiOperation(apiOperation),
ApiOkResponse(apiOkResponse),
ApiBadRequestResponse(apiBadRequestResponse),
ApiNotFoundResponse(apiNotFoundResponse),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiOkResponse,
ApiOperation,
} from '@nestjs/swagger';

const apiOperation = {
summary: '공유 링크 가져오기',
description: '공유 링크를 가져옵니다.',
};

const apiOkResponse = {
status: 200,
description: '공유 링크 가져오기 성공',
};

const apiBadRequestResponse = {
status: 400,
description: '유저가 비공개 상태임',
};

export const GetShareLinkSwaggerDecorator = () => {
return applyDecorators(
ApiOperation(apiOperation),
ApiOkResponse(apiOkResponse),
ApiBadRequestResponse(apiBadRequestResponse),
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiOkResponse,
ApiOperation,
} from '@nestjs/swagger';

const apiOperation = {
summary: '닉네임 으로 유저 검색',
description:
'닉네임으로 유저를 검색합니다.\n' +
'쿼리 스트링으로 "nickname=검색할닉네임"을 넘겨주세요\n' +
'해당 검색 닉네임으로 시작하는 유저들을 반환합니다.',
};

const apiOkResponse = {
status: 200,
description: '검색 성공',
};

const apiBadRequestResponse = {
status: 400,
description: '닉네임을 입력하지 않아 검색 실패',
};

export const SearchUserSwaggerDecorator = () => {
return applyDecorators(
ApiOperation(apiOperation),
ApiOkResponse(apiOkResponse),
ApiBadRequestResponse(apiBadRequestResponse),
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { applyDecorators } from '@nestjs/common';
import {
ApiOperation,
ApiBadRequestResponse,
ApiCreatedResponse,
ApiOkResponse,
} from '@nestjs/swagger';

const apiOperation = {
summary: '로그아웃',
description: '쿠키의 토큰을 읽어 해당 회원의 로그아웃을 진행합니다.',
};

const apiCreatedResponse = {
const apiOkResponse = {
status: 200,
description: '로그아웃 성공으로 쿠키의 토큰과 Redis의 토큰 정보가 삭제됨',
};
Expand All @@ -23,7 +23,7 @@ const apiBadRequestResponse = {
export const SignOutSwaggerDecorator = () => {
return applyDecorators(
ApiOperation(apiOperation),
ApiCreatedResponse(apiCreatedResponse),
ApiOkResponse(apiOkResponse),
ApiBadRequestResponse(apiBadRequestResponse),
);
};
4 changes: 4 additions & 0 deletions packages/server/src/auth/dto/user-data.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export class UserDataDto {
@IsNotEmpty()
@IsString()
nickname: string;

@IsNotEmpty()
@IsString()
status: string;
}
24 changes: 24 additions & 0 deletions packages/server/src/auth/entities/share_link.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { User } from './user.entity';

@Entity()
export class ShareLink {
@PrimaryGeneratedColumn()
id: number;

@Column({ type: 'varchar', length: 200, nullable: false, unique: true })
link: string;

@OneToOne(() => User, (user) => user.id, {
onDelete: 'CASCADE',
eager: false,
})
@JoinColumn()
user: number;
}
Loading