From 58d7fffc6b215f3a17ebf549bf57cf1ade5619d4 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Sat, 6 Jun 2026 00:18:07 +0300 Subject: [PATCH] feat: add gdrive backup support and update backup logic --- src/app/api/backups/gdrive/callback/route.ts | 68 ++++ src/app/api/backups/gdrive/connect/route.ts | 41 +++ src/app/api/backups/test/route.ts | 22 +- src/app/dashboard/databases/page.tsx | 315 ++++++++++--------- src/lib/backup.ts | 208 ++++++------ src/lib/cronWorker.ts | 6 +- 6 files changed, 388 insertions(+), 272 deletions(-) create mode 100644 src/app/api/backups/gdrive/callback/route.ts create mode 100644 src/app/api/backups/gdrive/connect/route.ts diff --git a/src/app/api/backups/gdrive/callback/route.ts b/src/app/api/backups/gdrive/callback/route.ts new file mode 100644 index 0000000..94610e7 --- /dev/null +++ b/src/app/api/backups/gdrive/callback/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/appDb' + +// GET /api/backups/gdrive/callback?code=xxx&state=db_id +// Google bu endpoint'e yönlendirir, code'u refresh_token ile değiştirip DB'ye kaydeder +export async function GET(req: NextRequest) { + const { searchParams } = req.nextUrl + const code = searchParams.get('code') + const db_id = searchParams.get('state') + const error = searchParams.get('error') + + const proto = req.headers.get('x-forwarded-proto') ?? req.nextUrl.protocol.replace(':', '') + const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? req.nextUrl.host + const redirectUri = `${proto}://${host}/api/backups/gdrive/callback` + const dashboardUrl = `${proto}://${host}/dashboard/databases` + + if (error) { + return NextResponse.redirect(`${dashboardUrl}?gdrive_error=${encodeURIComponent(error)}`) + } + + if (!code || !db_id) { + return NextResponse.redirect(`${dashboardUrl}?gdrive_error=missing_params`) + } + + // DB'den kayıtlı client_id + client_secret'i al + const res = await pool.query(`SELECT credentials FROM backup_configs WHERE db_id = $1 LIMIT 1`, [db_id]) + if (!res.rows[0]) { + return NextResponse.redirect(`${dashboardUrl}?gdrive_error=config_not_found`) + } + + let client_id: string, client_secret: string + try { + const creds = JSON.parse(res.rows[0].credentials) + client_id = creds.client_id + client_secret = creds.client_secret + } catch { + return NextResponse.redirect(`${dashboardUrl}?gdrive_error=invalid_credentials`) + } + + // Code → refresh_token + const tokenRes = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + code, + client_id, + client_secret, + redirect_uri: redirectUri, + grant_type: 'authorization_code', + }), + }) + + if (!tokenRes.ok) { + const err = await tokenRes.text() + return NextResponse.redirect(`${dashboardUrl}?gdrive_error=${encodeURIComponent(err.slice(0, 100))}`) + } + + const tokens = await tokenRes.json() + if (!tokens.refresh_token) { + return NextResponse.redirect(`${dashboardUrl}?gdrive_error=no_refresh_token`) + } + + // refresh_token'ı credentials'a ekle + const newCredentials = JSON.stringify({ client_id, client_secret, refresh_token: tokens.refresh_token }) + await pool.query(`UPDATE backup_configs SET credentials = $1 WHERE db_id = $2`, [newCredentials, db_id]) + + return NextResponse.redirect(`${dashboardUrl}?gdrive_connected=${db_id}`) +} diff --git a/src/app/api/backups/gdrive/connect/route.ts b/src/app/api/backups/gdrive/connect/route.ts new file mode 100644 index 0000000..d39b0f0 --- /dev/null +++ b/src/app/api/backups/gdrive/connect/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server' +import { requireAuth } from '@/lib/auth' +import pool from '@/lib/appDb' + +// GET /api/backups/gdrive/connect?db_id=xxx +// Reads saved client_id from backup_configs, redirects to Google OAuth consent screen +export async function GET(req: NextRequest) { + const authErr = await requireAuth(req) + if (authErr) return authErr + + const db_id = req.nextUrl.searchParams.get('db_id') + if (!db_id) return NextResponse.json({ error: 'db_id gerekli' }, { status: 400 }) + + const res = await pool.query(`SELECT credentials FROM backup_configs WHERE db_id = $1 LIMIT 1`, [db_id]) + if (!res.rows[0]) return NextResponse.json({ error: 'Önce config kaydedin.' }, { status: 404 }) + + let client_id: string + try { + client_id = JSON.parse(res.rows[0].credentials).client_id + } catch { + return NextResponse.json({ error: 'Credentials JSON geçersiz.' }, { status: 400 }) + } + + if (!client_id) return NextResponse.json({ error: 'client_id bulunamadı.' }, { status: 400 }) + + const proto = req.headers.get('x-forwarded-proto') ?? req.nextUrl.protocol.replace(':', '') + const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? req.nextUrl.host + const redirectUri = `${proto}://${host}/api/backups/gdrive/callback` + + const oauthUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + new URLSearchParams({ + client_id, + redirect_uri: redirectUri, + response_type: 'code', + scope: 'https://www.googleapis.com/auth/drive.file', + access_type: 'offline', + prompt: 'consent', + state: db_id, + }) + + return NextResponse.redirect(oauthUrl) +} diff --git a/src/app/api/backups/test/route.ts b/src/app/api/backups/test/route.ts index 90d00d0..6a1eb5d 100644 --- a/src/app/api/backups/test/route.ts +++ b/src/app/api/backups/test/route.ts @@ -1,23 +1,17 @@ import { NextRequest, NextResponse } from 'next/server' import { requireAuth } from '@/lib/auth' import { readConfig } from '@/lib/config' -import { createDbDumpBuffer, uploadToDropbox, uploadToGoogleDrive, backupFilename, testGoogleDriveCredentials } from '@/lib/backup' +import { createDbDumpBuffer, uploadToDropbox, uploadToGoogleDriveOAuth, uploadToGCS, backupFilename } from '@/lib/backup' import pool from '@/lib/appDb' -// POST /api/backups/test -// { db_id, cloud_type, credentials, gdrive_folder_id? } -// → Gerçek yedek alır, buluta yükler, loglar export async function POST(req: NextRequest) { const authErr = await requireAuth(req) if (authErr) return authErr const { db_id, cloud_type, credentials, gdrive_folder_id } = await req.json() - if (!credentials?.trim()) { - return NextResponse.json({ ok: false, error: 'Credentials boş olamaz.' }) - } + if (!credentials?.trim()) return NextResponse.json({ ok: false, error: 'Credentials boş olamaz.' }) - // DB bilgisini al const config = await readConfig() const db = config.databases.find(d => d.id === db_id) if (!db) return NextResponse.json({ ok: false, error: 'Veritabanı bulunamadı.' }) @@ -25,22 +19,21 @@ export async function POST(req: NextRequest) { const filename = backupFilename(db.name) try { - // 1. Dump al const buffer = await createDbDumpBuffer(db) - // 2. Buluta yükle if (cloud_type === 'dropbox') { await uploadToDropbox(credentials.trim(), db.name, filename, buffer) + } else if (cloud_type === 'gcs') { + await uploadToGCS(credentials, gdrive_folder_id, db.name, filename, buffer) } else if (cloud_type === 'gdrive') { - await uploadToGoogleDrive(credentials, db.name, filename, buffer, gdrive_folder_id || undefined) + await uploadToGoogleDriveOAuth(credentials, filename, buffer, gdrive_folder_id || undefined) } else { return NextResponse.json({ ok: false, error: 'Desteklenmeyen cloud türü.' }) } - // 3. Logla await pool.query( `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, - [db_id, 'success', `Manuel test yedek — ${cloud_type}`, buffer.length] + [db_id, 'success', `Manuel yedek — ${cloud_type}`, buffer.length] ) const sizeStr = buffer.length > 1024 * 1024 @@ -49,14 +42,13 @@ export async function POST(req: NextRequest) { return NextResponse.json({ ok: true, - detail: `✓ Yedek alındı ve yüklendi — ${db.name}/${filename} (${sizeStr})`, + detail: `✓ Yedek yüklendi — ${db.name}/${filename} (${sizeStr})`, }) } catch (e: any) { await pool.query( `INSERT INTO backup_logs (db_id, status, message, file_size) VALUES ($1, $2, $3, $4)`, [db_id, 'error', e.message, 0] ).catch(() => {}) - return NextResponse.json({ ok: false, error: e.message }) } } diff --git a/src/app/dashboard/databases/page.tsx b/src/app/dashboard/databases/page.tsx index dd696b2..4f48b0c 100644 --- a/src/app/dashboard/databases/page.tsx +++ b/src/app/dashboard/databases/page.tsx @@ -432,10 +432,6 @@ const CRON_PRESETS = [ { label: 'Her Pazartesi', value: '0 0 * * 1' }, ] -function extractGdriveEmail(json: string): string | null { - try { return JSON.parse(json)?.client_email ?? null } catch { return null } -} - function BackupTab({ db }: { db: Db }) { const [config, setConfig] = useState(null) const [logs, setLogs] = useState([]) @@ -443,8 +439,9 @@ function BackupTab({ db }: { db: Db }) { const [saving, setSaving] = useState(false) const [testing, setTesting] = useState(false) const [testResult, setTestResult] = useState<{ ok: boolean; detail?: string; error?: string } | null>(null) - const [copied, setCopied] = useState(false) - const [f, setF] = useState({ schedule: '0 0 * * *', cloud_type: 'gdrive', credentials: '', gdrive_folder_id: '' }) + const [copiedUri, setCopiedUri] = useState(false) + const [redirectUri, setRedirectUri] = useState('') + const [f, setF] = useState({ schedule: '0 0 * * *', cloud_type: 'gcs', credentials: '', gdrive_folder_id: '' }) const load = useCallback(async () => { setLoading(true) @@ -453,37 +450,56 @@ function BackupTab({ db }: { db: Db }) { const data = await r.json() setConfig(data.config) setLogs(data.logs) - if (data.config) { - setF({ schedule: data.config.schedule, cloud_type: data.config.cloud_type, credentials: data.config.credentials, gdrive_folder_id: data.config.gdrive_folder_id || '' }) - } + if (data.config) setF({ schedule: data.config.schedule, cloud_type: data.config.cloud_type, credentials: data.config.credentials, gdrive_folder_id: data.config.gdrive_folder_id || '' }) } setLoading(false) }, [db.id]) - useEffect(() => { load() }, [load]) + useEffect(() => { + load() + setRedirectUri(window.location.origin + '/api/backups/gdrive/callback') + // URL param kontrol — Google OAuth callback sonrası + const params = new URLSearchParams(window.location.search) + if (params.get('gdrive_connected') === db.id) { + window.history.replaceState({}, '', window.location.pathname) + load() + } else if (params.get('gdrive_error')) { + setTestResult({ ok: false, error: `Google bağlantı hatası: ${params.get('gdrive_error')}` }) + window.history.replaceState({}, '', window.location.pathname) + } + }, [load, db.id]) + + // gdrive credentials yardımcıları + const gdriveFields = (() => { + try { const c = JSON.parse(f.credentials); return { client_id: c.client_id || '', client_secret: c.client_secret || '', connected: !!c.refresh_token } } + catch { return { client_id: '', client_secret: '', connected: false } } + })() + + const updateGdriveField = (key: string, val: string) => { + try { const c = JSON.parse(f.credentials); setF(p => ({ ...p, credentials: JSON.stringify({ ...c, [key]: val }) })) } + catch { setF(p => ({ ...p, credentials: JSON.stringify({ [key]: val }) })) } + } const saveConfig = async () => { setSaving(true) - await fetch('/api/backups', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ db_id: db.id, ...f }), - }) + await fetch('/api/backups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db_id: db.id, ...f }) }) await load() setSaving(false) } - const testCredentials = async () => { - setTesting(true) - setTestResult(null) - const r = await fetch('/api/backups/test', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ db_id: db.id, cloud_type: f.cloud_type, credentials: f.credentials, gdrive_folder_id: f.gdrive_folder_id }), - }) + const connectGDrive = async () => { + setSaving(true) + await fetch('/api/backups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db_id: db.id, ...f }) }) + setSaving(false) + window.location.href = `/api/backups/gdrive/connect?db_id=${db.id}` + } + + const testNow = async () => { + setTesting(true); setTestResult(null) + const r = await fetch('/api/backups/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db_id: db.id, cloud_type: f.cloud_type, credentials: f.credentials, gdrive_folder_id: f.gdrive_folder_id }) }) const result = await r.json() setTestResult(result) - if (result.ok) await load() // log listesini güncelle + if (result.ok) await load() setTesting(false) } @@ -491,17 +507,14 @@ function BackupTab({ db }: { db: Db }) { if (!confirm('Otomatik yedeklemeyi kapat?')) return await fetch('/api/backups', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ db_id: db.id }) }) setConfig(null) - setF({ schedule: '0 0 * * *', cloud_type: 'gdrive', credentials: '', gdrive_folder_id: '' }) - } - - const serviceEmail = f.cloud_type === 'gdrive' ? extractGdriveEmail(f.credentials) : null - - const copyEmail = () => { - if (serviceEmail) { navigator.clipboard.writeText(serviceEmail); setCopied(true); setTimeout(() => setCopied(false), 2000) } + setF({ schedule: '0 0 * * *', cloud_type: 'gcs', credentials: '', gdrive_folder_id: '' }) } if (loading) return
Yükleniyor...
+ const isGdrive = f.cloud_type === 'gdrive' + const credentialsReady = isGdrive ? gdriveFields.connected : !!f.credentials.trim() + return (
@@ -513,17 +526,12 @@ function BackupTab({ db }: { db: Db }) {
Manuel Yedek Al
SQL dump olarak indir
- + İndir - {/* Cloud ayarları */} Otomatik Yedekleme @@ -531,151 +539,153 @@ function BackupTab({ db }: { db: Db }) { - {/* Provider */} + {/* Provider seçimi */}
- {[ - { id: 'gdrive', label: '📁 Google Drive' }, - { id: 'dropbox', label: '📦 Dropbox' }, - ].map(p => ( - ))}
- {/* Google Drive kurulum rehberi */} - {f.cloud_type === 'gdrive' && ( -
-
Google Drive Kurulumu
- {[ + {/* ── GCS ─────────────────────────────────────────────────────── */} + {f.cloud_type === 'gcs' && ( + <> + ( -
- {s.n} - {s.text} -
- ))} -
- )} - - {/* Credentials */} -
- -