UnauthorizedExceptionμ μΈμ¦μ΄ νμνκ±°λ κΆνμ΄ λΆμ‘±ν κ²½μ° μ¬μ©νλ μμΈμ
λλ€. HTTP 401 μν μ½λλ₯Ό λ°ννλ©°, λ‘κ·ΈμΈ νμ, μΈμ
λ§λ£, κΆν λΆμ‘± λ±μ μν©μ μ¬μ©λ©λλ€.
κΈ°λ³Έ μ¬μ©λ²
class UnauthorizedException extends SoException {
constructor(message: LocalizedString, payload?: unknown);
}
@api()
async getMyProfile(ctx: Context) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
return this.findById(ctx.user.id);
}
μ€μ© μμ
κΈ°λ³Έ μΈμ¦ 체ν¬
@api()
async updateMyProfile(
ctx: Context,
name: string,
bio: string
) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
return this.update(ctx.user.id, { name, bio });
}
Guardsλ₯Ό μ¬μ©ν μΈμ¦ (κΆμ₯)
κ°λ¨ν λ‘κ·ΈμΈ μ²΄ν¬λ Guardsλ₯Ό μ¬μ©νλ κ²μ΄ λ κΉλν©λλ€:// Guards μ¬μ© (κΆμ₯)
@api({ guards: ["user"] })
async getMyData(ctx: Context) {
// ctx.userκ° λ³΄μ₯λ¨
return this.findById(ctx.user!.id);
}
// sonamu.config.tsμμ Guard μ²λ¦¬
export default {
server: {
apiConfig: {
guardHandler: (guard, request, api) => {
if (guard === "user" && !request.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
return true;
}
}
}
} satisfies SonamuConfig;
리μμ€ μμ κΆ κ²μ¦
@api()
async deletePost(ctx: Context, postId: number) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
const post = await this.findById(postId);
if (!post) {
throw new NotFoundException("κ²μλ¬Όμ μ°Ύμ μ μμ΅λλ€");
}
// λ³ΈμΈμ΄ μμ±ν κ²μλ¬Όλ§ μμ κ°λ₯
if (post.authorId !== ctx.user.id) {
throw new UnauthorizedException(
"λ³ΈμΈμ΄ μμ±ν κ²μλ¬Όλ§ μμ ν μ μμ΅λλ€",
{ postId, authorId: post.authorId }
);
}
return this.delete(postId);
}
μν κΈ°λ° κΆν κ²μ¦
@api()
async deleteUser(ctx: Context, userId: number) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
// κ΄λ¦¬μ κΆν 체ν¬
if (!ctx.user.isAdmin) {
throw new UnauthorizedException(
"κ΄λ¦¬μλ§ μ¬μ©μλ₯Ό μμ ν μ μμ΅λλ€",
{ requiredRole: "admin", currentRole: ctx.user.role }
);
}
// μκΈ° μμ μ μμ λΆκ°
if (ctx.user.id === userId) {
throw new BadRequestException("μκΈ° μμ μ μμ ν μ μμ΅λλ€");
}
return this.delete(userId);
}
λ³΅ν© κΆν κ²μ¦
@api()
async publishPost(ctx: Context, postId: number) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
const post = await this.findById(postId);
if (!post) {
throw new NotFoundException("κ²μλ¬Όμ μ°Ύμ μ μμ΅λλ€");
}
// μμ±μμ΄κ±°λ νΈμ§μ κΆνμ΄ μμ΄μΌ ν¨
const isAuthor = post.authorId === ctx.user.id;
const isEditor = ctx.user.role === "editor" || ctx.user.role === "admin";
if (!isAuthor && !isEditor) {
throw new UnauthorizedException(
"κ²μλ¬Όμ λ°νν κΆνμ΄ μμ΅λλ€",
{
postId,
authorId: post.authorId,
currentUserId: ctx.user.id,
currentRole: ctx.user.role,
requiredCondition: "author or editor/admin role"
}
);
}
return this.update(postId, { status: "published" });
}
μ‘°μ§/ν κΆν κ²μ¦
@api()
async addTeamMember(
ctx: Context,
teamId: number,
newMemberId: number
) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
const team = await TeamModel.findById(teamId);
if (!team) {
throw new NotFoundException("νμ μ°Ύμ μ μμ΅λλ€");
}
// ν κ΄λ¦¬μλ§ λ©€λ² μΆκ° κ°λ₯
const membership = await TeamMemberModel.findOne({
teamId,
userId: ctx.user.id
});
if (!membership) {
throw new UnauthorizedException(
"ν λ©€λ²κ° μλλλ€",
{ teamId }
);
}
if (membership.role !== "admin" && membership.role !== "owner") {
throw new UnauthorizedException(
"ν κ΄λ¦¬μλ§ λ©€λ²λ₯Ό μΆκ°ν μ μμ΅λλ€",
{
teamId,
currentRole: membership.role,
requiredRole: "admin or owner"
}
);
}
return TeamMemberModel.create({
teamId,
userId: newMemberId,
role: "member"
});
}
API ν€ μΈμ¦
@api()
async getApiData(ctx: Context) {
const apiKey = ctx.headers["x-api-key"];
if (!apiKey) {
throw new UnauthorizedException(
"API ν€κ° νμν©λλ€",
{ header: "x-api-key" }
);
}
const validKey = await ApiKeyModel.findByKey(apiKey);
if (!validKey) {
throw new UnauthorizedException(
"μ ν¨νμ§ μμ API ν€μ
λλ€"
);
}
if (validKey.expiresAt && validKey.expiresAt < new Date()) {
throw new UnauthorizedException(
"λ§λ£λ API ν€μ
λλ€",
{ expiresAt: validKey.expiresAt }
);
}
if (!validKey.isActive) {
throw new UnauthorizedException(
"λΉνμ±νλ API ν€μ
λλ€",
{ keyId: validKey.id }
);
}
// API λ°μ΄ν° λ°ν
return this.getDataForApiKey(validKey.id);
}
μκ° κΈ°λ° μ κ·Ό μ μ΄
@api()
async accessRestrictedResource(ctx: Context, resourceId: number) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
const subscription = await SubscriptionModel.findByUserId(ctx.user.id);
if (!subscription) {
throw new UnauthorizedException(
"ꡬλ
μ΄ νμν 리μμ€μ
λλ€",
{ resourceId }
);
}
const now = new Date();
// ꡬλ
λ§λ£ 체ν¬
if (subscription.expiresAt < now) {
throw new UnauthorizedException(
"ꡬλ
μ΄ λ§λ£λμμ΅λλ€",
{
expiresAt: subscription.expiresAt,
renewUrl: "/subscription/renew"
}
);
}
// ꡬλ
νλλ³ μ κ·Ό μ ν
const resource = await ResourceModel.findById(resourceId);
if (resource.requiredPlan === "premium" && subscription.plan === "basic") {
throw new UnauthorizedException(
"ν리미μ νλμ΄ νμν 리μμ€μ
λλ€",
{
requiredPlan: "premium",
currentPlan: "basic",
upgradeUrl: "/subscription/upgrade"
}
);
}
return resource;
}
IP κΈ°λ° μ κ·Ό μ μ΄
@api()
async adminOnlyEndpoint(ctx: Context) {
if (!ctx.user?.isAdmin) {
throw new UnauthorizedException("κ΄λ¦¬μ κΆνμ΄ νμν©λλ€");
}
// νΉμ IP λμμμλ§ μ κ·Ό νμ©
const allowedIPs = ["192.168.1.0/24", "10.0.0.0/8"];
const clientIP = ctx.request.ip;
if (!this.isIPAllowed(clientIP, allowedIPs)) {
throw new UnauthorizedException(
"νμ©λμ§ μμ IP μ£Όμμ
λλ€",
{
clientIP,
allowedRanges: allowedIPs
}
);
}
return this.getAdminData();
}
private isIPAllowed(ip: string, allowedRanges: string[]): boolean {
// IP λμ μ²΄ν¬ λ‘μ§
return true; // μ€μ ꡬν νμ
}
Guardsμ μλ μ²΄ν¬ λΉκ΅
Guards μ¬μ© (κΆμ₯)
// sonamu.config.ts
guardHandler: (guard, request, api) => {
if (guard === "user" && !request.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
if (guard === "admin" && !request.user?.isAdmin) {
throw new UnauthorizedException("κ΄λ¦¬μ κΆνμ΄ νμν©λλ€");
}
return true;
}
// API λ©μλ
@api({ guards: ["user"] })
async simpleUserEndpoint(ctx: Context) {
// μΈμ¦ 체ν¬κ° μλμΌλ‘ μλ£λ¨
return this.getData(ctx.user!.id);
}
@api({ guards: ["admin"] })
async adminEndpoint(ctx: Context) {
// κ΄λ¦¬μ κΆν 체ν¬κ° μλμΌλ‘ μλ£λ¨
return this.getAdminData();
}
μλ μ²΄ν¬ (볡μ‘ν λ‘μ§)
@api()
async complexAuthEndpoint(ctx: Context, postId: number) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
const post = await PostModel.findById(postId);
// 볡μ‘ν κΆν λ‘μ§: μμ±μμ΄κ±°λ, νΈμ§μμ΄κ±°λ, κ°μ ν λ©€λ²
const isAuthor = post.authorId === ctx.user.id;
const isEditor = ctx.user.role === "editor";
const isSameTeam = await TeamModel.areInSameTeam(
ctx.user.id,
post.authorId
);
if (!isAuthor && !isEditor && !isSameTeam) {
throw new UnauthorizedException("μ κ·Ό κΆνμ΄ μμ΅λλ€");
}
return post;
}
payload νμ© ν¨ν΄
λ‘κ·ΈμΈ μ λ
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€", {
loginUrl: "/auth/login",
returnUrl: ctx.request.url,
});
κΆν μ κ·Έλ μ΄λ μ λ
throw new UnauthorizedException("ν리미μ κΈ°λ₯μ
λλ€", {
requiredPlan: "premium",
currentPlan: "basic",
upgradeUrl: "/subscription/upgrade",
features: ["κ³ κΈ λΆμ", "무μ ν νλ‘μ νΈ", "μ°μ μ§μ"],
});
λ§λ£ μ 보
throw new UnauthorizedException("μΈμ
μ΄ λ§λ£λμμ΅λλ€", {
expiredAt: session.expiresAt,
refreshUrl: "/auth/refresh",
});
ν΄λΌμ΄μΈνΈ μλ΅ μμ
κΈ°λ³Έ μλ΅
{
"statusCode": 401,
"message": "λ‘κ·ΈμΈμ΄ νμν©λλ€"
}
payload ν¬ν¨ μλ΅
{
"statusCode": 401,
"message": "ν리미μ νλμ΄ νμν 리μμ€μ
λλ€",
"payload": {
"requiredPlan": "premium",
"currentPlan": "basic",
"upgradeUrl": "/subscription/upgrade"
}
}
401 vs 403
- 401 Unauthorized: μΈμ¦ μμ²΄κ° μ λμκ±°λ κΆνμ΄ λΆμ‘±ν κ²½μ°
- 403 Forbidden: μΈμ¦μ λμμ§λ§ ν΄λΉ 리μμ€μ μ κ·Όν κΆνμ΄ λͺ μμ μΌλ‘ μλ κ²½μ°
UnauthorizedException νλλ‘ λ λ€ μ²λ¦¬νμ§λ§, νμνλ€λ©΄ 컀μ€ν
ForbiddenExceptionμ λ§λ€ μ μμ΅λλ€.
// 컀μ€ν
403 μμΈ
export class ForbiddenException extends SoException {
constructor(
public message = "Forbidden",
public payload?: unknown,
) {
super(403, message, payload);
}
}
// μ¬μ©
@api()
async deleteUser(ctx: Context, userId: number) {
if (!ctx.user) {
throw new UnauthorizedException("λ‘κ·ΈμΈμ΄ νμν©λλ€");
}
if (!ctx.user.isAdmin) {
throw new ForbiddenException(
"μ΄ μμ
μ μνν κΆνμ΄ μμ΅λλ€",
{ requiredRole: "admin" }
);
}
return this.delete(userId);
}