first commit

This commit is contained in:
AyrisAI
2026-08-20 01:51:59 +03:00
commit 97b83c7fd4
109 changed files with 21215 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
import dns from "node:dns/promises";
import type { FastifyPluginAsync } from "fastify";
import { z } from "zod";
import { requireAuth, requireRestaurantMember } from "../lib/auth.js";
import { env } from "../env.js";
import { supabase } from "../lib/supabase.js";
const addDomainSchema = z.object({
hostname: z
.string()
.min(3)
.max(255)
.transform((val) => val.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "")),
});
const resolver = new dns.Resolver();
// Use public authoritative DNS servers to check live global records
try {
resolver.setServers(["8.8.8.8", "1.1.1.1"]);
} catch {
// Fallback to default
}
export const domainsRoutes: FastifyPluginAsync = async (app) => {
// Get all domains for a restaurant
app.get("/restaurants/:id/domains", async (req, reply) => {
const userId = await requireAuth(req, reply);
if (!userId || !supabase) return;
const { id: restaurantId } = req.params as { id: string };
const isMember = await requireRestaurantMember(userId, restaurantId);
if (!isMember) return reply.code(403).send({ message: "forbidden" });
const { data: domains, error } = await supabase
.from("domains")
.select()
.eq("restaurant_id", restaurantId)
.order("created_at", { ascending: false });
if (error) {
req.log.error(error);
return reply.code(500).send({ message: "failed to load domains" });
}
const cnameTarget = `cname.${env.ROOT_DOMAIN}`;
return reply.send({
domains: domains || [],
cnameTarget,
instructions: {
type: "CNAME",
target: cnameTarget,
},
});
});
// Add a custom domain
app.post("/restaurants/:id/domains", async (req, reply) => {
const userId = await requireAuth(req, reply);
if (!userId || !supabase) return;
const { id: restaurantId } = req.params as { id: string };
const isMember = await requireRestaurantMember(userId, restaurantId);
if (!isMember) return reply.code(403).send({ message: "forbidden" });
const parsed = addDomainSchema.safeParse(req.body);
if (!parsed.success) {
return reply.code(400).send({ message: "Geçersiz alan adı formatı", issues: parsed.error.issues });
}
const { hostname } = parsed.data;
// Check if domain is already registered
const { data: existing } = await supabase
.from("domains")
.select("id, restaurant_id")
.eq("hostname", hostname)
.maybeSingle();
if (existing) {
if (existing.restaurant_id === restaurantId) {
return reply.code(409).send({ message: "Bu alan adı zaten bu restorana eklenmiş." });
}
return reply.code(409).send({ message: "Bu alan adı başka bir hesap tarafından kullanılıyor." });
}
const { data: newDomain, error } = await supabase
.from("domains")
.insert({
restaurant_id: restaurantId,
hostname,
is_custom: true,
status: "pending",
})
.select()
.single();
if (error || !newDomain) {
req.log.error(error);
return reply.code(500).send({ message: "failed to add domain" });
}
const cnameTarget = `cname.${env.ROOT_DOMAIN}`;
return reply.status(201).send({
domain: newDomain,
cnameTarget,
instructions: {
type: "CNAME",
host: hostname,
target: cnameTarget,
},
});
});
// REAL DNS Verification (Checks real CNAME records across 8.8.8.8 and 1.1.1.1)
app.post("/restaurants/:id/domains/:domainId/verify", async (req, reply) => {
const userId = await requireAuth(req, reply);
if (!userId || !supabase) return;
const { id: restaurantId, domainId } = req.params as { id: string; domainId: string };
const isMember = await requireRestaurantMember(userId, restaurantId);
if (!isMember) return reply.code(403).send({ message: "forbidden" });
const { data: domain, error: domainError } = await supabase
.from("domains")
.select()
.eq("id", domainId)
.eq("restaurant_id", restaurantId)
.single();
if (domainError || !domain) {
return reply.code(404).send({ message: "domain not found" });
}
const expectedTarget = `cname.${env.ROOT_DOMAIN}`;
let isVerified = false;
let dnsErrorDetails = "";
try {
// Real DNS lookup for CNAME
const cnames = await resolver.resolveCname(domain.hostname);
req.log.info({ hostname: domain.hostname, foundCnames: cnames }, "Real DNS CNAME check");
isVerified = cnames.some(
(c) =>
c.toLowerCase().includes(env.ROOT_DOMAIN.toLowerCase()) ||
c.toLowerCase().includes(`cname.${env.ROOT_DOMAIN}`.toLowerCase()) ||
c.toLowerCase().includes("menul.io"),
);
if (!isVerified) {
dnsErrorDetails = `Bulunan CNAME: ${cnames.join(", ")} (Beklenen: ${expectedTarget})`;
}
} catch (err: any) {
req.log.warn({ hostname: domain.hostname, err: err?.message || err }, "DNS CNAME lookup failed");
if (err?.code === "ENOTFOUND" || err?.code === "ENODATA") {
dnsErrorDetails = "DNS kaydı bulunamadı. Henüz CNAME yönlendirmesi yapılmamış veya DNS yayılımı (propagation) tamamlanmamış.";
} else {
dnsErrorDetails = err?.message || "DNS sorgusu başarısız oldu.";
}
}
if (isVerified) {
const { data: updated } = await supabase
.from("domains")
.update({
status: "verified",
verified_at: new Date().toISOString(),
})
.eq("id", domainId)
.select()
.single();
return reply.send({
verified: true,
domain: updated,
message: `Tebrikler! ${domain.hostname} alan adının CNAME kaydı doğrulandı ve menünüze bağlandı. 🎉`,
});
}
// If not verified, set/keep status as pending or failed
await supabase
.from("domains")
.update({
status: "pending",
verified_at: null,
})
.eq("id", domainId);
return reply.send({
verified: false,
domain: { ...domain, status: "pending", verified_at: null },
message: `Doğrulanamadı: ${dnsErrorDetails}\n\nLütfen alan adı yönetim panelinizden (Cloudflare, GoDaddy, Natro vb.) ${domain.hostname} için CNAME kaydını ${expectedTarget} adresine yönlendirin.`,
});
});
// Delete custom domain
app.delete("/restaurants/:id/domains/:domainId", async (req, reply) => {
const userId = await requireAuth(req, reply);
if (!userId || !supabase) return;
const { id: restaurantId, domainId } = req.params as { id: string; domainId: string };
const isMember = await requireRestaurantMember(userId, restaurantId);
if (!isMember) return reply.code(403).send({ message: "forbidden" });
const { error } = await supabase
.from("domains")
.delete()
.eq("id", domainId)
.eq("restaurant_id", restaurantId);
if (error) {
req.log.error(error);
return reply.code(500).send({ message: "failed to delete domain" });
}
return reply.send({ success: true });
});
};