commit fef66cd9dd6ef41213837e0eeefae5733f9c0f5c Author: ayrisdev Date: Wed Aug 26 14:21:53 2026 +0300 first commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7376b97 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.gitignore +.env*.local +.env +npm-debug.log* +yarn-debug.log* +yarn-error.log* +Dockerfile +.dockerignore +README.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0bf4ddf --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# ─── PostgreSQL bağlantısı ───────────────────────────────────────── +# Format: postgresql://kullanici:sifre@host:port/veritabani +DATABASE_URL=postgres://postgres:Wn4gK6Jbz6LinyQg32kwkWX6gKcgRfcYe2TDctLqPSBkLM7DmjDF1DxNpq4auxrl@65.109.236.58:29435/postgres + +# SSL (production'da genellikle true, lokal'de false) +DATABASE_SSL=false + +# ─── NextAuth ────────────────────────────────────────────────────── +NEXTAUTH_SECRET=ZHOxnSJZI1hYP2m9BUQL5Ni7y9jA/kRVtaHUqT+eZMY= +NEXTAUTH_URL=http://localhost:3000 + +# ─── Admin credentials ───────────────────────────────────────────── +ADMIN_USERNAME=admin +# Hash üretmek için: +# node -e "const b=require('bcryptjs'); console.log(b.hashSync('şifren', 12))" +ADMIN_PASSWORD_HASH=$2a$12$O6QRIW1TWO9SCAdxYEj8e.qhqiI6DuLPRQ8/YXfLyQ.DLB4ytLL2a + +# ─── Public URL (UI'da kod snippet için) ────────────────────────── +NEXT_PUBLIC_APP_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e50cce --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3640e04 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +# 1. Base image +FROM node:20-alpine AS base + +# 2. Dependencies +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci || npm install --legacy-peer-deps + +# 3. Builder +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Environment variables for Next.js build +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production + +RUN npm run build + +# 4. Runner +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Start the server +CMD ["node", "server.js"] diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..ee6fc48 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,166 @@ +# ConfigVault — Kurulum Rehberi + +## 1. PostgreSQL — Database kurulumu + +Herhangi bir Postgres çalışır: kendi sunucun, Railway, Render, Neon, Fly.io... + +```bash +# Lokal kurulum için (macOS): +brew install postgresql && brew services start postgresql +createdb configvault + +# Veya Docker ile: +docker run -d --name configvault-db \ + -e POSTGRES_DB=configvault \ + -e POSTGRES_PASSWORD=password \ + -p 5432:5432 postgres:16 +``` + +Schema'yı yükle: + +```bash +psql postgresql://postgres:password@localhost:5432/configvault < schema.sql +``` + +Veya `psql`'e bağlanıp `schema.sql` içeriğini yapıştır. + +--- + +## 2. Proje kurulumu + +```bash +npm install +cp .env.example .env.local +``` + +`.env.local` doldur: + +```bash +DATABASE_URL=postgresql://postgres:password@localhost:5432/configvault +DATABASE_SSL=false + +NEXTAUTH_SECRET=$(openssl rand -base64 32) +NEXTAUTH_URL=http://localhost:3000 + +ADMIN_USERNAME=admin +# Şifre hash'i oluştur: +node -e "const b=require('bcryptjs'); console.log(b.hashSync('şifren', 12))" +ADMIN_PASSWORD_HASH=$2a$12$... + +NEXT_PUBLIC_APP_URL=http://localhost:3000 +``` + +## 3. Çalıştır + +```bash +npm run dev +# → http://localhost:3000 +``` + +--- + +## Deploy seçenekleri + +### Vercel + Railway (kolay) +1. Railway'de PostgreSQL aç → connection string'i al +2. Vercel'e deploy et, env variable'ları ekle +3. `DATABASE_SSL=true` yap (Railway SSL ister) + +### Kendi sunucun (VPS) +```bash +# Sunucuda: +npm run build +npm start +# Veya PM2 ile: +pm2 start npm --name configvault -- start +``` + +### Docker Compose (her şey bir arada) +```yaml +version: '3.8' +services: + app: + build: . + ports: ["3000:3000"] + environment: + DATABASE_URL: postgresql://postgres:password@db:5432/configvault + DATABASE_SSL: "false" + NEXTAUTH_SECRET: your-secret + NEXTAUTH_URL: https://configvault.yourdomain.com + ADMIN_USERNAME: admin + ADMIN_PASSWORD_HASH: $2a$12$... + NEXT_PUBLIC_APP_URL: https://configvault.yourdomain.com + depends_on: [db] + db: + image: postgres:16 + environment: + POSTGRES_DB: configvault + POSTGRES_PASSWORD: password + volumes: + - pgdata:/var/lib/postgresql/data + - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql +volumes: + pgdata: +``` + +--- + +## React Native — Kullanım + +Uygulamanın içinde `lib/config.ts` oluştur: + +```typescript +import AsyncStorage from '@react-native-async-storage/async-storage' + +const CONFIG_URL = 'https://configvault.yourdomain.com/api/v1/config' +const APP_API_KEY = 'buraya_dashboard_api_key' // Dashboard > App > API Key tab + +export async function initConfig(env = 'production') { + try { + const res = await fetch(`${CONFIG_URL}?env=${env}`, { + headers: { 'X-Api-Key': APP_API_KEY }, + }) + if (!res.ok) throw new Error('Config fetch failed') + const config = await res.json() + + // Offline fallback için cache + await AsyncStorage.setItem('app_config', JSON.stringify(config)) + return config + } catch { + const cached = await AsyncStorage.getItem('app_config') + if (cached) return JSON.parse(cached) + throw new Error('No config available') + } +} +``` + +`App.tsx`: +```typescript +import { initConfig } from './lib/config' +import { createClient } from '@supabase/supabase-js' + +let supabase: any + +export default function App() { + useEffect(() => { + initConfig('production').then(config => { + supabase = createClient(config.SUPABASE_URL, config.SUPABASE_ANON_KEY) + // artık config.ICON_URL, config.API_ENDPOINT vs. kullanabilirsin + }) + }, []) +} +``` + +**Config değişince:** Dashboard'a gir → ilgili key'i güncelle → kaydet. +Tüm kullanıcılar bir sonraki açılışta yeni değeri alır. Rebuild yok. 🎉 + +--- + +## Yeni özellik ekleme + +**Yeni config tipi:** `schema.sql` → `type CHECK` + `types/index.ts` + `AppDetailClient.tsx` select + +**Yeni tablo/özellik:** +1. `schema.sql`'e ekle, `psql` ile migrate et +2. `app/api/apps/[id]/` altına route ekle +3. Dashboard'a tab/section ekle diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..038255b --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,148 @@ +'use client' + +import { useState } from 'react' +import { signIn } from 'next-auth/react' +import { useRouter } from 'next/navigation' +import { Shield, Lock, ArrowRight, Loader2, KeyRound, Sparkles, Eye, EyeOff } from 'lucide-react' + +export default function LoginPage() { + const router = useRouter() + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setLoading(true) + setError('') + + const result = await signIn('credentials', { + username, + password, + redirect: false, + }) + + setLoading(false) + if (result?.ok) { + router.push('/dashboard') + } else { + setError('Geçersiz kullanıcı adı veya şifre') + } + } + + return ( +
+
+ {/* Brand header */} +
+
+ +
+

