diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index 8585bce..ad8c585 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -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); } diff --git a/src/lib/server/modules/auth.ts b/src/lib/server/modules/auth.ts index d25033d..6ae3865 100644 --- a/src/lib/server/modules/auth.ts +++ b/src/lib/server/modules/auth.ts @@ -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; - -export async function authorize( +export const authorizeInternal = async ( locals: App.Locals, - requiredPermission: "notClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "anyClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "pendingClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "activeClient", -): Promise; - -export async function authorize( - locals: App.Locals, - requiredPermission: "any" | "notClient" | "anyClient" | "pendingClient" | "activeClient", -): Promise { + requiredPermission: SessionPermission, +): Promise => { 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; + +export async function authorize( + locals: App.Locals, + requiredPermission: "anyClient" | "pendingClient" | "activeClient", +): Promise; + +export async function authorize( + locals: App.Locals, + requiredPermission: SessionPermission, +): Promise { + try { + return await authorizeInternal(locals, requiredPermission); + } catch (e) { + if (e instanceof AuthorizationError) { + error(e.status, e.message); + } + throw e; + } } diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts index a6af870..15a35fa 100644 --- a/src/trpc/init.server.ts +++ b/src/trpc/init.server.ts @@ -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>>().create(); +export const t = initTRPC.context>>().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")), +}; diff --git a/src/trpc/middlewares/authorize.ts b/src/trpc/middlewares/authorize.ts new file mode 100644 index 0000000..53413b3 --- /dev/null +++ b/src/trpc/middlewares/authorize.ts @@ -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 } }); + });