tRPC Authorization 미들웨어 구현

This commit is contained in:
static
2025-12-25 16:50:41 +09:00
parent 7779910949
commit 640e12d2c3
4 changed files with 109 additions and 38 deletions

View File

@@ -4,7 +4,7 @@ import { authenticate, AuthenticationError } from "$lib/server/modules/auth";
export const authenticateMiddleware: Handle = async ({ event, resolve }) => {
const { pathname, search } = event.url;
if (pathname === "/api/auth/login") {
if (pathname === "/api/auth/login" || pathname.startsWith("/trpc")) {
return await resolve(event);
}

View File

@@ -11,10 +11,17 @@ interface Session {
clientId?: number;
}
interface ClientSession extends Session {
export interface ClientSession extends Session {
clientId: number;
}
export type SessionPermission =
| "any"
| "notClient"
| "anyClient"
| "pendingClient"
| "activeClient";
export class AuthenticationError extends Error {
constructor(
public status: 400 | 401,
@@ -25,6 +32,16 @@ export class AuthenticationError extends Error {
}
}
export class AuthorizationError extends Error {
constructor(
public status: 403 | 500,
message: string,
) {
super(message);
this.name = "AuthorizationError";
}
}
export const startSession = async (userId: number, ip: string, userAgent: string) => {
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
await createSession(userId, sessionId, ip, userAgent);
@@ -52,34 +69,12 @@ export const authenticate = async (sessionIdSigned: string, ip: string, userAgen
}
};
export async function authorize(locals: App.Locals, requiredPermission: "any"): Promise<Session>;
export async function authorize(
export const authorizeInternal = async (
locals: App.Locals,
requiredPermission: "notClient",
): Promise<Session>;
export async function authorize(
locals: App.Locals,
requiredPermission: "anyClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "pendingClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "activeClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: "any" | "notClient" | "anyClient" | "pendingClient" | "activeClient",
): Promise<Session> {
requiredPermission: SessionPermission,
): Promise<Session> => {
if (!locals.session) {
error(500, "Unauthenticated");
throw new AuthorizationError(500, "Unauthenticated");
}
const { id: sessionId, userId, clientId } = locals.session;
@@ -89,39 +84,63 @@ export async function authorize(
break;
case "notClient":
if (clientId) {
error(403, "Forbidden");
throw new AuthorizationError(403, "Forbidden");
}
break;
case "anyClient":
if (!clientId) {
error(403, "Forbidden");
throw new AuthorizationError(403, "Forbidden");
}
break;
case "pendingClient": {
if (!clientId) {
error(403, "Forbidden");
throw new AuthorizationError(403, "Forbidden");
}
const userClient = await getUserClient(userId, clientId);
if (!userClient) {
error(500, "Invalid session id");
throw new AuthorizationError(500, "Invalid session id");
} else if (userClient.state !== "pending") {
error(403, "Forbidden");
throw new AuthorizationError(403, "Forbidden");
}
break;
}
case "activeClient": {
if (!clientId) {
error(403, "Forbidden");
throw new AuthorizationError(403, "Forbidden");
}
const userClient = await getUserClient(userId, clientId);
if (!userClient) {
error(500, "Invalid session id");
throw new AuthorizationError(500, "Invalid session id");
} else if (userClient.state !== "active") {
error(403, "Forbidden");
throw new AuthorizationError(403, "Forbidden");
}
break;
}
}
return { sessionId, userId, clientId };
};
export async function authorize(
locals: App.Locals,
requiredPermission: "any" | "notClient",
): Promise<Session>;
export async function authorize(
locals: App.Locals,
requiredPermission: "anyClient" | "pendingClient" | "activeClient",
): Promise<ClientSession>;
export async function authorize(
locals: App.Locals,
requiredPermission: SessionPermission,
): Promise<Session> {
try {
return await authorizeInternal(locals, requiredPermission);
} catch (e) {
if (e instanceof AuthorizationError) {
error(e.status, e.message);
}
throw e;
}
}

View File

@@ -1,9 +1,25 @@
import type { RequestEvent } from "@sveltejs/kit";
import { initTRPC } from "@trpc/server";
import { initTRPC, TRPCError } from "@trpc/server";
import { authorizeMiddleware, authorizeClientMiddleware } from "./middlewares/authorize";
export const createContext = (event: RequestEvent) => event;
const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create();
export const t = initTRPC.context<Awaited<ReturnType<typeof createContext>>>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
const authedProcedure = publicProcedure.use(async ({ ctx, next }) => {
if (!ctx.locals.session) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next();
});
export const roleProcedure = {
any: authedProcedure.use(authorizeMiddleware("any")),
notClient: authedProcedure.use(authorizeMiddleware("notClient")),
anyClient: authedProcedure.use(authorizeClientMiddleware("anyClient")),
pendingClient: authedProcedure.use(authorizeClientMiddleware("pendingClient")),
activeClient: authedProcedure.use(authorizeClientMiddleware("activeClient")),
};

View File

@@ -0,0 +1,36 @@
import { TRPCError } from "@trpc/server";
import {
AuthorizationError,
authorizeInternal,
type ClientSession,
type SessionPermission,
} from "$lib/server/modules/auth";
import { t } from "../init.server";
const authorize = async (locals: App.Locals, requiredPermission: SessionPermission) => {
try {
return await authorizeInternal(locals, requiredPermission);
} catch (e) {
if (e instanceof AuthorizationError) {
throw new TRPCError({
code: e.status === 403 ? "FORBIDDEN" : "INTERNAL_SERVER_ERROR",
message: e.message,
});
}
throw e;
}
};
export const authorizeMiddleware = (requiredPermission: "any" | "notClient") =>
t.middleware(async ({ ctx, next }) => {
const session = await authorize(ctx.locals, requiredPermission);
return next({ ctx: { session } });
});
export const authorizeClientMiddleware = (
requiredPermission: "anyClient" | "pendingClient" | "activeClient",
) =>
t.middleware(async ({ ctx, next }) => {
const session = (await authorize(ctx.locals, requiredPermission)) as ClientSession;
return next({ ctx: { session } });
});