backend/src/auth/auth.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthController } from './auth.controller';

describe('AuthController', () => {
  // let controller: AuthController;

  beforeEach(async () => {
    // const module: TestingModule = await Test.createTestingModule({
    //   controllers: [AuthController],
    // }).compile();
    // controller = module.get<AuthController>(AuthController);
  });

  it('should be defined', () => {
    // expect(controller).toBeDefined();
  });
});

backend/src/auth/auth.controller.ts
import {
  BadRequestException,
  Controller,
  Get,
  Logger,
  Post,
  Query,
  Redirect,
  Req,
  Res,
} from '@nestjs/common';
import axios from 'axios';
import { Request, Response } from 'express';
import { UserService } from 'src/user/user.service';
import { AuthService } from './auth.service';
import { ConfigService } from '@nestjs/config';
import { ApiTags } from '@nestjs/swagger';
import * as domain from "domain";

@ApiTags('Auth')
@Controller('api/v1/auth')
export class AuthController {
  private readonly origin: string;
  private readonly client_id: string;
  private readonly redirect_uri: string;

  private readonly logger = new Logger(AuthController.name);

  private readonly accessTokenPath = '/api/v1';
  private readonly refreshTokenPath = '/api/v1/auth/redirect';

  constructor(
      private readonly config: ConfigService,
      private readonly userService: UserService,
      private readonly authService: AuthService,
  ) {
    this.origin = this.config.get<string>('ORIGIN');
    this.client_id = this.config.get<string>('REST_API');

    this.redirect_uri = `${this.origin}/api/v1/auth/redirect`;
  }

  @Get('authorize')
  @Redirect()
  authorize(@Query('scope') scope: string) {
    const scopeParam = scope ? `&scope=${scope}` : '';

    return {
      url: `https://kauth.kakao.com/oauth/authorize?client_id=${this.client_id}&redirect_uri=${this.redirect_uri}&response_type=code${scopeParam}`,
    };
  }

  @Get('redirect')
  async redirect(@Query('code') code: string, @Res() res: Response) {
    const data = {
      grant_type: 'authorization_code',
      client_id: this.client_id,
      redirect_uri: this.redirect_uri,
      code: code,
    };

    try {
      const accessTokenResponse = await axios.post(
          '<https://kauth.kakao.com/oauth/token>',
          data,
          {
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded',
            },
          },
      );

      const kakaoUserInfoResponse = await axios.post(
          '<https://kapi.kakao.com/v2/user/me>',
          {},
          {
            headers: {
              Authorization: 'Bearer ' + accessTokenResponse.data.access_token,
              'Content-Type': 'application/x-www-form-urlencoded',
            },
          },
      );

      const kakaoId = kakaoUserInfoResponse.data.id;
      const nickname = kakaoUserInfoResponse.data.properties.nickname;

      let user = await this.userService.getUserByKakaoId(kakaoId);
      if (!user) {
        user = await this.userService.createUserWithKakaoIdAndUsername(
            kakaoId,
            nickname,
        );
      }

      const {accessToken, refreshToken} = await this.authService.createTokens(
          user.id,
      );

      this.setTokens(
          res,
          accessToken,
          refreshToken,
          'here-there-fe.vercel.app',
          '<https://here-there-fe.vercel.app/boards>',
      );
    } catch {
      throw new BadRequestException();
    }
  }

  @Get('redirect/refresh')
  async refresh(@Req() req: Request, @Res() res: Response) {
    const {accessToken, refreshToken} = await this.authService.refreshTokens(
        req.cookies['refreshToken'],
    );

    this.setTokens(res, accessToken, refreshToken, 'here-there-fe.vercel.app');
  }

  @Post('logout')
  async logout(@Res() res: Response) {
    res.clearCookie('accessToken', {path: this.accessTokenPath});
    res.clearCookie('refreshToken', {path: this.refreshTokenPath});

    res.sendStatus(204);
  }

  // 카카오 로그인 테스트 용도, 삭제 예정
  @Get()
  getTestHtml(@Res() res: Response): void {
    res.header('Content-Type', 'text/html');
    res.send(`
            <html>
                <body>
                    <a href="/api/v1/auth/authorize">
                        <img src="//k.kakaocdn.net/14/dn/btqCn0WEmI3/nijroPfbpCa4at5EIsjyf0/o.jpg" width="222"/>
                    </a>
                </body>
            </html>
        `);
  }

  private setTokens(
      res: Response,
      accessToken: string,
      refreshToken: string,
      domain: string,
      redirectUrl?: string,
  ) {
    // 쿠키 설정 로그 추가
    this.logger.log(`Setting accessToken cookie: ${accessToken}`);
    this.logger.log(`Setting refreshToken cookie: ${refreshToken}`);
    this.logger.log(`Cookie domain: ${domain}`);
    this.logger.log(`Redirect URL: ${redirectUrl}`);

    
    res.cookie('accessToken', accessToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'none',
      //domain,
      path: this.accessTokenPath,
    });

    res.cookie('refreshToken', refreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: 'none',
      //domain,
      path: this.refreshTokenPath,
    });

    this.logger.log(`Tokens issued - Access Token: ${accessToken}, Refresh Token: ${refreshToken}`);

    if (redirectUrl) {
      res.redirect(302, redirectUrl);
    } else {
      res.sendStatus(204);
    }
  }
}

