๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
AuthContext๋Š” Context์˜ ์ผ๋ถ€๋กœ, ์‚ฌ์šฉ์ž ์ธ์ฆ ์ƒํƒœ์™€ ์ธ์ฆ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Fastify Passport๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํƒ€์ž… ์ •์˜

type AuthContext = {
  user: PassportUser | null;
  passport: {
    login: (user: PassportUser) => Promise<void>;
    logout: () => void;
  };
};

์†์„ฑ

user

user: PassportUser | null
ํ˜„์žฌ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด์ž…๋‹ˆ๋‹ค. ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ null์ž…๋‹ˆ๋‹ค. PassportUser ํƒ€์ž…์€ @fastify/passport์—์„œ ์ œ๊ณตํ•˜๋Š” ํƒ€์ž…์œผ๋กœ, ํ”„๋กœ์ ํŠธ์—์„œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ ์˜ˆ์‹œ:
class UserModelClass extends BaseModel {
  @api()
  async getMyProfile(ctx: Context) {
    if (!ctx.user) {
      throw new UnauthorizedException("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
    }

    return {
      id: ctx.user.id,
      email: ctx.user.email,
      name: ctx.user.name
    };
  }
}

passport.login

login: (user: PassportUser) => Promise<void>
์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ , ์ดํ›„ ์š”์ฒญ์—์„œ ctx.user๋ฅผ ํ†ตํ•ด ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ ์˜ˆ์‹œ:
class AuthModelClass extends BaseModel {
  @api()
  async login(
    ctx: Context,
    email: string,
    password: string
  ) {
    // ์‚ฌ์šฉ์ž ์ธ์ฆ ๋กœ์ง
    const user = await this.validateCredentials(email, password);
    
    if (!user) {
      throw new UnauthorizedException("์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค");
    }

    // ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
    await ctx.passport.login(user);

    return { success: true, user };
  }

  private async validateCredentials(email: string, password: string) {
    // ์‹ค์ œ ์ธ์ฆ ๋กœ์ง ๊ตฌํ˜„
    const user = await this.findByEmail(email);
    if (!user) return null;
    
    const isValid = await bcrypt.compare(password, user.passwordHash);
    return isValid ? user : null;
  }
}

passport.logout

logout: () => void
ํ˜„์žฌ ์‚ฌ์šฉ์ž๋ฅผ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ ์˜ˆ์‹œ:
class AuthModelClass extends BaseModel {
  @api()
  async logout(ctx: Context) {
    ctx.passport.logout();
    return { success: true, message: "๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" };
  }
}

์„ค์ •

AuthContext๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด sonamu.config.ts์—์„œ ์ธ์ฆ ์„ค์ •์„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์„ค์ •

// sonamu.config.ts
export default {
  server: {
    plugins: {
      // ์„ธ์…˜ ํ”Œ๋Ÿฌ๊ทธ์ธ ํ•„์ˆ˜
      session: {
        secret: process.env.SESSION_SECRET || "your-secret-key-here",
        cookie: {
          maxAge: 1000 * 60 * 60 * 24 * 7, // 7์ผ
        }
      }
    },
    auth: true // ๊ธฐ๋ณธ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์‚ฌ์šฉ
  }
} satisfies SonamuConfig;

์ปค์Šคํ…€ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”

์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์„ธ์…˜์— ์ €์žฅํ•˜๊ณ  ๋ณต์›ํ•˜๋Š” ๋ฐฉ์‹์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
// sonamu.config.ts
export default {
  server: {
    plugins: {
      session: {
        secret: process.env.SESSION_SECRET || "your-secret-key-here",
      }
    },
    auth: {
      userSerializer: async (user, request) => {
        // ์„ธ์…˜์— ์ €์žฅํ•  ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ๋ฐ˜ํ™˜
        return { id: user.id };
      },
      userDeserializer: async (serialized, request) => {
        // ์„ธ์…˜์—์„œ ๋ณต์›๋œ ์ •๋ณด๋กœ ์ „์ฒด ์‚ฌ์šฉ์ž ๊ฐ์ฒด ์žฌ๊ตฌ์„ฑ
        const user = await UserModel.findById(serialized.id);
        return user;
      }
    }
  }
} satisfies SonamuConfig;

Guards๋ฅผ ํ†ตํ•œ ์ ‘๊ทผ ์ œ์–ด

API ๋ฉ”์„œ๋“œ์— ์ธ์ฆ์„ ์š”๊ตฌํ•˜๋ ค๋ฉด guards ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
class UserModelClass extends BaseModel {
  @api({ guards: ["user"] })
  async getMyData(ctx: Context) {
    // guards: ["user"]๋กœ ์ธํ•ด ctx.user๊ฐ€ ๋ณด์žฅ๋จ
    return this.findById(ctx.user!.id);
  }

  @api({ guards: ["admin"] })
  async getAllUsers(ctx: Context) {
    // guards: ["admin"]์œผ๋กœ ๊ด€๋ฆฌ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ
    return this.findAll();
  }
}
Guard ์ฒ˜๋ฆฌ ๋กœ์ง์€ guardHandler์—์„œ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:
// sonamu.config.ts
export default {
  server: {
    apiConfig: {
      guardHandler: (guard, request, api) => {
        if (guard === "user" && !request.user) {
          throw new UnauthorizedException("๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
        }
        
        if (guard === "admin" && (!request.user || !request.user.isAdmin)) {
          throw new UnauthorizedException("๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค");
        }
        
        return true;
      }
    }
  }
} satisfies SonamuConfig;

PassportUser ํƒ€์ž… ํ™•์žฅ

ํ”„๋กœ์ ํŠธ์˜ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ตฌ์กฐ์— ๋งž๊ฒŒ PassportUser ํƒ€์ž…์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
// src/types.ts
declare module "fastify" {
  interface PassportUser {
    id: number;
    email: string;
    name: string;
    isAdmin: boolean;
    createdAt: Date;
  }
}

๊ด€๋ จ ๋ฌธ์„œ