SonamuContext๋ API ์์ฒญ ์ฒ๋ฆฌ ์ค์ ํ์ํ ์ ๋ณด๋ค์ ๋ด๊ณ ์๋ ๊ฐ์ฒด์
๋๋ค.
Context ๊ฐ์
Request ์ ๋ณด
HTTP ์์ฒญ ๋ฐ์ดํฐํค๋, ๋ฐ๋, ์ฟผ๋ฆฌ
Reply ๊ฐ์ฒด
HTTP ์๋ต ์ ์ด์ํ ์ฝ๋, ํค๋
User ์ ๋ณด
์ธ์ฆ๋ ์ฌ์ฉ์ID, ๊ถํ, ์ญํ
ํ์ฅ ๊ฐ๋ฅ
์ปค์คํ
ํ๋ ์ถ๊ฐํ๋ก์ ํธ๋ณ ๋ฐ์ดํฐ
SonamuContext ๊ตฌ์กฐ
๊ธฐ๋ณธ ๊ตฌ์กฐ
interface SonamuContext {
// HTTP Request ๊ฐ์ฒด
request: FastifyRequest;
// HTTP Reply ๊ฐ์ฒด
reply: FastifyReply;
// ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด (์ต์
)
user?: {
id: number;
email: string;
username: string;
role: string;
// ํ๋ก์ ํธ๋ณ ์ถ๊ฐ ํ๋
};
// ์ปค์คํ
ํ๋ (ํ๋ก์ ํธ๋ณ๋ก ํ์ฅ)
[key: string]: any;
}
Sonamu๋ ๋ด๋ถ์ ์ผ๋ก Fastify๋ฅผ ์ฌ์ฉํ๋ฏ๋ก, request์ reply๋ Fastify์ ๊ฐ์ฒด์
๋๋ค.
Context ๊ตฌ์ฑ ์์
1. Request
HTTP ์์ฒญ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ต๋๋ค.
class UserModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async list(): Promise<User[]> {
const context = Sonamu.getContext();
// Request ์ ๋ณด ์ ๊ทผ
const headers = context.request.headers;
const query = context.request.query;
const body = context.request.body;
const params = context.request.params;
const ip = context.request.ip;
const hostname = context.request.hostname;
const url = context.request.url;
const method = context.request.method;
console.log("Request from IP:", ip);
console.log("User-Agent:", headers["user-agent"]);
// ...
}
}
์ฃผ์ ์์ฑ:
headers: HTTP ํค๋ (๊ฐ์ฒด)
query: URL ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ (๊ฐ์ฒด)
body: ์์ฒญ ๋ฐ๋ (POST/PUT ๋ฑ)
params: URL ๊ฒฝ๋ก ํ๋ผ๋ฏธํฐ
ip: ํด๋ผ์ด์ธํธ IP ์ฃผ์
hostname: ํธ์คํธ๋ช
url: ์์ฒญ URL
method: HTTP ๋ฉ์๋
2. Reply
HTTP ์๋ต์ ์ ์ดํ ์ ์์ต๋๋ค.
class UserModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: CreateUserParams): Promise<{ userId: number }> {
const context = Sonamu.getContext();
// ์๋ต ํค๋ ์ค์
context.reply.header("X-Custom-Header", "value");
// ์๋ต ์ํ ์ฝ๋ ์ค์
context.reply.status(201); // Created
// ์ฟ ํค ์ค์
context.reply.cookie("session_id", "abc123", {
httpOnly: true,
secure: true,
maxAge: 3600000, // 1์๊ฐ
});
// User ์์ฑ
const wdb = this.getPuri("w");
const [userId] = await wdb
.table("users")
.insert(params)
.returning({ id: "id" });
return { userId: userId.id };
}
}
์ฃผ์ ๋ฉ์๋:
status(code): ์ํ ์ฝ๋ ์ค์
header(name, value): ์๋ต ํค๋ ์ค์
cookie(name, value, options): ์ฟ ํค ์ค์
redirect(url): ๋ฆฌ๋ค์ด๋ ํธ
send(data): ์๋ต ์ ์ก (์ผ๋ฐ์ ์ผ๋ก ์๋)
3. User
์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ต๋๋ค.
interface ContextUser {
id: number;
email: string;
username: string;
role: "admin" | "manager" | "normal";
// ํ๋ก์ ํธ๋ณ ์ถ๊ฐ ํ๋
}
class PostModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: PostSaveParams): Promise<{ postId: number }> {
const context = Sonamu.getContext();
// ์ธ์ฆ ํ์ธ
if (!context.user) {
throw new Error("Authentication required");
}
// ๊ถํ ํ์ธ
if (context.user.role !== "admin" && context.user.role !== "manager") {
throw new Error("Insufficient permissions");
}
const wdb = this.getPuri("w");
// ํ์ฌ ์ฌ์ฉ์๋ฅผ ์์ฑ์๋ก ์ค์
const [post] = await wdb
.table("posts")
.insert({
...params,
user_id: context.user.id,
author: context.user.username,
})
.returning({ id: "id" });
return { postId: post.id };
}
}
context.user๋ ์ธ์ฆ ๋ฏธ๋ค์จ์ด์ ์ํด ์ค์ ๋ฉ๋๋ค.
์ธ์ฆ์ด ํ์ํ API์์๋ ํญ์ context.user์ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํด์ผ ํฉ๋๋ค.
์ค์ ์ฌ์ฉ ์์
์ธ์ฆ ํ์ธ
class UserModel extends BaseModelClass {
@api({ httpMethod: "PUT" })
async updateProfile(params: ProfileParams): Promise<void> {
const context = Sonamu.getContext();
if (!context.user) {
context.reply.status(401);
throw new Error("Unauthorized");
}
const wdb = this.getPuri("w");
await wdb
.table("users")
.where("id", context.user.id)
.update({
bio: params.bio,
avatar_url: params.avatarUrl,
});
}
}
๊ถํ ํ์ธ
class UserModel extends BaseModelClass {
@api({ httpMethod: "DELETE" })
async remove(userId: number): Promise<void> {
const context = Sonamu.getContext();
// ์ธ์ฆ ํ์ธ
if (!context.user) {
context.reply.status(401);
throw new Error("Unauthorized");
}
// ๊ถํ ํ์ธ
if (context.user.role !== "admin") {
context.reply.status(403);
throw new Error("Forbidden: Admin only");
}
// ์๊ธฐ ์์ ์ ์ญ์ ๋ถ๊ฐ
if (context.user.id === userId) {
context.reply.status(400);
throw new Error("Cannot delete yourself");
}
const wdb = this.getPuri("w");
await wdb.table("users").where("id", userId).delete();
}
}
class OrderModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: OrderParams): Promise<{ orderId: number }> {
const context = Sonamu.getContext();
// ์์ฒญ ๋ก๊น
console.log({
timestamp: new Date(),
userId: context.user?.id,
ip: context.request.ip,
userAgent: context.request.headers["user-agent"],
endpoint: context.request.url,
method: context.request.method,
});
// ์ฃผ๋ฌธ ์์ฑ
const wdb = this.getPuri("w");
const [order] = await wdb
.table("orders")
.insert({
...params,
user_id: context.user!.id,
ip_address: context.request.ip,
})
.returning({ id: "id" });
return { orderId: order.id };
}
}
์ปค์คํ
ํค๋
class FileModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async download(fileId: number): Promise<Buffer> {
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
const file = await rdb
.table("files")
.where("id", fileId)
.first();
if (!file) {
throw new Error("File not found");
}
// ํ์ผ ๋ค์ด๋ก๋ ํค๋ ์ค์
context.reply.header("Content-Type", file.mime_type);
context.reply.header(
"Content-Disposition",
`attachment; filename="${file.filename}"`
);
context.reply.header("Content-Length", file.size.toString());
// ํ์ผ ๋ด์ฉ ๋ฐํ
return Buffer.from(file.content);
}
}
์ฟ ํค ์ฒ๋ฆฌ
class AuthModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async login(params: LoginParams): Promise<{ token: string }> {
const context = Sonamu.getContext();
const rdb = this.getPuri("r");
// ์ฌ์ฉ์ ์ธ์ฆ
const user = await rdb
.table("users")
.where("email", params.email)
.first();
if (!user || user.password !== params.password) {
context.reply.status(401);
throw new Error("Invalid credentials");
}
// JWT ํ ํฐ ์์ฑ
const jwt = require("jsonwebtoken");
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: "24h" }
);
// ์ฟ ํค์ ํ ํฐ ์ ์ฅ
context.reply.cookie("auth_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 86400000, // 24์๊ฐ
sameSite: "strict",
});
return { token };
}
@api({ httpMethod: "POST" })
async logout(): Promise<{ message: string }> {
const context = Sonamu.getContext();
// ์ฟ ํค ์ญ์
context.reply.clearCookie("auth_token");
return { message: "Logged out successfully" };
}
}
IP ๊ธฐ๋ฐ ์ ํ
class ApiModel extends BaseModelClass {
private readonly ALLOWED_IPS = [
"127.0.0.1",
"192.168.1.0/24",
// ...
];
@api({ httpMethod: "POST" })
async adminAction(params: AdminParams): Promise<void> {
const context = Sonamu.getContext();
// IP ํ์ธ
const clientIp = context.request.ip;
if (!this.isAllowedIp(clientIp)) {
context.reply.status(403);
throw new Error("Access denied from this IP");
}
// ๊ด๋ฆฌ์ ์์
์ํ
// ...
}
private isAllowedIp(ip: string): boolean {
// IP ์ฒดํฌ ๋ก์ง
return this.ALLOWED_IPS.includes(ip);
}
}
Rate Limiting ์ ๋ณด
class ApiModel extends BaseModelClass {
@api({ httpMethod: "GET" })
async list(): Promise<any[]> {
const context = Sonamu.getContext();
// Rate limit ์ ๋ณด๋ฅผ ์๋ต ํค๋์ ์ถ๊ฐ
context.reply.header("X-RateLimit-Limit", "100");
context.reply.header("X-RateLimit-Remaining", "95");
context.reply.header("X-RateLimit-Reset", Date.now() + 3600000);
const rdb = this.getPuri("r");
return rdb.table("items").select("*");
}
}
Context ์ฌ์ฉ ํจํด
์ฃผ์์ฌํญ
Context ์ฌ์ฉ ์ ์ฃผ์์ฌํญ:
- API ๋ฉ์๋ ๋ด์์๋ง ์ ๊ทผ ๊ฐ๋ฅ
context.user๋ ์ธ์ฆ ํ์๋ง ์กด์ฌ
- ๋น๋๊ธฐ ์์
์ค์๋ ๋์ผํ Context ์ ์ง
- Context ์์ ์ ์ ์คํ๊ฒ
- ์๋ต ํค๋๋ ๋ฐ์ดํฐ ์ ์ก ์ ์ ์ค์
ํํ ์ค์
// โ ์๋ชป๋จ: Context ์์ด user ์ ๊ทผ ์๋
class UserModel extends BaseModelClass {
private currentUserId?: number;
@api({ httpMethod: "POST" })
async create(params: UserParams): Promise<void> {
// Context๋ฅผ ๊ฐ์ ธ์ค์ง ์์
if (this.currentUserId) { // โ ์๋ ์ ํจ
// ...
}
}
}
// โ ์๋ชป๋จ: user ์กด์ฌ ํ์ธ ์์ด ์ฌ์ฉ
class PostModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: PostParams): Promise<void> {
const context = Sonamu.getContext();
// context.user๊ฐ undefined์ผ ์ ์์
const userId = context.user.id; // โ ์๋ฌ ๋ฐ์ ๊ฐ๋ฅ
}
}
// โ
์ฌ๋ฐ๋ฆ: Context ๊ฐ์ ธ์ค๊ณ user ํ์ธ
class PostModel extends BaseModelClass {
@api({ httpMethod: "POST" })
async create(params: PostParams): Promise<void> {
const context = Sonamu.getContext();
if (!context.user) {
throw new Error("Authentication required");
}
const userId = context.user.id; // โ ์์
// ...
}
}
๋ค์ ๋จ๊ณ