Initial commit
This commit is contained in:
174
src/lib/appDb.ts
Normal file
174
src/lib/appDb.ts
Normal 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
35
src/lib/auth.ts
Normal 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
50
src/lib/config.ts
Normal 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
85
src/lib/db.ts
Normal 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
35
src/lib/ping.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user