+ ConfigVault + + v1.0 + +

+

+ Merkezi Remote Config & Gizli Anahtar Yönetimi +

+
+ + {/* Glass Card */} +
+ {/* Subtle top glow line */} +
+ +
+
+ +
+ setUsername(e.target.value)} + required + autoFocus + className="w-full glass-input rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:outline-none text-sm font-medium" + placeholder="admin" + /> +
+
+ +
+
+ +
+
+ setPassword(e.target.value)} + required + className="w-full glass-input rounded-xl px-4 py-3 pr-11 text-white placeholder-slate-500 focus:outline-none text-sm font-medium" + placeholder="••••••••" + /> + +
+
+ + {error && ( +
+
+ {error} +
+ )} + + + + + {/* Security details pill */} +
+
+ + JWT & Bcrypt Korumalı +
+
+ + SSL / TLS Güvenli +
+
+
+ + {/* Footer info */} +

+ ConfigVault • Tüm ortam ve dinamik ayarlarınız güvende +

+
+
+ ) +} diff --git a/app/(dashboard)/dashboard/apps/[id]/page.tsx b/app/(dashboard)/dashboard/apps/[id]/page.tsx new file mode 100644 index 0000000..9c27b4b --- /dev/null +++ b/app/(dashboard)/dashboard/apps/[id]/page.tsx @@ -0,0 +1,48 @@ +import sql from '@/lib/db' +import { notFound } from 'next/navigation' +import { Navbar } from '@/components/Navbar' +import { AppDetailClient } from '@/components/AppDetailClient' + +export const dynamic = 'force-dynamic' + +async function getAppData(id: string) { + const [app] = await sql`SELECT * FROM apps WHERE id = ${id}` + if (!app) return null + + const environments = await sql` + SELECT * FROM environments WHERE app_id = ${id} ORDER BY name + ` + + const envIds = environments.map((e: any) => e.id) + const configs = envIds.length + ? await sql`SELECT * FROM config_entries WHERE environment_id = ANY(${envIds}) ORDER BY key` + : [] + + const auditLogs = await sql` + SELECT * FROM audit_logs WHERE app_id = ${id} + ORDER BY created_at DESC LIMIT 30 + ` + + return { app, environments, configs, auditLogs } +} + +export default async function AppDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const data = await getAppData(id) + if (!data) notFound() + + return ( +
+ + +
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/apps/new/page.tsx b/app/(dashboard)/dashboard/apps/new/page.tsx new file mode 100644 index 0000000..29bcd89 --- /dev/null +++ b/app/(dashboard)/dashboard/apps/new/page.tsx @@ -0,0 +1,202 @@ +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { ArrowLeft, Sparkles, Loader2, Rocket, Globe, Terminal, Smartphone } from 'lucide-react' +import { Navbar } from '@/components/Navbar' +import { IconPicker } from '@/components/IconPicker' +import { slugify } from '@/lib/utils' + +export default function NewAppPage() { + const router = useRouter() + const [name, setName] = useState('') + const [slug, setSlug] = useState('') + const [description, setDescription] = useState('') + const [iconUrl, setIconUrl] = useState('') + const [slugEdited, setSlugEdited] = useState(false) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + function handleNameChange(value: string) { + setName(value) + if (!slugEdited) { + setSlug(slugify(value)) + } + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setLoading(true) + setError('') + + try { + const res = await fetch('/api/apps', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, slug, description, icon_url: iconUrl || null }), + }) + + const data = await res.json() + setLoading(false) + + if (!res.ok) { + setError(data.error ?? 'Uygulama oluşturulurken bir hata oluştu.') + return + } + + router.push(`/dashboard/apps/${data.id}`) + } catch { + setLoading(false) + setError('Ağ hatası oluştu, lütfen tekrar deneyin.') + } + } + + return ( +
+ + +
+ + + Uygulamalara Geri Dön + + + {/* Page Title */} +
+

+ Yeni Uygulama Oluştur +
+ +
+

+

+ Uygulamanız için dinamik config yönetimi ve otomatik 3 ortam (Development, Staging, Production) kurulacaktır. +

+
+ + {/* Creation Card */} +
+
+ +
+
+ + handleNameChange(e.target.value)} + required + autoFocus + placeholder="Örn: Trendyol Satıcı Paneli, Mobil E-Ticaret" + className="w-full glass-input rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:outline-none text-sm font-medium" + /> +
+ +
+ +
+ + app/ + + { + setSlug(e.target.value) + setSlugEdited(true) + }} + required + placeholder="mobil-e-ticaret" + pattern="[a-z0-9-]+" + className="flex-1 bg-transparent px-3.5 py-3 text-white placeholder-slate-500 focus:outline-none text-sm font-mono" + /> +
+

+ Yalnızca küçük harfler, rakamlar ve tire (-) kullanılabilir. API isteklerinde kullanılacaktır. +

+
+ +
+ +