๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” Passport.js๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” ์ธ์ฆ ์‹œ์Šคํ…œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์ง€์›ํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

import { defineConfig } from "sonamu";

export default defineConfig({
  server: {
    auth: true,  // ๊ธฐ๋ณธ ์ธ์ฆ ํ™œ์„ฑํ™”
    
    // ๋˜๋Š” ์ปค์Šคํ„ฐ๋งˆ์ด์ง•
    auth: {
      userSerializer: async (user, request) => {
        // ์„ธ์…˜์— ์ €์žฅํ•  ๋ฐ์ดํ„ฐ
        return { id: user.id, email: user.email };
      },
      userDeserializer: async (serialized, request) => {
        // ์„ธ์…˜์—์„œ ์‚ฌ์šฉ์ž ๋ณต์›
        return UserModel.findById(serialized.id);
      },
    },
  },
  // ...
});

auth ์„ค์ •

๊ฐ„๋‹จํ•œ ํ™œ์„ฑํ™”

ํƒ€์ž…: boolean | AuthOptions
export default defineConfig({
  server: {
    auth: true,  // ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ํ™œ์„ฑํ™”
  },
});
๋น„ํ™œ์„ฑํ™”:
export default defineConfig({
  server: {
    auth: false,  // ์ธ์ฆ ๋น„ํ™œ์„ฑํ™”
  },
});
auth: true๋กœ ์„ค์ •ํ•˜๋ฉด Sonamu์˜ ๊ธฐ๋ณธ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์ปค์Šคํ…€ ์„ค์ •

type AuthOptions = {
  userSerializer: SerializeFunction<unknown, unknown>;
  userDeserializer: DeserializeFunction<unknown, unknown>;
};

userSerializer

์‚ฌ์šฉ์ž ๊ฐ์ฒด๋ฅผ ์„ธ์…˜์— ์ €์žฅ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: (user, request) => SerializedUser | Promise<SerializedUser>
export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user, request) => {
        // ์„ธ์…˜์— ์ตœ์†Œํ•œ์˜ ์ •๋ณด๋งŒ ์ €์žฅ
        return {
          id: user.id,
          email: user.email,
        };
      },
      // ...
    },
  },
});
user: ๋กœ๊ทธ์ธ ์‹œ ์ „๋‹ฌ๋œ ์‚ฌ์šฉ์ž ๊ฐ์ฒด
request: Fastify request ๊ฐ์ฒด
๋ฐ˜ํ™˜: ์„ธ์…˜์— ์ €์žฅ๋  ์ง๋ ฌํ™”๋œ ๋ฐ์ดํ„ฐ
์„ธ์…˜ ํฌ๊ธฐ๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ํ•„์ˆ˜ ์ •๋ณด๋งŒ ์ €์žฅํ•˜์„ธ์š”. ๋ณดํ†ต id๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

userDeserializer

์„ธ์…˜์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ณต์›ํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ž…: (serialized, request) => User | Promise<User>
export default defineConfig({
  server: {
    auth: {
      userDeserializer: async (serialized, request) => {
        // DB์—์„œ ์ „์ฒด ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
        return UserModel.findById(serialized.id);
      },
      // ...
    },
  },
});
serialized: userSerializer๊ฐ€ ๋ฐ˜ํ™˜ํ•œ ๋ฐ์ดํ„ฐ
request: Fastify request ๊ฐ์ฒด
๋ฐ˜ํ™˜: ๋ณต์›๋œ ์‚ฌ์šฉ์ž ๊ฐ์ฒด
userDeserializer๋Š” ๋งค ์š”์ฒญ๋งˆ๋‹ค ํ˜ธ์ถœ๋˜๋ฏ€๋กœ ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์บ์‹ฑ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๊ธฐ๋ณธ ์˜ˆ์‹œ

ID๋งŒ ์ €์žฅ (๊ถŒ์žฅ)

import { defineConfig } from "sonamu";

export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user, _request) => {
        return user.id;  // ID๋งŒ ์ €์žฅ
      },
      
      userDeserializer: async (userId, _request) => {
        return UserModel.findById(userId);
      },
    },
  },
});

๊ธฐ๋ณธ ์ •๋ณด ์ €์žฅ

