ParaKasa Panel
diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx
index 10e6dbf..3783efc 100644
--- a/app/(public)/login/page.tsx
+++ b/app/(public)/login/page.tsx
@@ -34,7 +34,8 @@ export default function LoginPage() {
return
}
- router.push("/dashboard")
+ // Redirect to 2FA verification instead of dashboard
+ router.push("/verify-2fa")
router.refresh()
} catch {
setError("Bir hata oluştu. Lütfen tekrar deneyin.")
diff --git a/app/(public)/products/page.tsx b/app/(public)/products/page.tsx
index 6de627f..db16c5b 100644
--- a/app/(public)/products/page.tsx
+++ b/app/(public)/products/page.tsx
@@ -1,48 +1,30 @@
+import { createClient } from "@/lib/supabase-server"
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
+import Image from "next/image"
-const products = [
- {
- id: 1,
- name: "Ev Tipi Çelik Kasa",
- image: "/images/safe-1.jpg",
- category: "Ev",
- },
- {
- id: 2,
- name: "Ofis Tipi Yanmaz Kasa",
- image: "/images/safe-2.jpg",
- category: "Ofis",
- },
- {
- id: 3,
- name: "Otel Odası Kasası",
- image: "/images/safe-3.jpg",
- category: "Otel",
- },
- {
- id: 4,
- name: "Silah Kasası (Tüfek)",
- image: "/images/safe-4.jpg",
- category: "Özel",
- },
- {
- id: 5,
- name: "Kuyumcu Kasası",
- image: "/images/safe-5.jpg",
- category: "Ticari",
- },
- {
- id: 6,
- name: "Duvar İçi Gizli Kasa",
- image: "/images/safe-6.jpg",
- category: "Ev",
- },
-]
-export default function ProductsPage() {
+// Helper to get products
+async function getProducts() {
+ const supabase = createClient()
+ const { data, error } = await supabase
+ .from("products")
+ .select("*")
+ .eq("is_active", true)
+ .order("created_at", { ascending: false })
+
+ if (error) {
+ console.error("Error fetching products:", error)
+ return []
+ }
+ return data
+}
+
+export default async function ProductsPage() {
+ const products = await getProducts()
+
return (
@@ -53,27 +35,44 @@ export default function ProductsPage() {
- {products.map((product) => (
-
-
- {/* Placeholder for real images */}
-
-
Görsel: {product.name}
+ {products && products.length > 0 ? (
+ products.map((product) => (
+
+
+ {product.image_url ? (
+
+ ) : (
+
+ Görsel Yok
+
+ )}
-
-
-
-
-
{product.category}
-
{product.name}
+
+
+
+
+ {product.category}
+ ₺{product.price}
+
+
{product.name}
+
-
-
-
-
-
-
- ))}
+
+
+
+
+
+ ))
+ ) : (
+
+
Henüz vitrinde ürünümüz bulunmuyor.
+
+ )}
)
diff --git a/app/(public)/verify-2fa/page.tsx b/app/(public)/verify-2fa/page.tsx
new file mode 100644
index 0000000..8993bd3
--- /dev/null
+++ b/app/(public)/verify-2fa/page.tsx
@@ -0,0 +1,130 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useRouter } from "next/navigation"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { AlertCircle, Loader2 } from "lucide-react"
+import { sendVerificationCode, verifyCode } from "@/lib/sms/verification-actions"
+import { createClient } from "@/lib/supabase-browser"
+
+export default function Verify2FAPage() {
+ const router = useRouter()
+ const [code, setCode] = useState("")
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [sent, setSent] = useState(false)
+ const [maskedPhone, setMaskedPhone] = useState("")
+
+ useEffect(() => {
+ // Init: Send code automatically
+ const init = async () => {
+ setLoading(true)
+ const result = await sendVerificationCode()
+ setLoading(false)
+ if (result.error) {
+ setError(result.error)
+ } else {
+ setSent(true)
+ setMaskedPhone(result.phone || "")
+ }
+ }
+ init()
+ }, [])
+
+ const handleVerify = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+ setError(null)
+
+ try {
+ const result = await verifyCode(code)
+ if (result.error) {
+ setError(result.error)
+ } else {
+ router.push("/dashboard")
+ router.refresh()
+ }
+ } catch {
+ setError("Bir hata oluştu.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleResend = async () => {
+ setLoading(true)
+ setError(null)
+ const result = await sendVerificationCode()
+ setLoading(false)
+ if (result.error) {
+ setError(result.error)
+ } else {
+ setSent(true)
+ }
+ }
+
+ return (
+
+
+
+ SMS Doğrulama
+
+ {sent ? `Telefonunuza (***${maskedPhone}) gönderilen 6 haneli kodu girin.` : "Doğrulama kodu gönderiliyor..."}
+
+
+
+ {error && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index e08d80f..8e67f87 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,6 +2,8 @@
import localFont from "next/font/local";
import { Toaster } from "sonner";
import "./globals.css";
+import { ThemeProvider } from "@/components/theme-provider";
+import { getSiteContents } from "@/lib/data";
const inter = localFont({
src: [
@@ -33,7 +35,6 @@ const outfit = localFont({
variable: "--font-outfit",
});
-import { getSiteContents } from "@/lib/data";
export async function generateMetadata() {
const settings = await getSiteContents();
@@ -44,10 +45,6 @@ export async function generateMetadata() {
};
}
-import { ThemeProvider } from "@/components/theme-provider"
-
-// ... imports
-
export default function RootLayout({
children,
}: Readonly<{
diff --git a/build_log.txt b/build_log.txt
new file mode 100644
index 0000000..ca02a48
Binary files /dev/null and b/build_log.txt differ
diff --git a/build_log_2.txt b/build_log_2.txt
new file mode 100644
index 0000000..dd7b342
Binary files /dev/null and b/build_log_2.txt differ
diff --git a/components/dashboard/auto-logout-handler.tsx b/components/dashboard/auto-logout-handler.tsx
new file mode 100644
index 0000000..25419ce
--- /dev/null
+++ b/components/dashboard/auto-logout-handler.tsx
@@ -0,0 +1,60 @@
+"use client"
+
+import { useEffect, useCallback, useRef } from "react"
+import { useRouter } from "next/navigation"
+import { createClient } from "@/lib/supabase-browser"
+import { toast } from "sonner"
+
+const INACTIVITY_TIMEOUT = 15 * 60 * 1000 // 15 minutes
+
+export function AutoLogoutHandler() {
+ const router = useRouter()
+ const supabase = createClient()
+ const timerRef = useRef(null)
+
+ const handleLogout = useCallback(async () => {
+ await supabase.auth.signOut()
+ toast.info("Oturumunuz uzun süre işlem yapılmadığı için sonlandırıldı.")
+ router.push("/login")
+ router.refresh()
+ }, [router, supabase])
+
+ const resetTimer = useCallback(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current)
+ }
+ timerRef.current = setTimeout(handleLogout, INACTIVITY_TIMEOUT)
+ }, [handleLogout])
+
+ useEffect(() => {
+ // Events to listen for
+ const events = [
+ "mousedown",
+ "mousemove",
+ "keydown",
+ "scroll",
+ "touchstart",
+ ]
+
+ // Initial set
+ resetTimer()
+
+ // Event listener wrapper to debounce slightly/reset
+ const onUserActivity = () => {
+ resetTimer()
+ }
+
+ events.forEach((event) => {
+ window.addEventListener(event, onUserActivity)
+ })
+
+ return () => {
+ if (timerRef.current) clearTimeout(timerRef.current)
+ events.forEach((event) => {
+ window.removeEventListener(event, onUserActivity)
+ })
+ }
+ }, [resetTimer])
+
+ return null
+}
diff --git a/components/dashboard/product-form.tsx b/components/dashboard/product-form.tsx
index c079223..7c273e6 100644
--- a/components/dashboard/product-form.tsx
+++ b/components/dashboard/product-form.tsx
@@ -23,9 +23,15 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
+import { Switch } from "@/components/ui/switch"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
-import { Loader2 } from "lucide-react"
+import { Loader2, X, UploadCloud } from "lucide-react"
+import imageCompression from 'browser-image-compression'
+import { createClient } from "@/lib/supabase-browser"
+import Image from "next/image"
+
+import { createProduct, updateProduct } from "@/app/(dashboard)/dashboard/products/actions"
const productSchema = z.object({
name: z.string().min(2, "Ürün adı en az 2 karakter olmalıdır"),
@@ -33,11 +39,12 @@ const productSchema = z.object({
description: z.string().optional(),
price: z.coerce.number().min(0, "Fiyat 0'dan küçük olamaz"),
image_url: z.string().optional(),
+ is_active: z.boolean().default(true),
+ images: z.array(z.string()).optional()
})
type ProductFormValues = z.infer
-import { createProduct, updateProduct } from "@/app/(dashboard)/dashboard/products/actions"
// Define the shape of data coming from Supabase
interface Product {
@@ -48,6 +55,10 @@ interface Product {
price: number
image_url: string | null
created_at: string
+ is_active?: boolean
+ // images? we might need to fetch them separately if they are in another table,
+ // but for now let's assume update passes them if fetched, or we can handle it later.
+ // Ideally the server component fetches relation.
}
interface ProductFormProps {
@@ -57,6 +68,13 @@ interface ProductFormProps {
export function ProductForm({ initialData }: ProductFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
+ const [uploading, setUploading] = useState(false)
+ const [previewImages, setPreviewImages] = useState(
+ initialData?.image_url ? [initialData.image_url] : []
+ )
+ // Note: initialData probably only has single image_url field unless we updated the fetch query.
+ // For MVP phase 1, we just sync with image_url or expect 'images' prop if we extended it.
+ // I will add a local state for images.
const form = useForm({
resolver: zodResolver(productSchema) as Resolver,
@@ -66,15 +84,93 @@ export function ProductForm({ initialData }: ProductFormProps) {
description: initialData.description || "",
price: initialData.price,
image_url: initialData.image_url || "",
+ is_active: initialData.is_active ?? true,
+ images: initialData.image_url ? [initialData.image_url] : []
} : {
name: "",
category: "",
description: "",
price: 0,
image_url: "",
+ is_active: true,
+ images: []
},
})
+ const handleImageUpload = async (event: React.ChangeEvent) => {
+ const files = event.target.files
+ if (!files || files.length === 0) return
+
+ setUploading(true)
+ const supabase = createClient()
+ const uploadedUrls: string[] = [...form.getValues("images") || []]
+
+ try {
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i]
+
+ // Compression
+ const options = {
+ maxSizeMB: 1, // Max 1MB
+ maxWidthOrHeight: 1920,
+ useWebWorker: true
+ }
+
+ let compressedFile = file
+ try {
+ compressedFile = await imageCompression(file, options)
+ } catch (error) {
+ console.error("Compression error:", error)
+ // Fallback to original
+ }
+
+ // Upload
+ const fileExt = file.name.split('.').pop()
+ const fileName = `${Math.random().toString(36).substring(2)}_${Date.now()}.${fileExt}`
+ const filePath = `products/${fileName}`
+
+ const { error: uploadError } = await supabase.storage
+ .from('products') // Assuming 'products' bucket exists
+ .upload(filePath, compressedFile)
+
+ if (uploadError) {
+ console.error(uploadError)
+ toast.error(`Resim yüklenemedi: ${file.name}`)
+ continue
+ }
+
+ // Get URL
+ const { data } = supabase.storage.from('products').getPublicUrl(filePath)
+ uploadedUrls.push(data.publicUrl)
+ }
+
+ // Update form
+ form.setValue("images", uploadedUrls)
+ // Set first image as main
+ if (uploadedUrls.length > 0) {
+ form.setValue("image_url", uploadedUrls[0])
+ }
+ setPreviewImages(uploadedUrls)
+
+ } catch {
+ toast.error("Yükleme sırasında hata oluştu")
+ } finally {
+ setUploading(false)
+ }
+ }
+
+ const removeImage = (index: number) => {
+ const currentImages = [...form.getValues("images") || []]
+ currentImages.splice(index, 1)
+ form.setValue("images", currentImages)
+ if (currentImages.length > 0) {
+ form.setValue("image_url", currentImages[0])
+ } else {
+ form.setValue("image_url", "")
+ }
+ setPreviewImages(currentImages)
+ }
+
async function onSubmit(data: ProductFormValues) {
try {
setLoading(true)
@@ -104,21 +200,45 @@ export function ProductForm({ initialData }: ProductFormProps) {
return (
diff --git a/drop_site_settings.sql b/drop_site_settings.sql
deleted file mode 100644
index c2fd82e..0000000
--- a/drop_site_settings.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- Drop the site_settings table as it is replaced by site_contents
-DROP TABLE IF EXISTS site_settings;
diff --git a/lib/sms/verification-actions.ts b/lib/sms/verification-actions.ts
new file mode 100644
index 0000000..1ccec31
--- /dev/null
+++ b/lib/sms/verification-actions.ts
@@ -0,0 +1,124 @@
+"use server"
+
+import { createClient } from "@/lib/supabase-server"
+import { createClient as createSupabaseClient } from "@supabase/supabase-js"
+
+import { cookies } from "next/headers"
+import { NetGsmService } from "./netgsm"
+// We will reuse sendTestSms logic or create a specific one. sendTestSms uses Netgsm Service.
+// Better to export a generic 'sendSms' from lib/sms/actions.ts or just invoke the service directly.
+// lib/sms/actions.ts has `sendBulkSms` and `sendTestSms`. I should probably expose a generic `sendSms` there.
+
+// Admin client for Auth Codes table access
+const supabaseAdmin = createSupabaseClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
+ { auth: { autoRefreshToken: false, persistSession: false } }
+)
+
+export async function sendVerificationCode() {
+ const supabase = createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) return { error: "Kullanıcı bulunamadı." }
+
+ // 1. Get user phone
+ const { data: profile } = await supabaseAdmin
+ .from('profiles')
+ .select('phone')
+ .eq('id', user.id)
+ .single()
+
+ if (!profile?.phone) {
+ return { error: "Profilinizde telefon numarası tanımlı değil. Lütfen yöneticinizle iletişime geçin." }
+ }
+
+ // 2. Generate Code
+ const code = Math.floor(100000 + Math.random() * 900000).toString() // 6 digit
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1000) // 5 mins
+
+ // 3. Store in DB
+ // First, delete old codes for this email/user
+ await supabaseAdmin.from('auth_codes').delete().eq('email', user.email!)
+
+ const { error: dbError } = await supabaseAdmin.from('auth_codes').insert({
+ email: user.email!,
+ code,
+ expires_at: expiresAt.toISOString()
+ })
+
+ if (dbError) {
+ console.error("Auth code db error:", dbError)
+ return { error: "Doğrulama kodu oluşturulamadı." }
+ }
+
+ // 4. Send SMS
+ // We import the logic from Netgsm service wrapper
+ // Since we don't have a direct 'sendSms' export in existing actions that accepts phone/message directly without admin assertion (which we have here via admin client, but the helper function `sendTestSms` does its own checks).
+ // I will use a direct call to the generic `NetgsmService` logic if I can, or modify `lib/sms/actions.ts` to export it.
+ // To avoid modifying too many files, I'll instantiate NetgsmService here if I can import it, or just use `sendBulkSms` with one number?
+ // `sendBulkSms` asserts admin user. But here the calling user IS logged in (but might not be admin?).
+ // Actually, `sendVerificationCode` is called by the logging-in user (who might be just 'user' role).
+ // `lib/sms/actions.ts` -> `assertAdmin()` checks if current user is admin.
+ // So if a normal user logs in, `sendBulkSms` will fail.
+ // WE NEED A SYSTEM LEVEL SEND FUNCTION.
+
+ // I will read credentials directly using Admin Client here.
+ const { data: settings } = await supabaseAdmin.from('sms_settings').select('*').single()
+ if (!settings) return { error: "SMS servisi yapılandırılmamış." }
+
+ // Import the class dynamically or duplicate usage?
+ // The class is in `./netgsm.ts` (based on actions.ts imports).
+ // Let's import { NetGsmService } from "./netgsm"
+ // NetGsmService imported at top
+
+ const mobileService = new NetGsmService({
+ username: settings.username,
+ password: settings.password,
+ header: settings.header,
+ apiUrl: settings.api_url
+ })
+
+ const smsResult = await mobileService.sendSms(profile.phone, `Giris Dogrulama Kodunuz: ${code}`)
+
+ if (!smsResult.success) {
+ console.error("SMS Send Error:", smsResult)
+ return { error: "SMS gönderilemedi. Lütfen daha sonra tekrar deneyin." }
+ }
+
+ return { success: true, phone: profile.phone.slice(-4) } // Return last 4 digits
+}
+
+export async function verifyCode(code: string) {
+ const supabase = createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) return { error: "Oturum bulunamadı." }
+
+ // Check code
+ const { data: record, error } = await supabaseAdmin
+ .from('auth_codes')
+ .select('*')
+ .eq('email', user.email!)
+ .eq('code', code)
+ .gt('expires_at', new Date().toISOString())
+ .single()
+
+ if (error || !record) {
+ return { error: "Geçersiz veya süresi dolmuş kod." }
+ }
+
+ // Success: Set Cookie
+ cookies().set('parakasa_2fa_verified', 'true', {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ sameSite: 'lax',
+ path: '/',
+ maxAge: 60 * 60 * 24 // 24 hours
+ })
+
+ // Delete used code
+ await supabaseAdmin.from('auth_codes').delete().eq('id', record.id)
+
+ return { success: true }
+}
diff --git a/lint_output.txt b/lint_output.txt
new file mode 100644
index 0000000..051a96b
Binary files /dev/null and b/lint_output.txt differ
diff --git a/lint_output_2.txt b/lint_output_2.txt
new file mode 100644
index 0000000..f1409d5
Binary files /dev/null and b/lint_output_2.txt differ
diff --git a/lint_output_3.txt b/lint_output_3.txt
new file mode 100644
index 0000000..f5c0bfc
Binary files /dev/null and b/lint_output_3.txt differ
diff --git a/make_admin.sql b/make_admin.sql
deleted file mode 100644
index b7a94b1..0000000
--- a/make_admin.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-
--- Insert profile for the existing user and make them admin
-insert into public.profiles (id, role, full_name)
-select id, 'admin', 'Sistem Yöneticisi'
-from auth.users
-where email = 'kenankaraerr@hotmail.com'
-on conflict (id) do update
-set role = 'admin';
diff --git a/middleware.ts b/middleware.ts
index 1efe886..9ff26d6 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -38,11 +38,19 @@ export async function middleware(request: NextRequest) {
} = await supabase.auth.getUser();
// Protected routes
- if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
- return NextResponse.redirect(new URL("/login", request.url));
+ if (request.nextUrl.pathname.startsWith("/dashboard")) {
+ if (!user) {
+ return NextResponse.redirect(new URL("/login", request.url));
+ }
+
+ // 2FA Check
+ const isVerified = request.cookies.get('parakasa_2fa_verified')?.value === 'true'
+ if (!isVerified) {
+ return NextResponse.redirect(new URL("/verify-2fa", request.url));
+ }
}
- // Redirect to dashboard if logged in and trying to access auth pages
+ // Redirect to dashboard (or verify) if logged in
if (user && (request.nextUrl.pathname.startsWith("/login") || request.nextUrl.pathname.startsWith("/signup"))) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
diff --git a/migrations/schema_full.sql b/migrations/schema_full.sql
new file mode 100644
index 0000000..d09a546
--- /dev/null
+++ b/migrations/schema_full.sql
@@ -0,0 +1,345 @@
+-- ParaKasa Consolidated Database Schema
+-- Generated on 2026-01-29
+-- This file contains the entire database structure, RLS policies, and storage setup.
+
+-- 1. Enable Extensions
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+-- 2. Tables
+
+-- PROFILES
+CREATE TABLE IF NOT EXISTS public.profiles (
+ id UUID REFERENCES auth.users ON DELETE CASCADE PRIMARY KEY,
+ role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('admin', 'user')),
+ full_name TEXT,
+ phone TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- SITE SETTINGS
+CREATE TABLE IF NOT EXISTS public.site_settings (
+ id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ site_title TEXT NOT NULL DEFAULT 'ParaKasa',
+ site_description TEXT,
+ contact_email TEXT,
+ contact_phone TEXT,
+ logo_url TEXT,
+ currency TEXT DEFAULT 'TRY',
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- SITE CONTENTS (Dynamic CMS)
+CREATE TABLE IF NOT EXISTS public.site_contents (
+ key TEXT PRIMARY KEY,
+ value TEXT,
+ type TEXT CHECK (type IN ('text', 'image_url', 'html', 'long_text', 'json')),
+ section TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- CATEGORIES
+CREATE TABLE IF NOT EXISTS public.categories (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ name TEXT NOT NULL,
+ slug TEXT NOT NULL UNIQUE,
+ description TEXT,
+ image_url TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- PRODUCTS
+CREATE TABLE IF NOT EXISTS public.products (
+ id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ name TEXT NOT NULL,
+ category TEXT NOT NULL, -- Legacy text field, kept for compatibility
+ category_id UUID REFERENCES public.categories(id) ON DELETE SET NULL, -- Foreign key relation
+ description TEXT,
+ image_url TEXT,
+ price DECIMAL(10,2),
+ is_active BOOLEAN DEFAULT true,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+CREATE INDEX IF NOT EXISTS idx_products_category_id ON public.products(category_id);
+
+-- PRODUCT IMAGES (Multi-image support)
+CREATE TABLE IF NOT EXISTS public.product_images (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ product_id BIGINT REFERENCES public.products(id) ON DELETE CASCADE NOT NULL,
+ image_url TEXT NOT NULL,
+ display_order INT DEFAULT 0,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_product_images_product_id ON public.product_images(product_id);
+
+-- SLIDERS
+CREATE TABLE IF NOT EXISTS public.sliders (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT,
+ image_url TEXT NOT NULL,
+ link TEXT,
+ "order" INTEGER DEFAULT 0,
+ is_active BOOLEAN DEFAULT true,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- CUSTOMERS
+CREATE TABLE IF NOT EXISTS public.customers (
+ id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
+ full_name TEXT NOT NULL,
+ email TEXT,
+ phone TEXT,
+ address TEXT,
+ notes TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- SMS SETTINGS
+CREATE TABLE IF NOT EXISTS public.sms_settings (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ provider TEXT DEFAULT 'netgsm',
+ api_url TEXT DEFAULT 'https://api.netgsm.com.tr/sms/send/get',
+ username TEXT,
+ password TEXT,
+ header TEXT,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
+);
+
+-- SMS LOGS
+CREATE TABLE IF NOT EXISTS public.sms_logs (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ phone TEXT NOT NULL,
+ message TEXT NOT NULL,
+ status TEXT, -- 'success' or 'error'
+ response_code TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
+);
+
+-- AUTH CODES (2FA / Verification)
+CREATE TABLE IF NOT EXISTS public.auth_codes (
+ id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
+ email TEXT NOT NULL,
+ code TEXT NOT NULL,
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS idx_auth_codes_email ON public.auth_codes(email);
+
+
+-- 3. Row Level Security (RLS) & Policies
+
+-- Enable RLS
+ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.site_contents ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.product_images ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.sliders ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.customers ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.sms_settings ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.sms_logs ENABLE ROW LEVEL SECURITY;
+ALTER TABLE public.auth_codes ENABLE ROW LEVEL SECURITY;
+
+-- Helper function for admin check (optional, but cleaner if used commonly)
+-- For this script, we'll use the EXISTS subquery pattern directly to ensure portability.
+
+-- PROFILES POLICIES
+CREATE POLICY "Public profiles are viewable by everyone."
+ ON public.profiles FOR SELECT USING ( true );
+
+CREATE POLICY "Users can insert their own profile."
+ ON public.profiles FOR INSERT WITH CHECK ( auth.uid() = id );
+
+CREATE POLICY "Users can update own profile."
+ ON public.profiles FOR UPDATE USING ( auth.uid() = id );
+
+-- SITE SETTINGS POLICIES
+CREATE POLICY "Site settings are viewable by everyone."
+ ON public.site_settings FOR SELECT USING ( true );
+
+CREATE POLICY "Admins can update site settings."
+ ON public.site_settings FOR UPDATE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- SITE CONTENTS POLICIES
+CREATE POLICY "Public read access"
+ ON public.site_contents FOR SELECT USING (true);
+
+CREATE POLICY "Admins can insert site contents"
+ ON public.site_contents FOR INSERT WITH CHECK (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can update site contents"
+ ON public.site_contents FOR UPDATE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- CATEGORIES POLICIES
+CREATE POLICY "Public categories are viewable by everyone."
+ ON public.categories FOR SELECT USING ( true );
+
+CREATE POLICY "Admins can insert categories."
+ ON public.categories FOR INSERT WITH CHECK (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can update categories."
+ ON public.categories FOR UPDATE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can delete categories."
+ ON public.categories FOR DELETE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- PRODUCTS POLICIES
+CREATE POLICY "Public products are viewable by everyone."
+ ON public.products FOR SELECT USING ( true );
+
+CREATE POLICY "Admins can insert products"
+ ON public.products FOR INSERT WITH CHECK (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can update products"
+ ON public.products FOR UPDATE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can delete products"
+ ON public.products FOR DELETE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- PRODUCT IMAGES POLICIES (Inherit from products basically, or admin only)
+CREATE POLICY "Public product images are viewable."
+ ON public.product_images FOR SELECT USING ( true );
+
+CREATE POLICY "Admins can insert product images"
+ ON public.product_images FOR INSERT WITH CHECK (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can delete product images"
+ ON public.product_images FOR DELETE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- SLIDERS POLICIES
+CREATE POLICY "Public sliders are viewable by everyone."
+ ON public.sliders FOR SELECT USING ( true );
+
+CREATE POLICY "Admins can insert sliders."
+ ON public.sliders FOR INSERT WITH CHECK (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can update sliders."
+ ON public.sliders FOR UPDATE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can delete sliders."
+ ON public.sliders FOR DELETE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- CUSTOMERS POLICIES
+CREATE POLICY "Admins can view customers"
+ ON public.customers FOR SELECT USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can insert customers"
+ ON public.customers FOR INSERT WITH CHECK (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can update customers"
+ ON public.customers FOR UPDATE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+CREATE POLICY "Admins can delete customers"
+ ON public.customers FOR DELETE USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+ );
+
+-- SMS SETTINGS/LOGS POLICIES
+CREATE POLICY "Admins can full access sms" ON public.sms_settings USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+);
+CREATE POLICY "Admins can full access sms logs" ON public.sms_logs USING (
+ EXISTS (SELECT 1 FROM public.profiles WHERE profiles.id = auth.uid() AND profiles.role = 'admin')
+);
+
+
+-- 4. Initial Data
+
+-- Site Settings Default
+INSERT INTO public.site_settings (site_title, contact_email)
+SELECT 'ParaKasa', 'info@parakasa.com'
+WHERE NOT EXISTS (SELECT 1 FROM public.site_settings);
+
+-- Site Contents Defaults
+INSERT INTO public.site_contents (key, value, type, section) VALUES
+ ('site_title', 'ParaKasa', 'text', 'general'),
+ ('site_description', 'ParaKasa Yönetim Paneli', 'long_text', 'general'),
+ ('site_logo', '', 'image_url', 'general'),
+ ('contact_phone', '', 'text', 'contact'),
+ ('contact_email', '', 'text', 'contact'),
+ ('contact_address', '', 'long_text', 'contact'),
+ ('social_instagram', '', 'text', 'contact'),
+ ('social_youtube', '', 'text', 'contact'),
+ ('social_tiktok', '', 'text', 'contact'),
+ ('contact_map_embed', '', 'html', 'contact')
+ON CONFLICT (key) DO NOTHING;
+
+
+-- 5. Storage Buckets & Policies
+
+-- Buckets
+INSERT INTO storage.buckets (id, name, public) VALUES ('products', 'products', true) ON CONFLICT (id) DO NOTHING;
+INSERT INTO storage.buckets (id, name, public) VALUES ('categories', 'categories', true) ON CONFLICT (id) DO NOTHING;
+INSERT INTO storage.buckets (id, name, public) VALUES ('sliders', 'sliders', true) ON CONFLICT (id) DO NOTHING;
+
+-- Drop existing policies to avoid duplicates if re-running
+DROP POLICY IF EXISTS "Public Access Products" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Upload Products" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Update Products" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Delete Products" ON storage.objects;
+
+-- Products Policies
+CREATE POLICY "Public Access Products" ON storage.objects FOR SELECT USING ( bucket_id = 'products' );
+CREATE POLICY "Auth Upload Products" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'products' AND auth.role() = 'authenticated' );
+CREATE POLICY "Auth Update Products" ON storage.objects FOR UPDATE USING ( bucket_id = 'products' AND auth.role() = 'authenticated' );
+CREATE POLICY "Auth Delete Products" ON storage.objects FOR DELETE USING ( bucket_id = 'products' AND auth.role() = 'authenticated' );
+
+-- Categories Policies
+DROP POLICY IF EXISTS "Public Access Categories" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Upload Categories" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Update Categories" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Delete Categories" ON storage.objects;
+
+CREATE POLICY "Public Access Categories" ON storage.objects FOR SELECT USING ( bucket_id = 'categories' );
+CREATE POLICY "Auth Upload Categories" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'categories' AND auth.role() = 'authenticated' );
+CREATE POLICY "Auth Update Categories" ON storage.objects FOR UPDATE USING ( bucket_id = 'categories' AND auth.role() = 'authenticated' );
+CREATE POLICY "Auth Delete Categories" ON storage.objects FOR DELETE USING ( bucket_id = 'categories' AND auth.role() = 'authenticated' );
+
+-- Sliders Policies
+DROP POLICY IF EXISTS "Public Access Sliders" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Upload Sliders" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Update Sliders" ON storage.objects;
+DROP POLICY IF EXISTS "Auth Delete Sliders" ON storage.objects;
+
+CREATE POLICY "Public Access Sliders" ON storage.objects FOR SELECT USING ( bucket_id = 'sliders' );
+CREATE POLICY "Auth Upload Sliders" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'sliders' AND auth.role() = 'authenticated' );
+CREATE POLICY "Auth Update Sliders" ON storage.objects FOR UPDATE USING ( bucket_id = 'sliders' AND auth.role() = 'authenticated' );
+CREATE POLICY "Auth Delete Sliders" ON storage.objects FOR DELETE USING ( bucket_id = 'sliders' AND auth.role() = 'authenticated' );
+
diff --git a/next.config.mjs b/next.config.mjs
index fd5c663..44e4c82 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -7,6 +7,10 @@ const nextConfig = {
protocol: 'https',
hostname: '**.supabase.co',
},
+ {
+ protocol: 'https',
+ hostname: 'api.unsalcelikparakasalari.com',
+ },
],
},
webpack: (config) => {
diff --git a/nixpacks.toml b/nixpacks.toml
new file mode 100644
index 0000000..ce7ad40
--- /dev/null
+++ b/nixpacks.toml
@@ -0,0 +1,8 @@
+[phases.setup]
+nixPkgs = ["nodejs-20_x", "npm-9_x"]
+
+[phases.install]
+cmds = ["npm ci"]
+
+[phases.build]
+cmds = ["npm run build"]
diff --git a/package-lock.json b/package-lock.json
index 6278036..0f3c019 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,14 +42,18 @@
},
"devDependencies": {
"@types/node": "^20",
+ "@types/pg": "^8.16.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.23",
+ "dotenv": "^17.2.3",
"eslint": "^8",
"eslint-config-next": "14.2.16",
+ "pg": "^8.17.2",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
+ "ts-node": "^10.9.2",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
@@ -67,6 +71,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
@@ -1775,6 +1803,34 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+ "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
@@ -1802,6 +1858,18 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/pg": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz",
+ "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
@@ -2413,6 +2481,19 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3105,6 +3186,13 @@
"integrity": "sha512-Z3Zi419FI889tlElMsVhCIS5eRkiLDWixr576J5DPiTe5RGxpbRi+enMpHdYVp5iK5WFjr8P/RgyIFAGhFsiFg==",
"license": "MIT"
},
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3285,6 +3373,16 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/diff": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+ "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
@@ -3305,6 +3403,19 @@
"node": ">=6.0.0"
}
},
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5344,6 +5455,13 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5854,6 +5972,103 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/pg": {
+ "version": "8.17.2",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz",
+ "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.10.1",
+ "pg-pool": "^3.11.0",
+ "pg-protocol": "^1.11.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.10.1",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz",
+ "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
+ "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
+ "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6066,6 +6281,49 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6699,6 +6957,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -7202,6 +7470,57 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/ts-node": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-node/node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -7515,6 +7834,13 @@
"integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==",
"license": "MIT"
},
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7759,6 +8085,26 @@
}
}
},
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 01a3cb1..ea36723 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,9 @@
"start": "next start",
"lint": "next lint"
},
+ "engines": {
+ "node": "20.x"
+ },
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.11",
@@ -43,15 +46,19 @@
},
"devDependencies": {
"@types/node": "^20",
+ "@types/pg": "^8.16.0",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.23",
+ "dotenv": "^17.2.3",
"eslint": "^8",
"eslint-config-next": "14.2.16",
+ "pg": "^8.17.2",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7",
+ "ts-node": "^10.9.2",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
-}
+}
\ No newline at end of file
diff --git a/scripts/deploy-migration.ts b/scripts/deploy-migration.ts
new file mode 100644
index 0000000..426101b
--- /dev/null
+++ b/scripts/deploy-migration.ts
@@ -0,0 +1,49 @@
+import { createClient } from "@supabase/supabase-js"
+import fs from "fs"
+import path from "path"
+import dotenv from "dotenv"
+
+// Load env vars
+dotenv.config({ path: '.env.local' })
+
+const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
+const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
+
+if (!supabaseUrl || !supabaseServiceKey) {
+ console.error("Missing Supabase credentials in .env.local")
+ process.exit(1)
+}
+
+const supabase = createClient(supabaseUrl, supabaseServiceKey, {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+})
+
+async function runMigration() {
+ const migrationPath = path.join(process.cwd(), 'migrations', 'new_features.sql')
+
+ try {
+ const sql = fs.readFileSync(migrationPath, 'utf8')
+ console.log(`Executing migration from: ${migrationPath}`)
+
+ // Supabase JS doesn't support raw SQL query directly on standard client unless enabled via rpc.
+ // However, we can use the 'pg' library if available, but it is not in package.json.
+ // Workaround: We will use the REST API 'rpc' if a function exists, or just tell the user.
+ // BUT! Since we are AGENT, we should try to be helpful.
+ // Actually, 'postgres' or 'pg' IS NOT in package.json.
+ // Alternative: We can try to use a 'rpc' call if we had a 'exec_sql' function.
+ // IF NOT, we are stuck.
+
+ // WAIT! I see `supabase_schema.sql` having `create table`.
+ // Installing 'pg' is easy.
+ console.log("This script requires 'pg' package. Please install it temporarily or run the SQL manually.")
+ } catch (err) {
+ console.error("Error reading file:", err)
+ }
+}
+
+// runMigration()
+// This script is just a placeholder because we realized 'pg' is missing.
+// I will instead install 'pg' temporarily to run this.
diff --git a/scripts/run-migration.js b/scripts/run-migration.js
new file mode 100644
index 0000000..e70f059
--- /dev/null
+++ b/scripts/run-migration.js
@@ -0,0 +1,39 @@
+
+const { Client } = require('pg');
+const fs = require('fs');
+const path = require('path');
+const dotenv = require('dotenv');
+
+dotenv.config({ path: '.env.local' });
+
+async function migrate() {
+ if (!process.env.DATABASE_URL) {
+ console.error("DATABASE_URL is missing in .env.local. Cannot run migration directly.");
+ // Fallback: Check if we have standard supabase credentials and try to construct it?
+ // Postgres URL: postgres://postgres:[PASSWORD]@[HOST]:[PORT]/postgres
+ // We usually don't have the password plain text in env if it's Supabase (unless user added it).
+ // If we fail here, we notify/ask user.
+ process.exit(1);
+ }
+
+ const client = new Client({
+ connectionString: process.env.DATABASE_URL,
+ ssl: { rejectUnauthorized: false }
+ });
+
+ try {
+ await client.connect();
+ const sqlPath = path.join(process.cwd(), 'migrations', 'new_features.sql');
+ const sql = fs.readFileSync(sqlPath, 'utf8');
+
+ await client.query(sql);
+ console.log("Migration executed successfully.");
+ } catch (err) {
+ console.error("Migration failed:", err);
+ process.exit(1);
+ } finally {
+ await client.end();
+ }
+}
+
+migrate();
diff --git a/scripts/run-migration.ts b/scripts/run-migration.ts
new file mode 100644
index 0000000..e8e511f
--- /dev/null
+++ b/scripts/run-migration.ts
@@ -0,0 +1,42 @@
+
+import { Client } from 'pg'
+import fs from 'fs'
+import path from 'path'
+import dotenv from 'dotenv'
+
+dotenv.config({ path: '.env.local' })
+
+// Parse connection string for PG
+// Supabase connection string is usually: postgres://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
+// But we might only have URL and Key in env.
+// If we don't have the connection string, we can't run this.
+// Let's check .env.local content (securely).
+// Actually, I can't see .env.local content due to security rules usually, but I can ask the code to read it.
+// If DATABASE_URL is in .env.local, we are good.
+
+async function migrate() {
+ if (!process.env.DATABASE_URL) {
+ console.error("DATABASE_URL is missing in .env.local. Cannot run migration directly.")
+ process.exit(1)
+ }
+
+ const client = new Client({
+ connectionString: process.env.DATABASE_URL,
+ ssl: { rejectUnauthorized: false }
+ })
+
+ try {
+ await client.connect()
+ const sqlPath = path.join(process.cwd(), 'migrations', 'new_features.sql')
+ const sql = fs.readFileSync(sqlPath, 'utf8')
+
+ await client.query(sql)
+ console.log("Migration executed successfully.")
+ } catch (err) {
+ console.error("Migration failed:", err)
+ } finally {
+ await client.end()
+ }
+}
+
+migrate()
diff --git a/security_updates.sql b/security_updates.sql
deleted file mode 100644
index e4fc5ce..0000000
--- a/security_updates.sql
+++ /dev/null
@@ -1,94 +0,0 @@
--- SECURITY UPDATES
--- This script strengthens the RLS policies by enforcing 'admin' role checks
--- instead of just checking if the user is authenticated.
-
--- 1. PRODUCTS TABLE
--- Drop existing loose policies
-DROP POLICY IF EXISTS "Authenticated users can insert products." ON products;
-DROP POLICY IF EXISTS "Authenticated users can update products." ON products;
-DROP POLICY IF EXISTS "Authenticated users can delete products." ON products;
-
--- Create strict admin policies
-CREATE POLICY "Admins can insert products"
- ON products FOR INSERT
- WITH CHECK (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-CREATE POLICY "Admins can update products"
- ON products FOR UPDATE
- USING (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-CREATE POLICY "Admins can delete products"
- ON products FOR DELETE
- USING (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-
--- 2. CUSTOMERS TABLE
--- Drop existing loose policies (if they match the previous loose pattern)
-DROP POLICY IF EXISTS "Admins can insert customers" ON customers;
-DROP POLICY IF EXISTS "Admins can update customers" ON customers;
-DROP POLICY IF EXISTS "Admins can delete customers" ON customers;
-
--- Re-create strict policies (just to be sure, ensuring the subquery check is present)
-CREATE POLICY "Strict Admin Insert Customers"
- ON customers FOR INSERT
- WITH CHECK (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-CREATE POLICY "Strict Admin Update Customers"
- ON customers FOR UPDATE
- USING (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-CREATE POLICY "Strict Admin Delete Customers"
- ON customers FOR DELETE
- USING (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
--- 3. SITE CONTENTS TABLE
-DROP POLICY IF EXISTS "Admin update access" ON site_contents;
-DROP POLICY IF EXISTS "Admin insert access" ON site_contents;
-
-CREATE POLICY "Strict Admin Update Site Contents"
- ON site_contents FOR UPDATE
- USING (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-CREATE POLICY "Strict Admin Insert Site Contents"
- ON site_contents FOR INSERT
- WITH CHECK (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
diff --git a/supabase_migration_add_phone.sql b/supabase_migration_add_phone.sql
deleted file mode 100644
index 6b171bc..0000000
--- a/supabase_migration_add_phone.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-
--- Add phone column to profiles table
-ALTER TABLE profiles ADD COLUMN IF NOT EXISTS phone TEXT;
diff --git a/supabase_migration_tiktok.sql b/supabase_migration_tiktok.sql
deleted file mode 100644
index 0424c3d..0000000
--- a/supabase_migration_tiktok.sql
+++ /dev/null
@@ -1,9 +0,0 @@
--- Rename social_twitter to social_tiktok if it exists
-UPDATE site_contents
-SET key = 'social_tiktok'
-WHERE key = 'social_twitter';
-
--- If social_twitter didn't exist, insert social_tiktok (handling the case where it might already exist to avoid unique constraint error)
-INSERT INTO site_contents (key, value, type, section)
-VALUES ('social_tiktok', '', 'text', 'contact')
-ON CONFLICT (key) DO NOTHING;
diff --git a/supabase_migration_youtube.sql b/supabase_migration_youtube.sql
deleted file mode 100644
index f00ee92..0000000
--- a/supabase_migration_youtube.sql
+++ /dev/null
@@ -1,9 +0,0 @@
--- Rename social_facebook to social_youtube if it exists
-UPDATE site_contents
-SET key = 'social_youtube'
-WHERE key = 'social_facebook';
-
--- If social_facebook didn't exist, insert social_youtube (handling the case where it might already exist)
-INSERT INTO site_contents (key, value, type, section)
-VALUES ('social_youtube', '', 'text', 'contact')
-ON CONFLICT (key) DO NOTHING;
diff --git a/supabase_schema.sql b/supabase_schema.sql
deleted file mode 100644
index abecd1a..0000000
--- a/supabase_schema.sql
+++ /dev/null
@@ -1,49 +0,0 @@
--- Create products table
-create table if not exists products (
- id bigint primary key generated always as identity,
- name text not null,
- category text not null,
- description text,
- image_url text,
- price decimal(10,2), -- Optional, validation can start without it
- created_at timestamp with time zone default timezone('utc'::text, now()) not null
-);
-
--- Enable RLS
-alter table products enable row level security;
-
--- Policies
--- 1. Public read access
-create policy "Public products are viewable by everyone."
- on products for select
- using ( true );
-
--- 2. Admin write access (Only authenticated users for now, can be restricted to specific emails/roles later)
-create policy "Authenticated users can insert products."
- on products for insert
- with check ( auth.role() = 'authenticated' );
-
-create policy "Authenticated users can update products."
- on products for update
- using ( auth.role() = 'authenticated' );
-
-create policy "Authenticated users can delete products."
- on products for delete
- using ( auth.role() = 'authenticated' );
-
--- Storage Bucket for Product Images
-insert into storage.buckets (id, name, public)
-values ('product-images', 'product-images', true)
-on conflict (id) do nothing;
-
-create policy "Images are publicly accessible."
- on storage.objects for select
- using ( bucket_id = 'product-images' );
-
-create policy "Authenticated users can upload images."
- on storage.objects for insert
- with check ( bucket_id = 'product-images' and auth.role() = 'authenticated' );
-
-create policy "Authenticated users can delete images."
- on storage.objects for delete
- using ( bucket_id = 'product-images' and auth.role() = 'authenticated' );
diff --git a/supabase_schema_additions.sql b/supabase_schema_additions.sql
deleted file mode 100644
index 43e935b..0000000
--- a/supabase_schema_additions.sql
+++ /dev/null
@@ -1,58 +0,0 @@
-
--- Create profiles table
-create table if not exists profiles (
- id uuid references auth.users on delete cascade primary key,
- role text not null default 'user' check (role in ('admin', 'user')),
- full_name text,
- created_at timestamp with time zone default timezone('utc'::text, now()) not null
-);
-
--- Enable RLS for profiles
-alter table profiles enable row level security;
-
--- Policies for profiles
-create policy "Public profiles are viewable by everyone."
- on profiles for select
- using ( true );
-
-create policy "Users can insert their own profile."
- on profiles for insert
- with check ( auth.uid() = id );
-
-create policy "Users can update own profile."
- on profiles for update
- using ( auth.uid() = id );
-
--- Create site_settings table
-create table if not exists site_settings (
- id bigint primary key generated always as identity,
- site_title text not null default 'ParaKasa',
- site_description text,
- contact_email text,
- contact_phone text,
- logo_url text,
- currency text default 'TRY',
- updated_at timestamp with time zone default timezone('utc'::text, now()) not null
-);
-
--- Enable RLS for site_settings
-alter table site_settings enable row level security;
-
--- Policies for site_settings
-create policy "Site settings are viewable by everyone."
- on site_settings for select
- using ( true );
-
-create policy "Only admins can update site settings."
- on site_settings for update
- using (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
--- Initialize default site settings if empty
-insert into site_settings (site_title, contact_email)
-select 'ParaKasa', 'info@parakasa.com'
-where not exists (select 1 from site_settings);
diff --git a/supabase_schema_categories.sql b/supabase_schema_categories.sql
deleted file mode 100644
index 3fb6f78..0000000
--- a/supabase_schema_categories.sql
+++ /dev/null
@@ -1,44 +0,0 @@
--- Create categories table
-create table if not exists categories (
- id uuid default gen_random_uuid() primary key,
- name text not null,
- slug text not null unique,
- description text,
- image_url text,
- created_at timestamp with time zone default timezone('utc'::text, now()) not null
-);
-
--- Enable RLS
-alter table categories enable row level security;
-
--- Policies
-create policy "Public categories are viewable by everyone."
- on categories for select
- using ( true );
-
-create policy "Admins can insert categories."
- on categories for insert
- with check (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-create policy "Admins can update categories."
- on categories for update
- using (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-create policy "Admins can delete categories."
- on categories for delete
- using (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
diff --git a/supabase_schema_customers.sql b/supabase_schema_customers.sql
deleted file mode 100644
index 71d9aa1..0000000
--- a/supabase_schema_customers.sql
+++ /dev/null
@@ -1,35 +0,0 @@
--- Create customers table
-CREATE TABLE IF NOT EXISTS customers (
- id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
- full_name TEXT NOT NULL,
- email TEXT,
- phone TEXT,
- address TEXT,
- notes TEXT,
- created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
-);
-
--- Enable Row Level Security
-ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
-
--- Policies
--- 1. Admin read access
-CREATE POLICY "Admins can view customers"
- ON customers FOR SELECT
- USING (auth.role() = 'authenticated');
-
--- 2. Admin insert access
-CREATE POLICY "Admins can insert customers"
- ON customers FOR INSERT
- WITH CHECK (auth.role() = 'authenticated');
-
--- 3. Admin update access
-CREATE POLICY "Admins can update customers"
- ON customers FOR UPDATE
- USING (auth.role() = 'authenticated');
-
--- 4. Admin delete access
-CREATE POLICY "Admins can delete customers"
- ON customers FOR DELETE
- USING (auth.role() = 'authenticated');
diff --git a/supabase_schema_netgsm.sql b/supabase_schema_netgsm.sql
deleted file mode 100644
index 0a26ad9..0000000
--- a/supabase_schema_netgsm.sql
+++ /dev/null
@@ -1,83 +0,0 @@
--- Create sms_settings table
-CREATE TABLE IF NOT EXISTS public.sms_settings (
- id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
- provider TEXT DEFAULT 'netgsm',
- api_url TEXT DEFAULT 'https://api.netgsm.com.tr/sms/send/get',
- username TEXT,
- password TEXT,
- header TEXT,
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
-);
-
--- Create sms_logs table
-CREATE TABLE IF NOT EXISTS public.sms_logs (
- id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
- phone TEXT NOT NULL,
- message TEXT NOT NULL,
- status TEXT, -- 'success' or 'error'
- response_code TEXT,
- created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now())
-);
-
--- Enable RLS
-ALTER TABLE public.sms_settings ENABLE ROW LEVEL SECURITY;
-ALTER TABLE public.sms_logs ENABLE ROW LEVEL SECURITY;
-
--- RLS Policies for sms_settings
--- Only admins can view settings
-CREATE POLICY "Admins can view sms settings" ON public.sms_settings
- FOR SELECT
- USING (
- exists (
- select 1 from public.profiles
- where profiles.id = auth.uid()
- and profiles.role = 'admin'
- )
- );
-
--- Only admins can update settings
-CREATE POLICY "Admins can update sms settings" ON public.sms_settings
- FOR UPDATE
- USING (
- exists (
- select 1 from public.profiles
- where profiles.id = auth.uid()
- and profiles.role = 'admin'
- )
- );
-
--- Only admins can insert settings (though usually init script does this)
-CREATE POLICY "Admins can insert sms settings" ON public.sms_settings
- FOR INSERT
- WITH CHECK (
- exists (
- select 1 from public.profiles
- where profiles.id = auth.uid()
- and profiles.role = 'admin'
- )
- );
-
--- RLS Policies for sms_logs
--- Only admins can view logs
-CREATE POLICY "Admins can view sms logs" ON public.sms_logs
- FOR SELECT
- USING (
- exists (
- select 1 from public.profiles
- where profiles.id = auth.uid()
- and profiles.role = 'admin'
- )
- );
-
--- System functionality (via Service Role) will bypass RLS, so we don't strictly need INSERT policies for user logic
--- unless we want admins to manually insert logs (unlikely).
--- But for good measure, allow admins to delete logs if needed
-CREATE POLICY "Admins can delete sms logs" ON public.sms_logs
- FOR DELETE
- USING (
- exists (
- select 1 from public.profiles
- where profiles.id = auth.uid()
- and profiles.role = 'admin'
- )
- );
diff --git a/supabase_schema_products_category_fk.sql b/supabase_schema_products_category_fk.sql
deleted file mode 100644
index 9d4cc5b..0000000
--- a/supabase_schema_products_category_fk.sql
+++ /dev/null
@@ -1,6 +0,0 @@
--- Add category_id to products table
-alter table products
-add column if not exists category_id uuid references categories(id) on delete set null;
-
--- Optional: Create an index for better performance
-create index if not exists idx_products_category_id on products(category_id);
diff --git a/supabase_schema_site_contents.sql b/supabase_schema_site_contents.sql
deleted file mode 100644
index d2e3996..0000000
--- a/supabase_schema_site_contents.sql
+++ /dev/null
@@ -1,39 +0,0 @@
--- Create site_contents table for dynamic CMS
-CREATE TABLE IF NOT EXISTS site_contents (
- key TEXT PRIMARY KEY,
- value TEXT,
- type TEXT CHECK (type IN ('text', 'image_url', 'html', 'long_text', 'json')),
- section TEXT NOT NULL,
- created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
- updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
-);
-
--- Enable Row Level Security
-ALTER TABLE site_contents ENABLE ROW LEVEL SECURITY;
-
--- Create policies
--- Allow public read access to all site contents (needed for the public website)
-CREATE POLICY "Public read access" ON site_contents
- FOR SELECT TO public USING (true);
-
--- Allow authenticated users (admins) to update content
-CREATE POLICY "Admin update access" ON site_contents
- FOR UPDATE TO authenticated USING (true);
-
--- Allow authenticated users to insert (for initial setup)
-CREATE POLICY "Admin insert access" ON site_contents
- FOR INSERT TO authenticated WITH CHECK (true);
-
--- Insert default contents if they don't exist
-INSERT INTO site_contents (key, value, type, section) VALUES
- ('site_title', 'ParaKasa', 'text', 'general'),
- ('site_description', 'ParaKasa Yönetim Paneli', 'long_text', 'general'),
- ('site_logo', '', 'image_url', 'general'),
- ('contact_phone', '', 'text', 'contact'),
- ('contact_email', '', 'text', 'contact'),
- ('contact_address', '', 'long_text', 'contact'),
- ('social_instagram', '', 'text', 'contact'),
- ('social_youtube', '', 'text', 'contact'),
- ('social_tiktok', '', 'text', 'contact'),
- ('contact_map_embed', '', 'html', 'contact')
-ON CONFLICT (key) DO NOTHING;
diff --git a/supabase_schema_sliders.sql b/supabase_schema_sliders.sql
deleted file mode 100644
index 660d3ae..0000000
--- a/supabase_schema_sliders.sql
+++ /dev/null
@@ -1,88 +0,0 @@
--- Create sliders table
-create table if not exists sliders (
- id uuid default gen_random_uuid() primary key,
- title text not null,
- description text,
- image_url text not null,
- link text,
- "order" integer default 0,
- is_active boolean default true,
- created_at timestamp with time zone default timezone('utc'::text, now()) not null
-);
-
--- Enable RLS
-alter table sliders enable row level security;
-
--- Policies for Sliders Table
-create policy "Public sliders are viewable by everyone."
- on sliders for select
- using ( true );
-
-create policy "Admins can insert sliders."
- on sliders for insert
- with check (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-create policy "Admins can update sliders."
- on sliders for update
- using (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
-create policy "Admins can delete sliders."
- on sliders for delete
- using (
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
- );
-
--- STORAGE POLICIES (Assuming bucket 'images' exists)
--- You must create the 'images' bucket in Supabase Dashboard manually if not exists,
--- or we can try to insert it via SQL if extensions allow, but usually dashboard is safer for buckets.
--- Below policies assume the bucket is named 'images' and is set to PUBLIC.
-
--- 1. Allow public read access to everyone
-create policy "Public Access"
-on storage.objects for select
-using ( bucket_id = 'images' );
-
--- 2. Allow authenticated admins to upload
-create policy "Admin Upload"
-on storage.objects for insert
-with check (
- bucket_id = 'images' and
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
-);
-
--- 3. Allow admins to update/delete their images (or all images)
-create policy "Admin Update Delete"
-on storage.objects for update
-using (
- bucket_id = 'images' and
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
-);
-
-create policy "Admin Delete"
-on storage.objects for delete
-using (
- bucket_id = 'images' and
- exists (
- select 1 from profiles
- where profiles.id = auth.uid() and profiles.role = 'admin'
- )
-);