From dc1b6f13596fc5a87a1ac1f01e1b6e4957a5959c Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Tue, 13 Jan 2026 22:37:50 +0300 Subject: [PATCH] =?UTF-8?q?hata=20d=C3=BCzeltme=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PLANLAMA.md | 5 + app/(dashboard)/dashboard/page.tsx | 2 +- app/(dashboard)/dashboard/settings/page.tsx | 40 ++-- components/dashboard/settings-tabs.tsx | 53 ++++++ components/dashboard/sms-settings-form.tsx | 198 ++++++++++++++++++++ components/ui/tabs.tsx | 55 ++++++ lib/sms/actions.ts | 130 +++++++++++++ lib/sms/netgsm.ts | 88 +++++++++ package-lock.json | 31 +++ package.json | 3 +- supabase_schema_netgsm.sql | 83 ++++++++ 11 files changed, 658 insertions(+), 30 deletions(-) create mode 100644 components/dashboard/settings-tabs.tsx create mode 100644 components/dashboard/sms-settings-form.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 lib/sms/actions.ts create mode 100644 lib/sms/netgsm.ts create mode 100644 supabase_schema_netgsm.sql diff --git a/PLANLAMA.md b/PLANLAMA.md index 332adec..933128c 100644 --- a/PLANLAMA.md +++ b/PLANLAMA.md @@ -21,3 +21,8 @@ - [x] Temel Site Ayarları (Başlık, İletişim). - [x] Ürün Yönetimi (Temel CRUD). - [x] Kategori Yönetimi (Arayüz hazır, veritabanı bekleniyor). + + +## 3. NetGSm Entegrasyonu +- Login için Sms doğrulama entegrasyonu yapılacak. +- \ No newline at end of file diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index ef7c0f5..d8e5508 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -129,7 +129,7 @@ export default async function DashboardPage() { ) } -function PlusIcon(props: any) { +function PlusIcon(props: React.SVGProps) { return (

Ayarlar

- - {/* Site General Settings */} -
- -
- -
- - - - - Hesap Güvenliği - - Şifre ve oturum yönetimi. - - - - - - - -
+ ) } diff --git a/components/dashboard/settings-tabs.tsx b/components/dashboard/settings-tabs.tsx new file mode 100644 index 0000000..997facc --- /dev/null +++ b/components/dashboard/settings-tabs.tsx @@ -0,0 +1,53 @@ +"use client" + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { SiteSettingsForm } from "@/components/dashboard/site-settings-form" +import { SmsSettingsForm } from "@/components/dashboard/sms-settings-form" +import { AppearanceForm } from "@/components/dashboard/appearance-form" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" + +interface SettingsTabsProps { + siteSettings: Record | null + smsSettings: Record | null +} + +export function SettingsTabs({ siteSettings, smsSettings }: SettingsTabsProps) { + return ( + + + Genel + SMS / Bildirimler + Görünüm + Güvenlik + + + + + + + + + + + + + + + + + + Hesap Güvenliği + + Şifre ve oturum yönetimi. + + + + + + + + + + ) +} diff --git a/components/dashboard/sms-settings-form.tsx b/components/dashboard/sms-settings-form.tsx new file mode 100644 index 0000000..34cdb84 --- /dev/null +++ b/components/dashboard/sms-settings-form.tsx @@ -0,0 +1,198 @@ +"use client" + +import { useState } from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card" +import { toast } from "sonner" +import { Loader2, Smartphone, Send } from "lucide-react" +import { updateSmsSettings, sendTestSms } from "@/lib/sms/actions" + +const smsSettingsSchema = z.object({ + username: z.string().min(1, "Kullanıcı adı gereklidir."), + password: z.string().optional(), + header: z.string().min(1, "Başlık (Gönderici Adı) gereklidir."), +}) + +type SmsSettingsValues = z.infer + +interface SmsSettingsFormProps { + initialData: { + username: string + header: string + } | null +} + +export function SmsSettingsForm({ initialData }: SmsSettingsFormProps) { + const [loading, setLoading] = useState(false) + const [testLoading, setTestLoading] = useState(false) + const [testPhone, setTestPhone] = useState("") + + const form = useForm({ + resolver: zodResolver(smsSettingsSchema), + defaultValues: { + username: initialData?.username || "", + header: initialData?.header || "", + password: "", + }, + }) + + const onSubmit = async (data: SmsSettingsValues) => { + setLoading(true) + try { + const result = await updateSmsSettings({ + username: data.username, + password: data.password, + header: data.header, + }) + + if (result.error) { + toast.error(result.error) + return + } + + toast.success("SMS ayarları güncellendi.") + // Don't reset form fully, keeps values visible except password + form.setValue("password", "") + } catch { + toast.error("Bir sorun oluştu.") + } finally { + setLoading(false) + } + } + + const onTestSms = async () => { + if (!testPhone) { + toast.error("Lütfen bir test numarası girin.") + return + } + + setTestLoading(true) + try { + const result = await sendTestSms(testPhone) + if (result.error) { + toast.error("Test başarısız: " + result.error) + } else { + toast.success("Test SMS başarıyla gönderildi!") + } + } catch { + toast.error("Test sırasında bir hata oluştu.") + } finally { + setTestLoading(false) + } + } + + return ( +
+ + + NetGSM Konfigürasyonu + + NetGSM API bilgilerinizi buradan yönetebilirsiniz. Şifre alanı sadece değiştirmek istediğinizde gereklidir. + + + +
+ + ( + + NetGSM Kullanıcı Adı (850...) + + + + + + )} + /> + + ( + + Şifre + + + + + Mevcut şifreyi korumak için boş bırakın. + + + + )} + /> + + ( + + Mesaj Başlığı (Gönderici Adı) + + + + + NetGSM panelinde tanımlı gönderici adınız. + + + + )} + /> + + + + +
+
+ + + + Bağlantı Testi + + Ayarların doğru çalıştığını doğrulamak için test SMS gönderin. + + + +
+
+ setTestPhone(e.target.value)} + /> +