export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user, _request) => {
        return {
          id: user.id,
          email: user.email,
          role: user.role,
        };
      },
      
      userDeserializer: async (serialized, _request) => {
        return UserModel.findById(serialized.id);
      },
    },
  },
});

์บ์‹ฑ ์ถ”๊ฐ€

import { cache } from "sonamu/cache";

export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user, _request) => {
        return user.id;
      },
      
      userDeserializer: async (userId, _request) => {
        // ์‚ฌ์šฉ์ž ์ •๋ณด ์บ์‹ฑ (1๋ถ„)
        const cacheKey = `user:${userId}`;
        
        let user = await cache.get(cacheKey);
        if (!user) {
          user = await UserModel.findById(userId);
          await cache.set(cacheKey, user, { ttl: "1m" });
        }
        
        return user;
      },
    },
  },
});

๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ

์ธ์ฆ ์„ค์ • ํ›„ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ API๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ

import { api } from "sonamu";
import { login } from "sonamu/auth";
import bcrypt from "bcrypt";

export class AuthModel {
  @api({ httpMethod: "POST" })
  static async login(email: string, password: string) {
    // ์‚ฌ์šฉ์ž ์กฐํšŒ
    const user = await UserModel.findByEmail(email);
    if (!user) {
      throw new BadRequestError("Invalid credentials");
    }
    
    // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ
    const valid = await bcrypt.compare(password, user.passwordHash);
    if (!valid) {
      throw new BadRequestError("Invalid credentials");
    }
    
    // ๋กœ๊ทธ์ธ (์„ธ์…˜ ์ƒ์„ฑ)
    await login(user);
    
    return { success: true, user };
  }
}

๋กœ๊ทธ์•„์›ƒ

import { api } from "sonamu";
import { logout } from "sonamu/auth";

export class AuthModel {
  @api({ httpMethod: "POST" })
  static async logout() {
    await logout();
    return { success: true };
  }
}

ํ˜„์žฌ ์‚ฌ์šฉ์ž

import { api, getUser } from "sonamu";

export class AuthModel {
  @api()
  static async me() {
    const user = getUser();
    if (!user) {
      throw new UnauthorizedError("Not authenticated");
    }
    
    return user;
  }
}
โ†’ ์ธ์ฆ API ๊ตฌํ˜„ ๊ฐ€์ด๋“œ

์‹ค์ „ ์˜ˆ์‹œ

๊ธฐ๋ณธ ์„ค์ •

import { defineConfig } from "sonamu";

export default defineConfig({
  server: {
    plugins: {
      session: {
        secret: process.env.SESSION_SECRET!,
        salt: process.env.SESSION_SALT!,
        cookie: {
          domain: "localhost",
          path: "/",
          maxAge: 60 * 60 * 24 * 30,  // 30์ผ
        },
      },
    },
    
    auth: {
      userSerializer: async (user) => user.id,
      userDeserializer: async (userId) => {
        return UserModel.findById(userId);
      },
    },
  },
});

Role ๊ธฐ๋ฐ˜ ์ธ์ฆ

export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user) => ({
        id: user.id,
        role: user.role,
      }),
      
      userDeserializer: async (serialized) => {
        const user = await UserModel.findById(serialized.id);
        
        // Role ๋ณ€๊ฒฝ ๊ฐ์ง€
        if (user.role !== serialized.role) {
          // ์„ธ์…˜ ๊ฐฑ์‹  ํ•„์š”
          await updateSession(user);
        }
        
        return user;
      },
    },
  },
});

JWT์™€ ์„ธ์…˜ ๋ณ‘ํ–‰

import jwt from "jsonwebtoken";

export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user) => {
        // JWT ํ† ํฐ ์ƒ์„ฑ
        const token = jwt.sign(
          { userId: user.id },
          process.env.JWT_SECRET!,
          { expiresIn: "7d" }
        );
        
        return { id: user.id, token };
      },
      
      userDeserializer: async (serialized) => {
        // JWT ๊ฒ€์ฆ
        try {
          jwt.verify(serialized.token, process.env.JWT_SECRET!);
        } catch {
          throw new UnauthorizedError("Invalid token");
        }
        
        return UserModel.findById(serialized.id);
      },
    },
  },
});

