Migrate to local PG & Update Solana Checkout

This commit is contained in:
2026-03-12 19:22:10 +03:00
parent e7f9325b1c
commit 321f25a15c
16 changed files with 1445 additions and 4980 deletions

View File

@@ -28,10 +28,8 @@ export default function AdminLayout({
}) {
const pathname = usePathname();
const router = useRouter();
const supabase = createClient();
const handleSignOut = async () => {
await supabase.auth.signOut();
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/login');
router.refresh();
};

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
import {
Search,
Filter,
@@ -13,49 +13,45 @@ import TransactionSearch from '@/components/admin/TransactionSearch';
import TransactionStatusFilter from '@/components/admin/TransactionStatusFilter';
async function getTransactions(filters: { merchant_id?: string; q?: string; status?: string }) {
let query = supabaseAdmin
.from('transactions')
.select('*, merchants(name)')
.order('created_at', { ascending: false });
let sql = `
SELECT t.*, m.name as merchant_name
FROM transactions t
LEFT JOIN merchants m ON t.merchant_id = m.id
WHERE 1=1
`;
let params: any[] = [];
let paramIndex = 1;
if (filters.merchant_id) {
query = query.eq('merchant_id', filters.merchant_id);
sql += ` AND t.merchant_id = $${paramIndex++}`;
params.push(filters.merchant_id);
}
if (filters.status) {
query = query.eq('status', filters.status);
sql += ` AND t.status = $${paramIndex++}`;
params.push(filters.status);
}
if (filters.q) {
// First, search for merchants matching the name to get their IDs
const { data: matchedMerchants } = await supabaseAdmin
.from('merchants')
.select('id')
.ilike('name', `%${filters.q}%`);
const merchantIds = matchedMerchants?.map(m => m.id) || [];
// Construct OR query parts
let orParts = [
`stripe_pi_id.ilike.%${filters.q}%`,
`source_ref_id.ilike.%${filters.q}%`,
`customer_name.ilike.%${filters.q}%`
];
if (merchantIds.length > 0) {
orParts.push(`merchant_id.in.(${merchantIds.join(',')})`);
}
query = query.or(orParts.join(','));
sql += ` AND (
t.stripe_pi_id ILIKE $${paramIndex} OR
t.source_ref_id ILIKE $${paramIndex} OR
t.customer_name ILIKE $${paramIndex} OR
m.name ILIKE $${paramIndex}
)`;
params.push(`%${filters.q}%`);
paramIndex++;
}
const { data, error } = await query;
sql += ` ORDER BY t.created_at DESC`;
if (error) {
try {
const { rows } = await db.query(sql, params);
return rows.map(r => ({ ...r, merchants: r.merchant_name ? { name: r.merchant_name } : null }));
} catch (error) {
console.error('Fetch error:', error);
return [];
}
return data;
}
export default async function TransactionsPage(props: {

View File

@@ -0,0 +1,56 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { cookies } from 'next/headers';
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key-12345';
export async function POST(request: Request) {
try {
const { email, password } = await request.json();
if (!email || !password) {
return NextResponse.json({ error: 'Email ve şifre zorunludur.' }, { status: 400 });
}
const res = await db.query('SELECT * FROM admin_users WHERE email = $1', [email]);
const user = res.rows[0];
if (!user || (!user.password_hash && password !== 'password123')) {
return NextResponse.json({ error: 'Geçersiz email veya şifre.' }, { status: 401 });
}
// Verify password
let isValid = false;
if (user.password_hash) {
isValid = await bcrypt.compare(password, user.password_hash);
} else if (password === 'password123') { // Fallback if someone forgot to run the init script
isValid = true;
}
if (!isValid) {
return NextResponse.json({ error: 'Geçersiz email veya şifre.' }, { status: 401 });
}
// Generate token
const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, {
expiresIn: '1d',
});
// Set cookie
const cookieStore = await cookies();
cookieStore.set('admin_session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24, // 1 day
});
return NextResponse.json({ success: true, user: { id: user.id, email: user.email } });
} catch (error: any) {
console.error('Login error:', error);
return NextResponse.json({ error: 'Giriş sırasında bir hata oluştu.' }, { status: 500 });
}
}

View File

@@ -0,0 +1,8 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST() {
const cookieStore = await cookies();
cookieStore.delete('admin_session');
return NextResponse.json({ success: true });
}

View File

@@ -1,36 +1,40 @@
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
import { PaymentProviderFactory } from '@/lib/payment-providers';
export async function POST(req: NextRequest) {
try {
const { amount, currency, ref_id, callback_url, customer_name, customer_phone, merchant_id } = await req.json();
if (!amount || !currency || !merchant_id) {
if (!amount || !currency) {
return NextResponse.json(
{ error: 'Tutar, para birimi ve firma ID zorunludur.' },
{ error: 'Tutar ve para birimi zorunludur.' },
{ status: 400 }
);
}
let merchant;
// 1. Fetch Merchant to check provider (Support both UUID and Short ID)
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(merchant_id);
const query = supabaseAdmin
.from('merchants')
.select('*');
if (isUUID) {
query.eq('id', merchant_id);
if (merchant_id) {
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(merchant_id);
let result;
if (isUUID) {
result = await db.query('SELECT * FROM merchants WHERE id = $1', [merchant_id]);
} else {
result = await db.query('SELECT * FROM merchants WHERE short_id = $1', [merchant_id]);
}
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
}
merchant = result.rows[0];
} else {
query.eq('short_id', merchant_id);
}
const { data: merchant, error: merchantError } = await query.single();
if (merchantError || !merchant) {
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
// For demo purposes on the landing page, fetch any existing merchant
const result = await db.query('SELECT * FROM merchants LIMIT 1');
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Sistemde kayıtlı firma bulunamadı (Önce admin panelinden firma ekleyin).' }, { status: 404 });
}
merchant = result.rows[0];
}
// Use the actual UUID for DB operations
@@ -67,26 +71,19 @@ export async function POST(req: NextRequest) {
}
// 3. Log transaction in Supabase
const { error: dbError } = await supabaseAdmin
.from('transactions')
.insert({
amount,
currency,
status: 'pending',
stripe_pi_id: providerTxId, // We keep using this column for now or we could use provider_tx_id if we updated schema
source_ref_id: ref_id,
customer_name,
customer_phone,
callback_url,
merchant_id: resolvedMerchantId,
provider: provider,
metadata: {
nextAction,
redirectUrl
}
});
if (dbError) {
try {
await db.query(`
INSERT INTO transactions (
amount, currency, status, stripe_pi_id, source_ref_id,
customer_name, customer_phone, callback_url, merchant_id,
provider, metadata
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, [
amount, currency, 'pending', providerTxId, ref_id,
customer_name, customer_phone, callback_url, resolvedMerchantId,
provider, JSON.stringify({ nextAction, redirectUrl })
]);
} catch (dbError) {
console.error('Database log error:', dbError);
}

View File

@@ -1,27 +1,66 @@
import { NextResponse } from 'next/server';
import { CryptoEngine } from '@/lib/crypto-engine';
import { db } from '@/lib/db';
export async function POST(request: Request) {
try {
const body = await request.json();
const { txId, merchantAddress } = body;
const { txId, merchantAddress, amount, currency } = body;
// In a real app, you would fetch the private key for this txId from a secure DB/Vault
// For demo: We just simulate the result of a sweep
console.log(`[API] Checking and Sweeping for Transaction: ${txId}`);
// Mock success response
// This is a demo integration. In a real application:
// 1. We would look up the transaction ID from the DB
// 2. Fetch the temporary wallet private key created for that specific TX
// 3. We use the platform address defined in our .env or settings
// For this demo, we'll use the user's devnet wallet as both the source (temp wallet) and platform
const demoTempWalletPrivKey = "3Ab6AyfDDWquPJ6ySjHmQmiW3USg7CuDxJSNtrNQySsXj5v4KfBKcw9vnK1Rrfwm6RYq43PdKjiNZekgtNzGsNm2";
const platformAddress = "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe";
// Ensure we have a merchant address or use a fallback for testing
const targetMerchant = merchantAddress || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe"; // using same for demo
// Initialize Crypto Engine for Solana
const cryptoEngine = new CryptoEngine('SOLANA');
console.log("Starting Sweep Process on SOLANA DEVNET...");
// Attempt the sweep (this will do a real devnet transaction if uncommented in engine)
const sweepResult = await cryptoEngine.sweepFunds(
demoTempWalletPrivKey,
targetMerchant,
platformAddress,
'SOL' // Using native SOL for demo
);
if (!sweepResult.success) {
throw new Error("Süpürme işlemi başarısız oldu.");
}
// --- UPDATE DATABASE STATUS ---
// Marks the transaction as succeeded so it updates in the Admin dashboard
try {
await db.query(
`UPDATE transactions
SET status = 'succeeded'
WHERE stripe_pi_id = $1`,
[txId]
);
} catch (dbError) {
console.error('[API] Failed to update transaction status in DB:', dbError);
}
return NextResponse.json({
success: true,
message: "Ödeme başarıyla doğrulandı ve dağıtıldı.",
message: "Ödeme başarıyla doğrulandı ve dağıtıldı (Solana Devnet).",
split: {
platform: "1.00 USDT (%1)",
merchant: "99.00 USDT (%99)"
platform: "%1",
merchant: "%99"
},
hashes: {
platform: "0x" + Math.random().toString(16).slice(2, 66),
merchant: "0x" + Math.random().toString(16).slice(2, 66)
platform: sweepResult.platformTx,
merchant: sweepResult.merchantTx
}
});

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
export async function POST(req: NextRequest) {
try {
@@ -22,24 +22,21 @@ export async function POST(req: NextRequest) {
return result;
};
const { data, error } = await supabaseAdmin
.from('merchants')
.insert([{
name,
webhook_url,
short_id: generateShortId(),
payment_provider: payment_provider || 'stripe',
provider_config: provider_config || {}
}])
.select()
.single();
const shortId = generateShortId();
const provider = payment_provider || 'stripe';
const configStr = provider_config ? JSON.stringify(provider_config) : '{}';
if (error) {
console.error('Merchant creation error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
const result = await db.query(
`INSERT INTO merchants (name, webhook_url, short_id, payment_provider, provider_config)
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
[name, webhook_url, shortId, provider, configStr]
);
if (result.rows.length === 0) {
throw new Error('Could not insert merchant');
}
return NextResponse.json(data);
return NextResponse.json(result.rows[0]);
} catch (err: any) {
console.error('Internal Error:', err);
return NextResponse.json(
@@ -51,16 +48,8 @@ export async function POST(req: NextRequest) {
export async function GET() {
try {
const { data, error } = await supabaseAdmin
.from('merchants')
.select('*')
.order('created_at', { ascending: false });
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data);
const result = await db.query('SELECT * FROM merchants ORDER BY created_at DESC');
return NextResponse.json(result.rows);
} catch (err: any) {
return NextResponse.json(
{ error: `Internal Server Error: ${err.message}` },

View File

@@ -158,10 +158,10 @@ function CheckoutContent() {
</div>
{paymentMethod === 'crypto' ? (
<CryptoCheckout
amount={amount}
currency={currency}
txId={paymentData?.transactionId || 'TX-8231'}
<CryptoCheckout
amount={amount}
currency={currency}
txId={paymentData?.clientSecret || 'TX-8231'}
onSuccess={(hash) => {
setTimeout(() => {
window.location.href = `${callbackUrl}?status=success&tx_hash=${hash}`;
@@ -229,26 +229,6 @@ function CheckoutContent() {
);
}
<div className="mt-8 flex justify-center lg:justify-start">
<Link href={callbackUrl} className="flex items-center gap-2 text-sm font-semibold text-gray-500 hover:text-gray-900 transition translate-x-0 hover:-translate-x-1 duration-200">
<ArrowLeft size={16} />
Mağazaya Dön
</Link>
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<footer className="py-12 border-t border-gray-100 text-center">
<p className="text-[#94A3B8] text-[10px] font-medium tracking-tight uppercase">
© 2026 P2CGateway Inc. Tüm hakları saklıdır.
</p>
</footer>
</div>
);
}
export default function CheckoutPage() {
return (

View File

@@ -1,7 +1,6 @@
'use client';
import { useState } from 'react';
import { createClient } from '@/utils/supabase/client';
import { useRouter } from 'next/navigation';
import { Lock, Mail, Loader2, Wallet } from 'lucide-react';
@@ -11,24 +10,29 @@ export default function LoginPage() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const supabase = createClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (error) {
setError(error.message);
if (!res.ok) {
setError(data.error || 'Login failed');
} else {
router.push('/admin');
router.refresh();
}
} catch (err: any) {
setError('Bir ağ hatası oluştu, lütfen tekrar deneyin.');
} finally {
setIsLoading(false);
} else {
router.push('/admin');
router.refresh();
}
};