feat: implement DELETE /restaurants/:id endpoint and mobile UI danger zone for restaurant deletion
This commit is contained in:
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import { requireAuth, requireRestaurantMember } from "../lib/auth.js";
|
import { requireAuth, requireRestaurantMember } from "../lib/auth.js";
|
||||||
import { generateUniqueRestaurantSlug } from "../lib/slug.js";
|
import { generateUniqueRestaurantSlug } from "../lib/slug.js";
|
||||||
import { supabase } from "../lib/supabase.js";
|
import { supabase } from "../lib/supabase.js";
|
||||||
|
import { deleteCustomHostname, getCustomHostnameByHostname } from "../lib/cloudflare.js";
|
||||||
|
|
||||||
const createRestaurantSchema = z.object({
|
const createRestaurantSchema = z.object({
|
||||||
name: z.string().min(1).max(120),
|
name: z.string().min(1).max(120),
|
||||||
@@ -206,4 +207,59 @@ export const restaurantsRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
|
|
||||||
return reply.send({ success: true, themeKey });
|
return reply.send({ success: true, themeKey });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.delete("/restaurants/:id", async (req, reply) => {
|
||||||
|
const userId = await requireAuth(req, reply);
|
||||||
|
if (!userId || !supabase) return;
|
||||||
|
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const isMember = await requireRestaurantMember(userId, id);
|
||||||
|
if (!isMember) return reply.code(403).send({ message: "forbidden" });
|
||||||
|
|
||||||
|
// Delete custom hostnames from Cloudflare if any custom domains exist
|
||||||
|
const { data: domains } = await supabase.from("domains").select("id, hostname").eq("restaurant_id", id);
|
||||||
|
if (domains && domains.length > 0) {
|
||||||
|
for (const dom of domains) {
|
||||||
|
try {
|
||||||
|
const cfHostname = await getCustomHostnameByHostname(dom.hostname);
|
||||||
|
if (cfHostname?.id) {
|
||||||
|
await deleteCustomHostname(cfHostname.id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
req.log.warn({ err, hostname: dom.hostname }, "Cloudflare hostname delete failed during restaurant deletion");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete associated database records
|
||||||
|
await supabase.from("domains").delete().eq("restaurant_id", id);
|
||||||
|
await supabase.from("restaurant_themes").delete().eq("restaurant_id", id);
|
||||||
|
await supabase.from("restaurant_members").delete().eq("restaurant_id", id);
|
||||||
|
|
||||||
|
// Delete locations, menus, categories, items
|
||||||
|
const { data: locations } = await supabase.from("locations").select("id").eq("restaurant_id", id);
|
||||||
|
if (locations && locations.length > 0) {
|
||||||
|
const locIds = locations.map((l) => l.id);
|
||||||
|
const { data: menus } = await supabase.from("menus").select("id").in("location_id", locIds);
|
||||||
|
if (menus && menus.length > 0) {
|
||||||
|
const menuIds = menus.map((m) => m.id);
|
||||||
|
const { data: categories } = await supabase.from("menu_categories").select("id").in("menu_id", menuIds);
|
||||||
|
if (categories && categories.length > 0) {
|
||||||
|
const catIds = categories.map((c) => c.id);
|
||||||
|
await supabase.from("menu_items").delete().in("category_id", catIds);
|
||||||
|
await supabase.from("menu_categories").delete().in("menu_id", menuIds);
|
||||||
|
}
|
||||||
|
await supabase.from("menus").delete().in("location_id", locIds);
|
||||||
|
}
|
||||||
|
await supabase.from("locations").delete().eq("restaurant_id", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await supabase.from("restaurants").delete().eq("id", id);
|
||||||
|
if (error) {
|
||||||
|
req.log.error(error);
|
||||||
|
return reply.code(500).send({ message: "failed to delete restaurant" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.send({ success: true, message: "Restoran başarıyla silindi." });
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export default function AccountScreen() {
|
|||||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||||
const [slug, setSlug] = useState("");
|
const [slug, setSlug] = useState("");
|
||||||
const [savingRest, setSavingRest] = useState(false);
|
const [savingRest, setSavingRest] = useState(false);
|
||||||
|
const [deletingRestaurant, setDeletingRestaurant] = useState(false);
|
||||||
|
|
||||||
// Theme state
|
// Theme state
|
||||||
const [selectedTheme, setSelectedTheme] = useState("elegant");
|
const [selectedTheme, setSelectedTheme] = useState("elegant");
|
||||||
@@ -319,6 +320,39 @@ export default function AccountScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteRestaurant() {
|
||||||
|
if (!active) return;
|
||||||
|
Alert.alert(
|
||||||
|
"Restoranı Kalıcı Olarak Sil ⚠️",
|
||||||
|
`"${name}" restoranını ve bu restorana ait tüm menüleri, kategorileri, ürünleri ve özel alan adlarını kalıcı olarak silmek istediğinize emin misiniz?\n\nBu işlem geri alınamaz!`,
|
||||||
|
[
|
||||||
|
{ text: "Vazgeç", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Evet, Kalıcı Olarak Sil",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: async () => {
|
||||||
|
setDeletingRestaurant(true);
|
||||||
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy).catch(() => {});
|
||||||
|
try {
|
||||||
|
await api.delete(`/restaurants/${active.restaurantId}`);
|
||||||
|
await clearActiveRestaurant();
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
|
Alert.alert("Silindi 🗑️", "Restoranınız başarıyla silindi.", [
|
||||||
|
{
|
||||||
|
text: "Tamam",
|
||||||
|
onPress: () => router.replace("/(onboarding)/restaurant"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} catch (err) {
|
||||||
|
Alert.alert("Hata", err instanceof Error ? err.message : "Restoran silinemedi.");
|
||||||
|
setDeletingRestaurant(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center" }}>
|
<View style={{ flex: 1, backgroundColor: "#FAF8F5", alignItems: "center", justifyContent: "center" }}>
|
||||||
@@ -951,6 +985,51 @@ export default function AccountScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Danger Zone: Delete Restaurant */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#FEF2F2",
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#FEE2E2",
|
||||||
|
gap: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||||
|
<Ionicons name="warning-outline" size={20} color="#DC2626" />
|
||||||
|
<Text style={{ fontSize: 16, fontWeight: "800", color: "#991B1B" }}>
|
||||||
|
Tehlikeli Bölge
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={{ fontSize: 12, color: "#991B1B", lineHeight: 18 }}>
|
||||||
|
Restoranınızı sildiğinizde bu restorana bağlı tüm menüler, kategoriler, ürünler ve özel alan adı ayarları kalıcı olarak silinir.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Pressable
|
||||||
|
onPress={handleDeleteRestaurant}
|
||||||
|
disabled={deletingRestaurant}
|
||||||
|
style={({ pressed }) => ({
|
||||||
|
backgroundColor: "#DC2626",
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
opacity: pressed || deletingRestaurant ? 0.85 : 1,
|
||||||
|
marginTop: 4,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{deletingRestaurant ? (
|
||||||
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||||
|
) : (
|
||||||
|
<Text style={{ color: "#FFFFFF", fontSize: 14, fontWeight: "800" }}>
|
||||||
|
Restoranı Kalıcı Olarak Sil
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user