๊ถŒํ•œ ์ฒดํฌ

export default defineConfig({
  server: {
    auth: {
      userSerializer: async (user) => user.id,
      
      userDeserializer: async (userId) => {
        const user = await UserModel.findById(userId);
        
        // ๋น„ํ™œ์„ฑํ™”๋œ ์‚ฌ์šฉ์ž
        if (!user.isActive) {
          throw new UnauthorizedError("Account deactivated");
        }
        
        // ๋งŒ๋ฃŒ๋œ ๊ณ„์ •
        if (user.expiresAt && user.expiresAt < new Date()) {
          throw new UnauthorizedError("Account expired");
        }
        
        return user;
      },
    },
    
    apiConfig: {
      guardHandler: (guard, request, api) => {
        const user = request.user;
        
        if (guard === "admin" && user?.role !== "admin") {
          throw new UnauthorizedError("Admin access required");
        }
        
        if (guard === "premium" && !user?.isPremium) {
          throw new UnauthorizedError("Premium subscription required");
        }
      },
    },
  },
});

Session ์„ค์ •

์ธ์ฆ์€ ์„ธ์…˜์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋ฏ€๋กœ session ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
export default defineConfig({
  server: {
    plugins: {
      session: {
        secret: process.env.SESSION_SECRET!,
        salt: process.env.SESSION_SALT!,
        cookie: {
          domain: process.env.COOKIE_DOMAIN,
          path: "/",
          maxAge: 60 * 60 * 24 * 30,  // 30์ผ
          secure: process.env.NODE_ENV === "production",
          httpOnly: true,
          sameSite: "strict",
        },
      },
    },
    
    auth: true,
  },
});
โ†’ Session ์„ค์ •

์ฃผ์˜์‚ฌํ•ญ

1. ์„ธ์…˜ ํฌ๊ธฐ

// โŒ ๋‚˜์œ ์˜ˆ: ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ ์ €์žฅ
userSerializer: async (user) => ({
  id: user.id,
  email: user.email,
  profile: user.profile,  // ํฐ ๊ฐ์ฒด
  preferences: user.preferences,
  history: user.history,
})

// โœ… ์ข‹์€ ์˜ˆ: ID๋งŒ ์ €์žฅ
userSerializer: async (user) => user.id

2. userDeserializer ์„ฑ๋Šฅ

// โŒ ๋‚˜์œ ์˜ˆ: ๋งค๋ฒˆ DB ์กฐํšŒ
userDeserializer: async (userId) => {
  return UserModel.findById(userId);  // ๋ชจ๋“  ์š”์ฒญ๋งˆ๋‹ค!
}

// โœ… ์ข‹์€ ์˜ˆ: ์บ์‹ฑ ์ถ”๊ฐ€
userDeserializer: async (userId) => {
  const cached = await cache.get(`user:${userId}`);
  if (cached) return cached;
  
  const user = await UserModel.findById(userId);
  await cache.set(`user:${userId}`, user, { ttl: "1m" });
  return user;
}

3. ์„ธ์…˜ secret

// โŒ ๋‚˜์œ ์˜ˆ: ํ•˜๋“œ์ฝ”๋”ฉ๋œ secret
session: {
  secret: "my-secret-key",
  salt: "my-salt",
}

// โœ… ์ข‹์€ ์˜ˆ: ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์‚ฌ์šฉ
session: {
  secret: process.env.SESSION_SECRET!,
  salt: process.env.SESSION_SALT!,
}

4. null ์ฒ˜๋ฆฌ

// โœ… ์‚ฌ์šฉ์ž๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ
userDeserializer: async (userId) => {
  const user = await UserModel.findById(userId);
  if (!user) {
    // ์„ธ์…˜์€ ์žˆ์ง€๋งŒ ์‚ฌ์šฉ์ž ์‚ญ์ œ๋จ
    throw new UnauthorizedError("User not found");
  }
  return user;
}

๋‹ค์Œ ๋‹จ๊ณ„

์ธ์ฆ ์„ค์ •์„ ์™„๋ฃŒํ–ˆ๋‹ค๋ฉด: