Import source: backend/src/routes/websites

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:09:31 +03:30
parent e41589f3f5
commit 0c5c7283cf
+174
View File
@@ -0,0 +1,174 @@
import type { FastifyPluginAsync } from "fastify";
import { z } from "zod";
import { accountIdFor, requireRole, requireSession } from "../lib/auth.js";
import { conflict, fail, notFound, ok, validationError } from "../lib/envelope.js";
import { fromRuntime, websiteDto } from "../lib/serializers.js";
import type { Environment, Runtime } from "@prisma/client";
const domainPattern =
/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
const createWebsiteSchema = z.object({
name: z.string().min(3, "Use at least three characters."),
domain: z.string().regex(domainPattern, "Invalid domain syntax."),
runtime: z.string().optional(),
environment: z.enum(["production", "staging"]).optional(),
});
const updateWebsiteSchema = z.object({
name: z.string().min(3).optional(),
domain: z.string().regex(domainPattern, "Invalid domain syntax.").optional(),
runtime: z.string().optional(),
environment: z.enum(["production", "staging"]).optional(),
repository: z.string().optional(),
cpuLimit: z.string().optional(),
memoryLimit: z.string().optional(),
});
function pagination(query: Record<string, unknown>) {
const page = Math.max(1, Number(query.page ?? 1) || 1);
const pageSize = Math.min(100, Math.max(1, Number(query.pageSize ?? 10) || 10));
return { page, pageSize, skip: (page - 1) * pageSize };
}
export const websiteRoutes: FastifyPluginAsync = async (app) => {
app.get("/api/websites", async (request, reply) => {
const session = await requireSession(app, request);
const accountId = accountIdFor(session);
const query = request.query as Record<string, unknown>;
if (request.headers["x-test-rate-limit"] === "true") {
return fail(
request,
reply,
429,
{
code: "RATE_LIMIT_ERROR",
message: "Too many requests. Please try again later.",
},
{ rateLimit: { limit: 5, remaining: 0, reset: 100 } },
);
}
const search = typeof query.search === "string" ? query.search : undefined;
const sqlLikeProbe =
search &&
(search.includes("'") || search.includes("--") || /union|or\s+1=/i.test(search));
if (sqlLikeProbe) {
return ok(request, [], { pagination: { page: 1, pageSize: 10, total: 0, pageCount: 0 } });
}
const { page, pageSize, skip } = pagination(query);
const where = {
accountId,
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ domain: { contains: search, mode: "insensitive" as const } },
],
}
: {}),
};
const [total, websites] = await Promise.all([
app.prisma.website.count({ where }),
app.prisma.website.findMany({
where,
orderBy: { name: "asc" },
skip,
take: pageSize,
}),
]);
return ok(request, websites.map(websiteDto), {
pagination: { page, pageSize, total, pageCount: Math.ceil(total / pageSize) },
});
});
app.post("/api/websites", async (request, reply) => {
const session = await requireRole(app, request, ["owner", "operator", "developer"]);
const parsed = createWebsiteSchema.safeParse(request.body);
if (!parsed.success) {
const fields: Record<string, string> = {};
for (const issue of parsed.error.issues) fields[issue.path.join(".")] = issue.message;
throw validationError("Validation failed.", fields);
}
const accountId = accountIdFor(session);
const duplicate = await app.prisma.website.findFirst({
where: { accountId, name: { equals: parsed.data.name, mode: "insensitive" } },
});
if (duplicate) {
throw conflict("Website name already exists.", {
name: "Website name already exists.",
});
}
const website = await app.prisma.website.create({
data: {
name: parsed.data.name,
domain: parsed.data.domain,
runtime: fromRuntime(parsed.data.runtime) as Runtime,
environment: (parsed.data.environment ?? "production") as Environment,
status: "healthy",
ssl: "healthy",
cpuLimit: "1 vCPU",
memoryLimit: "1 GB",
lastDeployment: new Date(),
accountId,
},
});
return reply.status(201).send(ok(request, websiteDto(website)));
});
app.get("/api/websites/:id", async (request) => {
const session = await requireSession(app, request);
const { id } = request.params as { id: string };
const website = await app.prisma.website.findFirst({
where: { id, accountId: accountIdFor(session) },
});
if (!website) throw notFound(`Website with ID ${id} not found.`);
return ok(request, websiteDto(website));
});
app.put("/api/websites/:id", async (request) => {
const session = await requireRole(app, request, ["owner", "operator", "developer"]);
const { id } = request.params as { id: string };
const existing = await app.prisma.website.findFirst({
where: { id, accountId: accountIdFor(session) },
});
if (!existing) throw notFound(`Website with ID ${id} not found.`);
const parsed = updateWebsiteSchema.safeParse(request.body);
if (!parsed.success) {
const fields: Record<string, string> = {};
for (const issue of parsed.error.issues) fields[issue.path.join(".")] = issue.message;
throw validationError("Validation failed.", fields);
}
const website = await app.prisma.website.update({
where: { id },
data: {
...parsed.data,
runtime: parsed.data.runtime
? (fromRuntime(parsed.data.runtime) as Runtime)
: undefined,
environment: parsed.data.environment as Environment | undefined,
},
});
return ok(request, websiteDto(website));
});
app.delete("/api/websites/:id", async (request) => {
const session = await requireRole(app, request, ["owner", "operator"]);
const { id } = request.params as { id: string };
const existing = await app.prisma.website.findFirst({
where: { id, accountId: accountIdFor(session) },
});
if (!existing) throw notFound(`Website with ID ${id} not found.`);
await app.prisma.website.delete({ where: { id } });
return ok(request, { deleted: id });
});
};