backend/src/auth/auth.decorator.ts
import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const Token = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const token = request.token;
    console.log('Decorator token:', token);

    return data ? token?.[data] : token;
  },
);

backend/src/auth/auth.guard.spec.ts
import { AuthGuard } from './auth.guard';

describe('AuthGuard', () => {
  it('should be defined', () => {
    expect(new AuthGuard(null, null)).toBeDefined();
  });
});

backend/src/auth/auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
      private readonly jwtService: JwtService,
      private readonly configService: ConfigService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromCookie(request);
    if (!token) {
      console.log('토큰이 제공되지 않았습니다.');
      throw new UnauthorizedException('토큰이 제공되지 않았습니다.');
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      //console.log('토큰 페이로드:', payload);
      request['token'] = payload;
    } catch (err) {
      console.log('유효하지 않은 토큰:', err);
      throw new UnauthorizedException('유효하지 않은 토큰');
    }
    return true;
  }

  private extractTokenFromCookie(request: Request): string | undefined {
    return request.cookies['accessToken'];
  }
}
backend/src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UserModule } from '../user/user.module';
import { AuthGuard } from './auth.guard';

@Module({
  imports: [
    ConfigModule,
    JwtModule.registerAsync({
      global: true,
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: {
          expiresIn: configService.get<string>('JWT_EXPIRATION_TIME'),
        },
      }),
    }),
    UserModule,
  ],
  controllers: [AuthController],
  providers: [AuthService, AuthGuard],
  exports: [AuthGuard, JwtModule],
})
export class AuthModule {}

backend/src/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';

describe('AuthService', () => {
  let service: AuthService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [AuthService],
    }).compile();

    service = module.get<AuthService>(AuthService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

backend/src/auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
  ) {}

  private async generateTokens(payload: any) {
    const refreshTokenExpirationTime = this.configService.get<string>(
      'REFRESH_TOKEN_EXPIRATION_TIME',
    );

    const accessToken = await this.jwtService.signAsync(payload);
    const refreshToken = refreshTokenExpirationTime
      ? await this.jwtService.signAsync(payload, {
          expiresIn: refreshTokenExpirationTime,
        })
      : undefined;

    return { accessToken, refreshToken };
  }

  async createTokens(
    id: number,
  ): Promise<{ accessToken: string; refreshToken: string }> {
    const payload = { sub: id };
    return this.generateTokens(payload);
  }

  async refreshTokens(refreshToken: string) {
    const payload = await this.jwtService.verifyAsync(refreshToken);
    const newPayload = { sub: payload['sub'] };
    return this.generateTokens(newPayload);
  }
}