mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-14 03:09:50 +00:00
✨ feat: topology, and improves handling
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import type { User } from "@minikura/db";
|
||||
import type { Elysia } from "elysia";
|
||||
import { ForbiddenError, UnauthorizedError } from "../domain/errors/base.error";
|
||||
|
||||
export const requireAuth = (app: Elysia) => {
|
||||
return app.derive((ctx: any) => {
|
||||
const { user, isSuspended } = ctx as {
|
||||
user: User | null;
|
||||
isSuspended: boolean;
|
||||
};
|
||||
if (!user) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
if (isSuspended) {
|
||||
throw new ForbiddenError("Account is suspended");
|
||||
}
|
||||
return { user };
|
||||
});
|
||||
};
|
||||
|
||||
export const requireAdmin = (app: Elysia) => {
|
||||
return app.derive((ctx: any) => {
|
||||
const { user, isSuspended } = ctx as {
|
||||
user: User | null;
|
||||
isSuspended: boolean;
|
||||
};
|
||||
if (!user) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
if (isSuspended) {
|
||||
throw new ForbiddenError("Account is suspended");
|
||||
}
|
||||
if (user.role !== "admin") {
|
||||
throw new ForbiddenError("Admin access required");
|
||||
}
|
||||
return { user };
|
||||
});
|
||||
};
|
||||
|
||||
export const requireRole = (role: string) => (app: Elysia) => {
|
||||
return app.derive((ctx: any) => {
|
||||
const { user, isSuspended } = ctx as {
|
||||
user: User | null;
|
||||
isSuspended: boolean;
|
||||
};
|
||||
if (!user) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
if (isSuspended) {
|
||||
throw new ForbiddenError("Account is suspended");
|
||||
}
|
||||
if (user.role !== role) {
|
||||
throw new ForbiddenError(`${role} access required`);
|
||||
}
|
||||
return { user };
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { isUserSuspended } from "@minikura/db";
|
||||
import { Elysia } from "elysia";
|
||||
import { auth } from "./auth";
|
||||
|
||||
async function getSessionFromHeaders(headers: Headers | Record<string, string>) {
|
||||
const headersObj =
|
||||
headers instanceof Headers ? headers : new Headers(headers as Record<string, string>);
|
||||
|
||||
return auth.api.getSession({
|
||||
headers: headersObj,
|
||||
});
|
||||
}
|
||||
|
||||
export const authPlugin = new Elysia({ name: "auth" })
|
||||
.mount(auth.handler)
|
||||
.derive({ as: "scoped" }, async ({ request }) => {
|
||||
const session = await getSessionFromHeaders(request.headers);
|
||||
|
||||
if (
|
||||
session?.user &&
|
||||
isUserSuspended(
|
||||
session.user as unknown as Pick<
|
||||
{ isSuspended: boolean; suspendedUntil: Date | null },
|
||||
"isSuspended" | "suspendedUntil"
|
||||
>
|
||||
)
|
||||
) {
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
isAuthenticated: false,
|
||||
isSuspended: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
user: session?.user || null,
|
||||
session: session?.session || null,
|
||||
isAuthenticated: Boolean(session?.user),
|
||||
isSuspended: false,
|
||||
};
|
||||
});
|
||||
|
||||
export type AuthPlugin = typeof authPlugin;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { prisma } from "@minikura/db";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||
import { admin, openAPI } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: prismaAdapter(prisma, {
|
||||
provider: "postgresql",
|
||||
usePlural: false,
|
||||
}),
|
||||
emailAndPassword: { enabled: true },
|
||||
plugins: [admin(), openAPI()],
|
||||
trustedOrigins: [process.env.WEB_URL || "http://localhost:3001"],
|
||||
session: {
|
||||
cookieCache: { enabled: true, maxAge: 60 * 5 },
|
||||
},
|
||||
basePath: "/auth",
|
||||
});
|
||||
|
||||
export type Auth = typeof auth;
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Elysia } from "elysia";
|
||||
import { DomainError } from "../domain/errors/base.error";
|
||||
import { logger } from "../infrastructure/logger";
|
||||
|
||||
export const errorHandler = (app: Elysia) => {
|
||||
return app.onError(({ error, set }) => {
|
||||
if (error instanceof DomainError) {
|
||||
logger.warn({ code: error.code, message: error.message }, "Domain error occurred");
|
||||
set.status = error.statusCode;
|
||||
return {
|
||||
success: false,
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof Error && (error.name === "ValidationError" || error.name === "ZodError")) {
|
||||
logger.warn({ err: error, message: error.message }, "Validation error");
|
||||
set.status = 400;
|
||||
return {
|
||||
success: false,
|
||||
code: "VALIDATION_ERROR",
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
logger.error({ err: error }, "Unhandled error in API request");
|
||||
|
||||
set.status = 500;
|
||||
return {
|
||||
success: false,
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "An unexpected error occurred",
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
type ErrorHandler = (code: number, value: unknown) => never;
|
||||
|
||||
export function validateBody<T extends z.ZodType>(
|
||||
schema: T,
|
||||
body: unknown,
|
||||
error: ErrorHandler
|
||||
): z.infer<T> {
|
||||
const result = schema.safeParse(body);
|
||||
|
||||
if (!result.success) {
|
||||
const firstError = result.error.issues[0];
|
||||
const message = `${firstError.path.join(".")}: ${firstError.message}`;
|
||||
throw error(400, { message });
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function zodValidate<T extends z.ZodType>(schema: T) {
|
||||
return (context: { body: unknown; error: ErrorHandler }) => {
|
||||
return validateBody(schema, context.body, context.error);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user