0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Vite×React×Nest.js×PostgreSQL】セッションがブラウザに返ってこないトラブルの対処方法

Posted at

前回の記事に保続する形で、今回の記事を書いていきます。

NestJS 側ではsessionが保存できており、ログイン成功のレスポンスも返っている。
→ しかしNext(Vite)側でaxios.get(adminSessionUrl, { withCredentials: true })を送っても
セッション情報が読み取れず「未ログインです」が返ってしまう、という現象が起きていました。

(原因)
CORS(および Cookie の SameSite/secure 設定)でした。
つまり、
ログイン時に Set-Cookie がブラウザに保存されていない
または 保存されても cross-origin だと送信されていない

ので、auth/check にアクセスしてもsessionが読み込めず
req.session.user undefined → 「未ログインです」と返されます。

Backendのmain.ts修正

express-session Cookie設定を見直しました。

src/main.ts
app.use(
  session({
    secret: 'your-secret',
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: false,          // ← ローカルなら false にする
      sameSite: 'lax',        // ← 重要! none は https でしか動かない
    },
  }),
);

全体のコードはこちら⇩

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import session from "express-session";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // ★ CORS(セッション対応)
  app.enableCors({
    origin: 'http://localhost:5173', // ← React の URL
    credentials: true,                // ← Cookie を送受信
  });

  // ★ session ミドルウェア
  app.use(
    session({
      secret: 'nest-app',
      resave: false,
      saveUninitialized: false,
      cookie: {
        maxAge: 30 * 1000, // ← 30秒
        httpOnly: true,
        sameSite: 'lax',
        secure:false,
      },
      rolling: true,
    }),
  );

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

❗もし sameSite: 'none' + secure: true にしていたら Cookie は ローカルでは絶対送信されません

→ Chrome が完全に拒否します。

関連コード

修正する箇所ないですが、上記のクラスファイルとやり取りするクラスを載せておきます。

Nest.js

src/adminuser/adminuser.controller.ts
import { Controller, Post, Body, UseInterceptors,Req,Get } from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { User } from 'src/models/user.entity';
import { UserService } from 'src/models/user.service';
import type { Request } from 'express';//セッションライブラリを追加
import { MailService } from 'src/mail/mail.service';//追加
import * as crypto from "crypto"//追加

//画面側に返却するレスポンス型の定義
interface UserResponse{
    status:number,
    message:string,
    error?:string
}

interface TypeUserLoginFormData{
  userName:string;
  userEmail:string;
  userPassword:string
}

interface UserResponseWithEmail{
    status:number,
    message:string,
    error?:string,
    email:string,
}

@Controller('adminuser')
export class AdminuserController {
  //コンストラクタ
  constructor(private readonly userService:UserService,private readonly mailService: MailService){}

  /**
   * ユーザ登録する処理
   * @param body(any) リクエストパラメータ
   * @returns response(UserResponse)
   */
  @Post('register')
  @UseInterceptors(AnyFilesInterceptor())
  async registerAdminUser(@Body() body: any):Promise<UserResponse>{
    console.log("ブラウザから受け取ったデータは:", body);

    // User Entityのインスタンスをnewする
    const adminUser = new User();
    adminUser.setName(body.user_name);
    adminUser.setEmail(body.user_email);
    adminUser.setPassword(body.user_password);   // ← 絶対にこの形式

    adminUser.setAdminCheck(body.admin_user === "true");
    // userserviceクラスのcreateOrUpdate関数の返却値(型:UserResponse)を代入
    const response:UserResponse = await this.userService.createOrUpdate(adminUser);
    return response;
  }

  /**
   * メールアドレスから一意のユーザ情報を取得する処理
   * @param body:string
   * @returns response:USer
   */
  @Post('getoneuser')
  @UseInterceptors(AnyFilesInterceptor())
  async getOneUserFromEmail(@Body('email') email:string):Promise<User | UserResponse>{
    console.log("Post送信で受け取ったEmailアドレスは:",email);
    return this.userService.findOneUserFromEmail(email);
  }

  /**
   * (セッション対応)ログイン画面でメールアドレスから一意のユーザ情報を取得する処理
   * @param body 
   * @returns 
   */
  @Post('login')
  //@UseInterceptors(AnyFilesInterceptor())
  async loginUser(@Req() req: Request,@Body() body: TypeUserLoginFormData):Promise<User | UserResponse | UserResponseWithEmail>{
    console.log("ログイン画面から受けとったデータは:",body);

    const userOrError = await this.userService.findOneUserUsingPassword(body.userEmail,body.userPassword);
    
    if(!isUser(userOrError)){
      return {
        status:userOrError.status || 404,
        message: userOrError.message || 'ユーザーが取得できません / パスワード不一致',
      }
    }
    
    // ★ セッションにログイン情報を保存する
    req.session.user = {
      id:userOrError.user_id,
      email:userOrError.user_email,
      name:userOrError.user_name,
    }
    console.log("セッション保存:", req.session.user);
    return {
      status:201,
      message:"ログイン成功",
      email:userOrError.user_email
    };
  }
  /**
   * セッションチェック API
   * ログイン状態かどうか確認する
   */
  @Get('auth/check')
  async checkSession(@Req() req: Request){
    if(req.session.user){
      console.log("auth/check ユーザ名は:", req.session.user);
      return {
        status:200,
        message: 'ログイン中',
        user:req.session.user,
      }
    }else{
      console.log("未ログイン")
      return {
        status:401,
        message:'未ログインです',
      }
    }
  }

  /**
   * ログアウト API
   * セッション破棄してログイン状態を解除
   */
  @Post('logout')
  async logout(@Req() req: Request){
    return new Promise((resolve)=>{
      req.session.destroy((err)=>{
        if(err){
          console.log('セッション破棄エラー:', err);
          resolve({
            status:500,
            message: 'ログアウトできませんでした',
          });
        }else{
          resolve({
            status:200,
            message: 'ログアウト成功',
          })
        }
      })
    });
  }

  @Post('resetpassword')
  async resetPassword(@Body('email') email: string){
    console.log("パスワード再設定リクエスト:", email);
    // 対象ユーザを取得
    const user = await this.userService.findOneUserFromEmail(email);
    console.log("サービスクラスから受け取ったユーザ情報は:",user);
    if(!isUser(user)){
      return {
        status:404,
        message:"該当するユーザが存在しません。",
      };
    }
    // 仮パスワード生成
    const tempPassword = crypto.randomBytes(4).toString('hex'); // 8桁程度の仮PW
    console.log("生成された仮パスワード:", tempPassword);
    // 仮パスワードをハッシュ化して保存
    (user as any).user_password = tempPassword; // ★ UserService 側でハッシュ化される想定
    await this.userService.updateUserInfoUsingEmail(email,tempPassword);
    console.log("162行目");
    // メール送信
    await this.mailService.sendTemporaryPassword(email,tempPassword);
    return {
      status:201,
      message:"仮パスワードを送信しました。",
    }
  }

}

function isUser(obj: User | UserResponse): obj is User {
  return (obj as User).user_id !== undefined;
}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?