feat : 6 astra improve it
This commit is contained in:
+14
-7
@@ -1,3 +1,5 @@
|
||||
import { errorResponse } from "@/lib/security/http";
|
||||
import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { normalizeDeploymentId } from "@/lib/deployment/version";
|
||||
@@ -5,12 +7,17 @@ import { createEventStream, eventStreamHeaders } from "@/lib/events/sse";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
await connection();
|
||||
const deploymentId =
|
||||
normalizeDeploymentId(process.env.NEXT_DEPLOYMENT_ID) ?? "development";
|
||||
const channel = createEventStream({ signal: request.signal });
|
||||
channel.sendNamed("deployment", deploymentId);
|
||||
try {
|
||||
await limitRequest("stream-open", trustedClientAddress(request.headers), 60);
|
||||
const deploymentId =
|
||||
normalizeDeploymentId(process.env.NEXT_DEPLOYMENT_ID) ?? "development";
|
||||
const channel = createEventStream({ signal: request.signal });
|
||||
channel.sendNamed("deployment", deploymentId);
|
||||
|
||||
return new Response(channel.stream, {
|
||||
headers: eventStreamHeaders(),
|
||||
});
|
||||
return new Response(channel.stream, {
|
||||
headers: eventStreamHeaders(),
|
||||
});
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import { errorResponse, requireSameOrigin, withUploadSlot } from "@/lib/security/http";
|
||||
import { limitRequest } from "@/lib/security/rate-limit";
|
||||
import * as z from "zod";
|
||||
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { completePendingMedia } from "@/lib/media/repository";
|
||||
|
||||
export async function POST(
|
||||
_request: Request,
|
||||
request: Request,
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
await requireAdmin();
|
||||
const { id } = await context.params;
|
||||
if (!z.string().uuid().safeParse(id).success) {
|
||||
return Response.json({ error: "invalid-id" }, { status: 400 });
|
||||
try {
|
||||
requireSameOrigin(request);
|
||||
const admin = await requireAdmin();
|
||||
await limitRequest("media", admin.user.id, 20);
|
||||
const { id } = await context.params;
|
||||
if (!z.string().uuid().safeParse(id).success) {
|
||||
return Response.json({ error: "invalid-id" }, { status: 400 });
|
||||
}
|
||||
const result = await withUploadSlot(() => completePendingMedia(id));
|
||||
if (result.status === "ready") {
|
||||
return Response.json({
|
||||
media: {
|
||||
id: result.media.id,
|
||||
fileName: result.media.fileName,
|
||||
mimeType: result.media.mimeType,
|
||||
byteSize: result.media.byteSize,
|
||||
url: `/media/${result.media.id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
const status = result.status === "not-found" ? 404 : result.status === "missing" ? 409 : 422;
|
||||
return Response.json({ error: result.status }, { status });
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
const result = await completePendingMedia(id);
|
||||
if (result.status === "ready") {
|
||||
return Response.json({
|
||||
media: {
|
||||
id: result.media.id,
|
||||
fileName: result.media.fileName,
|
||||
mimeType: result.media.mimeType,
|
||||
byteSize: result.media.byteSize,
|
||||
url: `/media/${result.media.id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
const status = result.status === "not-found" ? 404 : result.status === "missing" ? 409 : 422;
|
||||
return Response.json({ error: result.status }, { status });
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { errorResponse, requireSameOrigin } from "@/lib/security/http";
|
||||
import { limitRequest } from "@/lib/security/rate-limit";
|
||||
import * as z from "zod";
|
||||
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { discardUnreferencedMedia } from "@/lib/media/repository";
|
||||
|
||||
export async function DELETE(
|
||||
_request: Request,
|
||||
request: Request,
|
||||
context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
await requireAdmin();
|
||||
const { id } = await context.params;
|
||||
if (!z.string().uuid().safeParse(id).success) {
|
||||
return Response.json({ error: "invalid-id" }, { status: 400 });
|
||||
try {
|
||||
requireSameOrigin(request);
|
||||
const admin = await requireAdmin();
|
||||
await limitRequest("media", admin.user.id, 20);
|
||||
const { id } = await context.params;
|
||||
if (!z.string().uuid().safeParse(id).success) {
|
||||
return Response.json({ error: "invalid-id" }, { status: 400 });
|
||||
}
|
||||
const deleted = await discardUnreferencedMedia(id);
|
||||
return deleted
|
||||
? new Response(null, { status: 204 })
|
||||
: Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 });
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
const deleted = await discardUnreferencedMedia(id);
|
||||
return deleted
|
||||
? new Response(null, { status: 204 })
|
||||
: Response.json({ error: "media-is-referenced-or-missing" }, { status: 409 });
|
||||
}
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
import { errorResponse, requireSameOrigin, readJson } from "@/lib/security/http";
|
||||
import { limitRequest } from "@/lib/security/rate-limit";
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { createPendingMedia } from "@/lib/media/repository";
|
||||
import { mediaUploadSchema } from "@/lib/media/validation";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const admin = await requireAdmin();
|
||||
let input: unknown;
|
||||
try {
|
||||
input = await request.json();
|
||||
} catch {
|
||||
return Response.json({ error: "invalid-json" }, { status: 400 });
|
||||
}
|
||||
const parsed = mediaUploadSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
return Response.json(
|
||||
{ error: "invalid-media", issues: parsed.error.issues.map((issue) => issue.message) },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const upload = await createPendingMedia(parsed.data, admin.user.id);
|
||||
return Response.json({
|
||||
mediaId: upload.id,
|
||||
uploadUrl: upload.uploadUrl,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": parsed.data.mimeType },
|
||||
expiresIn: upload.expiresIn,
|
||||
});
|
||||
} catch {
|
||||
return Response.json({ error: "storage-unavailable" }, { status: 503 });
|
||||
requireSameOrigin(request);
|
||||
const admin = await requireAdmin();
|
||||
await limitRequest("media", admin.user.id, 20);
|
||||
const input = await readJson(request);
|
||||
const parsed = mediaUploadSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
return Response.json(
|
||||
{ error: "invalid-media", issues: parsed.error.issues.map((issue) => issue.message) },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const upload = await createPendingMedia(parsed.data, admin.user.id);
|
||||
return Response.json({
|
||||
mediaId: upload.id,
|
||||
uploadUrl: upload.uploadUrl,
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": parsed.data.mimeType },
|
||||
expiresIn: upload.expiresIn,
|
||||
});
|
||||
} catch {
|
||||
return Response.json({ error: "storage-unavailable" }, { status: 503 });
|
||||
}
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { uploadPublicMedia } from "@/lib/media/repository";
|
||||
import { MAX_MEDIA_BYTES, mediaUploadSchema } from "@/lib/media/validation";
|
||||
import { boundedBody, errorResponse, HttpError, requireSameOrigin, securityLog, withUploadSlot } from "@/lib/security/http";
|
||||
import { limitRequest } from "@/lib/security/rate-limit";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const admin = await requireAdmin();
|
||||
const form = await request.formData();
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ error: "กรุณาเลือกไฟล์รูปภาพ" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
return NextResponse.json(await uploadPublicMedia(file, admin.user.id), { status: 201 });
|
||||
requireSameOrigin(request);
|
||||
const admin = await requireAdmin();
|
||||
await limitRequest("media", admin.user.id, 20);
|
||||
if (!request.headers.get("content-type")?.toLowerCase().startsWith("multipart/form-data;")) {
|
||||
throw new HttpError(415, "expected-multipart");
|
||||
}
|
||||
return await withUploadSlot(async () => {
|
||||
let form: FormData;
|
||||
try {
|
||||
form = await boundedBody(request, MAX_MEDIA_BYTES + 64 * 1024).formData();
|
||||
} catch (cause) {
|
||||
if (cause instanceof HttpError) throw cause;
|
||||
throw new HttpError(400, "invalid-form");
|
||||
}
|
||||
const file = form.get("file");
|
||||
if (!(file instanceof File)) throw new HttpError(400, "missing-image");
|
||||
if (file.size > MAX_MEDIA_BYTES) throw new HttpError(413, "image-too-large");
|
||||
if (!mediaUploadSchema.safeParse({ fileName: file.name, mimeType: file.type, byteSize: file.size }).success) {
|
||||
throw new HttpError(415, "invalid-image");
|
||||
}
|
||||
const result = await uploadPublicMedia(file, admin.user.id);
|
||||
securityLog("media-uploaded", { actorId: admin.user.id, targetId: result.id });
|
||||
return Response.json(result, { status: 201, headers: { "Cache-Control": "no-store" } });
|
||||
});
|
||||
} catch (cause) {
|
||||
return NextResponse.json(
|
||||
{ error: cause instanceof Error ? cause.message : "อัปโหลดไม่สำเร็จ" },
|
||||
{ status: 400 },
|
||||
);
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
import { toNextJsHandler } from "better-auth/next-js";
|
||||
|
||||
import { getAuth } from "@/lib/auth/server";
|
||||
import { errorResponse, HttpError, readJson, requireSameOrigin } from "@/lib/security/http";
|
||||
|
||||
const handlers = toNextJsHandler((request) => getAuth().handler(request));
|
||||
|
||||
export const GET = handlers.GET;
|
||||
export const POST = handlers.POST;
|
||||
export const PATCH = handlers.PATCH;
|
||||
export const PUT = handlers.PUT;
|
||||
export const DELETE = handlers.DELETE;
|
||||
|
||||
async function mutate(request: Request) {
|
||||
try {
|
||||
requireSameOrigin(request);
|
||||
const input = await readJson(request.clone());
|
||||
const path = new URL(request.url).pathname;
|
||||
if (["/admin/create-user", "/admin/set-user-password"].some((endpoint) => path.endsWith(endpoint))) {
|
||||
const password = input && typeof input === "object" && "password" in input ? input.password : undefined;
|
||||
const newPassword = input && typeof input === "object" && "newPassword" in input ? input.newPassword : undefined;
|
||||
const value = password ?? newPassword;
|
||||
if (typeof value !== "string" || value.length < 12 || value.length > 128) {
|
||||
throw new HttpError(400, "invalid-password-length");
|
||||
}
|
||||
}
|
||||
return await getAuth().handler(request);
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
export const POST = mutate;
|
||||
export const PATCH = mutate;
|
||||
export const PUT = mutate;
|
||||
export const DELETE = mutate;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { errorResponse } from "@/lib/security/http";
|
||||
import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import {
|
||||
@@ -6,7 +8,6 @@ import {
|
||||
} from "@/lib/auth/server";
|
||||
import {
|
||||
createRedisEventResponse,
|
||||
eventsUnavailableResponse,
|
||||
} from "@/lib/events/redis-stream";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
@@ -20,8 +21,9 @@ export async function GET(request: Request) {
|
||||
throw cause;
|
||||
}
|
||||
try {
|
||||
await limitRequest("stream-open", trustedClientAddress(request.headers), 60);
|
||||
return await createRedisEventResponse("admin", request.signal);
|
||||
} catch {
|
||||
return eventsUnavailableResponse();
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { errorResponse } from "@/lib/security/http";
|
||||
import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import {
|
||||
createRedisEventResponse,
|
||||
eventsUnavailableResponse,
|
||||
} from "@/lib/events/redis-stream";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
await connection();
|
||||
try {
|
||||
await limitRequest("stream-open", trustedClientAddress(request.headers), 60);
|
||||
return await createRedisEventResponse("directory", request.signal);
|
||||
} catch {
|
||||
return eventsUnavailableResponse();
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { errorResponse } from "@/lib/security/http";
|
||||
import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit";
|
||||
import { connection } from "next/server";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { getDb } from "@/db";
|
||||
import { guides } from "@/db/schema";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
|
||||
import {
|
||||
createRedisEventResponse,
|
||||
eventsUnavailableResponse,
|
||||
} from "@/lib/events/redis-stream";
|
||||
|
||||
const UUID_PATTERN =
|
||||
@@ -18,8 +23,14 @@ export async function GET(
|
||||
return Response.json({ error: "invalid-page-id" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
await limitRequest("stream-open", trustedClientAddress(request.headers), 60);
|
||||
const [published] = await getDb().select({ id: guides.id }).from(guides)
|
||||
.where(and(eq(guides.id, id), eq(guides.isPublic, true), isNull(guides.trashedAt))).limit(1);
|
||||
if (!published && !(await getAdminSession())) {
|
||||
return new Response(null, { status: 404, headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
return await createRedisEventResponse(`page:${id}`, request.signal);
|
||||
} catch {
|
||||
return eventsUnavailableResponse();
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,6 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.BUZZ_DEMO_MODE === "true") {
|
||||
return Response.json(
|
||||
{ status: "ready", deploymentId, checks: { demo: "ok" } },
|
||||
{ headers: NO_STORE_HEADERS },
|
||||
);
|
||||
}
|
||||
|
||||
const timeout = Number(process.env.HEALTHCHECK_TIMEOUT_MS || 2_500);
|
||||
const result = await checkReadiness(
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { errorResponse, readJson, requireSameOrigin } from "@/lib/security/http";
|
||||
import { limitRequest } from "@/lib/security/rate-limit";
|
||||
import { connection } from "next/server";
|
||||
import * as z from "zod";
|
||||
|
||||
import { AdminAuthorizationError, requireAdmin } from "@/lib/auth/server";
|
||||
import { requireAdmin } from "@/lib/auth/server";
|
||||
import { updateAdminPresence } from "@/lib/presence";
|
||||
|
||||
const inputSchema = z.strictObject({
|
||||
@@ -14,8 +16,10 @@ const inputSchema = z.strictObject({
|
||||
export async function POST(request: Request) {
|
||||
await connection();
|
||||
try {
|
||||
requireSameOrigin(request);
|
||||
const admin = await requireAdmin();
|
||||
const parsed = inputSchema.safeParse(await request.json().catch(() => null));
|
||||
await limitRequest("admin-presence", admin.session.id, 120);
|
||||
const parsed = inputSchema.safeParse(await readJson(request, 2048));
|
||||
if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 });
|
||||
return Response.json(await updateAdminPresence({
|
||||
...parsed.data,
|
||||
@@ -23,12 +27,6 @@ export async function POST(request: Request) {
|
||||
user: admin.user,
|
||||
}), { headers: { "Cache-Control": "no-store" } });
|
||||
} catch (cause) {
|
||||
if (cause instanceof AdminAuthorizationError) {
|
||||
return Response.json({ error: "unauthorized" }, { status: 401 });
|
||||
}
|
||||
return Response.json({ error: "presence-unavailable" }, {
|
||||
status: 503,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { errorResponse, readJson, requireSameOrigin } from "@/lib/security/http";
|
||||
import { limitRequest, trustedClientAddress } from "@/lib/security/rate-limit";
|
||||
import { connection } from "next/server";
|
||||
import * as z from "zod";
|
||||
|
||||
@@ -11,16 +13,15 @@ const inputSchema = z.strictObject({
|
||||
|
||||
export async function POST(request: Request) {
|
||||
await connection();
|
||||
const parsed = inputSchema.safeParse(await request.json().catch(() => null));
|
||||
if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 });
|
||||
try {
|
||||
requireSameOrigin(request);
|
||||
await limitRequest("presence", trustedClientAddress(request.headers), 600);
|
||||
const parsed = inputSchema.safeParse(await readJson(request, 1024));
|
||||
if (!parsed.success) return Response.json({ error: "invalid-presence" }, { status: 400 });
|
||||
return Response.json(await updateVisitorPresence(parsed.data), {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
} catch {
|
||||
return Response.json({ error: "presence-unavailable" }, {
|
||||
status: 503,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
} catch (cause) {
|
||||
return errorResponse(cause);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user