Initial commit

This commit is contained in:
mstfyldz
2026-05-27 16:47:37 +03:00
commit 3ee41864f4
40 changed files with 9041 additions and 0 deletions

174
src/lib/appDb.ts Normal file
View File

@@ -0,0 +1,174 @@
import { Pool } from 'pg'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
// Initialize tables
pool.query(`
CREATE TABLE IF NOT EXISTS ping_logs (
id SERIAL PRIMARY KEY,
site_id TEXT NOT NULL,
status TEXT NOT NULL,
ms INTEGER,
code INTEGER,
error TEXT,
ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT)
);
CREATE INDEX IF NOT EXISTS idx_ping_site ON ping_logs(site_id, ts DESC);
CREATE TABLE IF NOT EXISTS pageviews (
id SERIAL PRIMARY KEY,
domain TEXT NOT NULL,
path TEXT NOT NULL,
referrer TEXT,
country TEXT,
city TEXT,
ua TEXT,
device TEXT,
browser TEXT,
os TEXT,
visitor_id TEXT NOT NULL,
ts BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP)::BIGINT)
);
CREATE INDEX IF NOT EXISTS idx_pv_domain ON pageviews(domain, ts DESC);
CREATE INDEX IF NOT EXISTS idx_pv_ts ON pageviews(ts DESC);
`).catch(console.error)
export type PingLog = {
id: number
site_id: string
status: 'up' | 'down' | 'degraded'
ms: number | null
code: number | null
error: string | null
ts: number
}
export async function insertPingLog(log: Omit<PingLog, 'id' | 'ts'>) {
await pool.query(
`INSERT INTO ping_logs (site_id, status, ms, code, error) VALUES ($1, $2, $3, $4, $5)`,
[log.site_id, log.status, log.ms, log.code, log.error]
)
}
export async function getRecentLogs(siteId: string, limit = 100): Promise<PingLog[]> {
const res = await pool.query(
`SELECT * FROM ping_logs WHERE site_id = $1 ORDER BY ts DESC LIMIT $2`,
[siteId, limit]
)
return res.rows.map(r => ({ ...r, ms: Number(r.ms), code: Number(r.code), ts: Number(r.ts) }))
}
export async function getUptimePercent(siteId: string, hours = 24): Promise<number> {
const since = Math.floor(Date.now() / 1000) - hours * 3600
const res = await pool.query(
`SELECT status FROM ping_logs WHERE site_id = $1 AND ts > $2`,
[siteId, since]
)
const rows = res.rows
if (rows.length === 0) return 100
const up = rows.filter(r => r.status === 'up').length
return Math.round((up / rows.length) * 1000) / 10
}
export async function getLastLog(siteId: string): Promise<PingLog | null> {
const res = await pool.query(
`SELECT * FROM ping_logs WHERE site_id = $1 ORDER BY ts DESC LIMIT 1`,
[siteId]
)
if (res.rows.length === 0) return null
const r = res.rows[0]
return { ...r, ms: Number(r.ms), code: Number(r.code), ts: Number(r.ts) }
}
export type Pageview = {
id: number
domain: string
path: string
referrer: string | null
country: string | null
city: string | null
ua: string | null
device: string | null
browser: string | null
os: string | null
visitor_id: string
ts: number
}
export async function insertPageview(pv: Omit<Pageview, 'id' | 'ts'>) {
await pool.query(
`INSERT INTO pageviews (domain, path, referrer, country, city, ua, device, browser, os, visitor_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[pv.domain, pv.path, pv.referrer, pv.country, pv.city, pv.ua, pv.device, pv.browser, pv.os, pv.visitor_id]
)
}
export async function getAnalyticsStats(domain: string, days = 30) {
const since = Math.floor(Date.now() / 1000) - days * 86400
const totalRes = await pool.query(`SELECT COUNT(*) as n FROM pageviews WHERE domain = $1 AND ts > $2`, [domain, since])
const total = Number(totalRes.rows[0].n)
const uniqueRes = await pool.query(`SELECT COUNT(DISTINCT visitor_id) as n FROM pageviews WHERE domain = $1 AND ts > $2`, [domain, since])
const unique = Number(uniqueRes.rows[0].n)
const topPagesRes = await pool.query(`
SELECT path, COUNT(*) as views
FROM pageviews WHERE domain = $1 AND ts > $2
GROUP BY path ORDER BY views DESC LIMIT 10
`, [domain, since])
const topPages = topPagesRes.rows.map(r => ({ path: r.path, views: Number(r.views) }))
const topReferrersRes = await pool.query(`
SELECT referrer, COUNT(*) as visits
FROM pageviews WHERE domain = $1 AND ts > $2 AND referrer IS NOT NULL AND referrer != ''
GROUP BY referrer ORDER BY visits DESC LIMIT 10
`, [domain, since])
const topReferrers = topReferrersRes.rows.map(r => ({ referrer: r.referrer, visits: Number(r.visits) }))
const byDeviceRes = await pool.query(`
SELECT device, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND device IS NOT NULL
GROUP BY device ORDER BY n DESC
`, [domain, since])
const byDevice = byDeviceRes.rows.map(r => ({ device: r.device, n: Number(r.n) }))
const byBrowserRes = await pool.query(`
SELECT browser, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND browser IS NOT NULL
GROUP BY browser ORDER BY n DESC LIMIT 6
`, [domain, since])
const byBrowser = byBrowserRes.rows.map(r => ({ browser: r.browser, n: Number(r.n) }))
const byOsRes = await pool.query(`
SELECT os, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND os IS NOT NULL
GROUP BY os ORDER BY n DESC LIMIT 6
`, [domain, since])
const byOs = byOsRes.rows.map(r => ({ os: r.os, n: Number(r.n) }))
const byCountryRes = await pool.query(`
SELECT country, COUNT(*) as n
FROM pageviews WHERE domain = $1 AND ts > $2 AND country IS NOT NULL
GROUP BY country ORDER BY n DESC LIMIT 10
`, [domain, since])
const byCountry = byCountryRes.rows.map(r => ({ country: r.country, n: Number(r.n) }))
const dailyRes = await pool.query(`
SELECT to_char(to_timestamp(ts), 'YYYY-MM-DD') as day, COUNT(*) as views, COUNT(DISTINCT visitor_id) as visitors
FROM pageviews WHERE domain = $1 AND ts > $2
GROUP BY day ORDER BY day ASC
`, [domain, since])
const daily = dailyRes.rows.map(r => ({ day: r.day, views: Number(r.views), visitors: Number(r.visitors) }))
return { total, unique, topPages, topReferrers, byDevice, byBrowser, byOs, byCountry, daily }
}
export async function getAllDomains(): Promise<string[]> {
const res = await pool.query(`SELECT DISTINCT domain FROM pageviews ORDER BY domain`)
return res.rows.map(r => r.domain)
}
export default pool

35
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,35 @@
import { SignJWT, jwtVerify } from 'jose'
import { cookies } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
const COOKIE = 'panel_token'
function secret() {
return new TextEncoder().encode(process.env.JWT_SECRET ?? 'dev-secret-change-this')
}
export async function createToken() {
return new SignJWT({ r: 'admin' })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(secret())
}
export async function verifyToken(token: string) {
try { await jwtVerify(token, secret()); return true } catch { return false }
}
export async function isAuthenticated() {
const store = await cookies()
const token = store.get(COOKIE)?.value
return token ? verifyToken(token) : false
}
export async function requireAuth(req: NextRequest): Promise<NextResponse | null> {
const token = req.cookies.get(COOKIE)?.value
if (!token || !(await verifyToken(token)))
return NextResponse.json({ error: 'Yetkisiz' }, { status: 401 })
return null
}
export { COOKIE }

50
src/lib/config.ts Normal file
View File

@@ -0,0 +1,50 @@
import fs from 'fs'
import path from 'path'
const CONFIG_PATH = path.join(process.cwd(), 'config.json')
export interface Site {
id: string
name: string
url: string
interval_min: number
}
export interface Database {
id: string
name: string
host: string
port: number
database: string
username: string
password: string
ssl: boolean
color: string
}
export interface Service {
id: string
name: string
url: string
icon: string
description: string
}
export interface Config {
sites: Site[]
databases: Database[]
services: Service[]
}
export function readConfig(): Config {
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8')
return JSON.parse(raw)
}
export function writeConfig(config: Config): void {
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8')
}
export function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
}

85
src/lib/db.ts Normal file
View File

@@ -0,0 +1,85 @@
import { Pool } from 'pg'
import { Database } from './config'
const pools = new Map<string, Pool>()
function getPool(db: Database): Pool {
if (pools.has(db.id)) return pools.get(db.id)!
const pool = new Pool({
host: db.host, port: db.port, database: db.database,
user: db.username, password: db.password,
ssl: db.ssl ? { rejectUnauthorized: false } : false,
max: 3, connectionTimeoutMillis: 8000,
})
pools.set(db.id, pool)
return pool
}
export async function testDb(db: Database) {
const start = Date.now()
try {
const pool = db.id === '__test__' ? new Pool({
host: db.host, port: db.port, database: db.database,
user: db.username, password: db.password,
ssl: db.ssl ? { rejectUnauthorized: false } : false,
max: 1, connectionTimeoutMillis: 5000,
}) : getPool(db)
const client = await pool.connect()
const res = await client.query('SELECT version()')
client.release()
if (db.id === '__test__') await pool.end()
return { ok: true, ms: Date.now() - start, version: (res.rows[0].version as string).split(' ').slice(0, 2).join(' ') }
} catch (e: unknown) {
return { ok: false, ms: Date.now() - start, error: e instanceof Error ? e.message : 'Hata' }
}
}
export async function getDbStats(db: Database) {
const pool = getPool(db)
const client = await pool.connect()
try {
const [size, conns, tables] = await Promise.all([
client.query(`SELECT pg_size_pretty(pg_database_size(current_database())) AS size`),
client.query(`SELECT count(*) AS active FROM pg_stat_activity WHERE state='active'`),
client.query(`SELECT count(*) AS cnt FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE'`),
])
return {
size: size.rows[0].size,
active_connections: conns.rows[0].active,
table_count: tables.rows[0].cnt,
}
} finally { client.release() }
}
export async function getTables(db: Database) {
const pool = getPool(db)
const client = await pool.connect()
try {
const res = await client.query(`
SELECT t.table_name,
c.reltuples::bigint AS row_count,
pg_size_pretty(pg_total_relation_size(quote_ident(t.table_name))) AS size
FROM information_schema.tables t
JOIN pg_class c ON c.relname = t.table_name
WHERE t.table_schema = 'public' AND t.table_type = 'BASE TABLE'
ORDER BY pg_total_relation_size(quote_ident(t.table_name)) DESC
`)
return res.rows
} finally { client.release() }
}
export async function runQuery(db: Database, sql: string) {
const trimmed = sql.trim().toLowerCase()
const blocked = ['drop','delete','truncate','alter','create','insert','update','grant','revoke']
if (blocked.some(k => trimmed.startsWith(k)))
return { rows: [], fields: [], error: 'Sadece SELECT sorguları çalıştırılabilir.' }
const pool = getPool(db)
const client = await pool.connect()
try {
const res = await client.query(sql)
return { rows: res.rows, fields: res.fields.map(f => f.name), error: null }
} catch (e: unknown) {
return { rows: [], fields: [], error: e instanceof Error ? e.message : 'Sorgu hatası' }
} finally { client.release() }
}

35
src/lib/ping.ts Normal file
View File

@@ -0,0 +1,35 @@
import { Site } from './config'
import { insertPingLog } from './appDb'
export async function pingSite(site: Site): Promise<{
status: 'up' | 'down' | 'degraded'
ms: number
code: number | null
error: string | null
}> {
const start = Date.now()
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 10000)
try {
const res = await fetch(site.url, {
signal: controller.signal,
headers: { 'User-Agent': 'VPSPanel/1.0' },
})
clearTimeout(timer)
const ms = Date.now() - start
const status = res.ok ? (ms > 3000 ? 'degraded' : 'up') : 'down'
return { status, ms, code: res.status, error: null }
} catch (e: unknown) {
clearTimeout(timer)
const ms = Date.now() - start
const isTimeout = e instanceof Error && e.name === 'AbortError'
return { status: 'down', ms, code: null, error: isTimeout ? 'Timeout' : (e instanceof Error ? e.message : 'Hata') }
}
}
export async function pingAndLog(site: Site) {
const result = await pingSite(site)
await insertPingLog({ site_id: site.id, ...result })
return result
}