+ Başında 0 olmadan 10 hane giriniz. +

+
+ +
+
+
+
+ ) +} diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..6c01864 --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/lib/sms/actions.ts b/lib/sms/actions.ts new file mode 100644 index 0000000..b71c6fd --- /dev/null +++ b/lib/sms/actions.ts @@ -0,0 +1,130 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { createClient as createSupabaseClient } from "@supabase/supabase-js" +import { revalidatePath } from "next/cache" +import { NetGsmService } from "./netgsm" + +// Admin client for privileged operations (accessing sms_settings) +const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } +) + +async function assertAdmin() { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + if (!user) throw new Error("Oturum açmanız gerekiyor.") + + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') throw new Error("Yetkisiz işlem.") + + return user +} + +export async function getSmsSettings() { + try { + await assertAdmin() + + const { data, error } = await supabaseAdmin + .from('sms_settings') + .select('*') + .single() + + if (error && error.code !== 'PGRST116') { // PGRST116 is 'not found', which is fine initially + throw error + } + + return { data } + } catch (error) { + return { error: (error as Error).message } + } +} + +export async function updateSmsSettings(data: { + username: string + password?: string // Optional if not changing + header: string +}) { + try { + await assertAdmin() + + // Check if exists + const { data: existing } = await supabaseAdmin.from('sms_settings').select('id').single() + + const updates: any = { + username: data.username, + header: data.header, + updated_at: new Date().toISOString() + } + + // Only update password if provided + if (data.password && data.password.trim() !== '') { + updates.password = data.password + } + + if (existing) { + const { error } = await supabaseAdmin + .from('sms_settings') + .update(updates) + .eq('id', existing.id) + if (error) throw error + } else { + // First time setup, password is mandatory if not exists, but we can't easily check 'locally' + // We assume if new, password must be in updates. + if (!data.password) throw new Error("Yeni kurulum için şifre gereklidir.") + + const { error } = await supabaseAdmin + .from('sms_settings') + .insert({ ...updates, password: data.password }) + if (error) throw error + } + + revalidatePath("/dashboard/settings") + return { success: true } + } catch (error) { + return { error: (error as Error).message } + } +} + +export async function sendTestSms(phone: string) { + try { + await assertAdmin() + + // Fetch credentials + const { data: settings } = await supabaseAdmin.from('sms_settings').select('*').single() + if (!settings) throw new Error("SMS ayarları yapılmamış.") + + const mobileService = new NetGsmService({ + username: settings.username, + password: settings.password, + header: settings.header, + apiUrl: settings.api_url + }) + + const result = await mobileService.sendSms(phone, "ParaKasa Test SMS: Entegrasyon basarili.") + + // Log the result + await supabaseAdmin.from('sms_logs').insert({ + phone, + message: "ParaKasa Test SMS: Entegrasyon basarili.", + status: result.success ? 'success' : 'error', + response_code: result.code || result.error + }) + + if (!result.success) { + throw new Error(result.error || "SMS gönderilemedi.") + } + + return { success: true, jobId: result.jobId } + + } catch (error) { + return { error: (error as Error).message } + } +} diff --git a/lib/sms/netgsm.ts b/lib/sms/netgsm.ts new file mode 100644 index 0000000..c64ae4c --- /dev/null +++ b/lib/sms/netgsm.ts @@ -0,0 +1,88 @@ +export interface NetGsmConfig { + username?: string; + password?: string; + header?: string; + apiUrl?: string; +} + +export interface SmsResult { + success: boolean; + jobId?: string; + error?: string; + code?: string; +} + +export class NetGsmService { + private config: NetGsmConfig; + + constructor(config: NetGsmConfig) { + this.config = config; + } + + /** + * Send SMS using NetGSM GET API + * Refer: https://www.netgsm.com.tr/dokuman/#http-get-servisi + */ + async sendSms(phone: string, message: string): Promise { + if (!this.config.username || !this.config.password || !this.config.header) { + return { success: false, error: "NetGSM konfigürasyonu eksik." }; + } + + // Clean phone number (remove spaces, parentheses, etc) + // NetGSM expects 905xxxxxxxxx or just 5xxxxxxxxx, we'll ensure format + let cleanPhone = phone.replace(/\D/g, ''); + if (cleanPhone.startsWith('90')) { + cleanPhone = cleanPhone.substring(0); // keep it + } else if (cleanPhone.startsWith('0')) { + cleanPhone = '9' + cleanPhone; + } else if (cleanPhone.length === 10) { + cleanPhone = '90' + cleanPhone; + } + + try { + // Encode parameters + const params = new URLSearchParams({ + usercode: this.config.username, + password: this.config.password, + gsmno: cleanPhone, + message: message, + msgheader: this.config.header, + dil: 'TR' // Turkish characters support + }); + + const url = `${this.config.apiUrl || 'https://api.netgsm.com.tr/sms/send/get'}?${params.toString()}`; + + const response = await fetch(url); + const textResponse = await response.text(); + + // NetGSM returns a code (e.g. 00 123456789) or error code (e.g. 20) + // Codes starting with 00, 01, 02 indicate success + const code = textResponse.split(' ')[0]; + + if (['00', '01', '02'].includes(code)) { + return { success: true, jobId: textResponse.split(' ')[1] || code, code }; + } else { + const errorMap: Record = { + '20': 'Mesaj metni ya da karakter sınırını (1.000) aştı veya mesaj boş.', + '30': 'Geçersiz kullanıcı adı , şifre veya kullanıcınızın API erişim izni yok.', + '40': 'Gönderici adı (Başlık) sistemde tanımlı değil.', + '70': 'Hatalı sorgu.', + '50': 'Kendi numaranıza veya Rehberden SMS gönderiyorsanız; Abone kendi numarasını veya rehberindeki bir numarayı gönderici kimliği (MsgHeader) olarak kullanamaz.', + '51': 'Aboneliğinizin süresi dolmuş.', + '52': 'Aboneliğiniz bulunmamaktadır.', + '60': 'Bakiyeniz yetersiz.', + '71': 'Gönderim yapmak istediğiniz gsm numarası/numaraları hatalı.' + }; + + return { + success: false, + code, + error: errorMap[code] || `Bilinmeyen hata kodu: ${code} - Yanıt: ${textResponse}` + }; + } + + } catch (error) { + return { success: false, error: (error as Error).message }; + } + } +} diff --git a/package-lock.json b/package-lock.json index e084744..e9d45d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.89.0", "class-variance-authority": "^0.7.1", @@ -1368,6 +1369,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", diff --git a/package.json b/package.json index 7db7e91..cf396d8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@supabase/ssr": "^0.8.0", "@supabase/supabase-js": "^2.89.0", "class-variance-authority": "^0.7.1", @@ -49,4 +50,4 @@ "tw-animate-css": "^1.4.0", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/supabase_schema_netgsm.sql b/supabase_schema_netgsm.sql new file mode 100644 index 0000000..0a26ad9 --- /dev/null +++ b/supabase_schema_netgsm.sql @@ -0,0 +1,83 @@ +-- 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' + ) + );