From fef66cd9dd6ef41213837e0eeefae5733f9c0f5c Mon Sep 17 00:00:00 2001 From: ayrisdev Date: Wed, 26 Aug 2026 14:21:53 +0300 Subject: [PATCH] first commit --- .dockerignore | 12 + .env.example | 19 + .gitignore | 39 + Dockerfile | 53 + SETUP.md | 166 ++ app/(auth)/login/page.tsx | 148 ++ app/(dashboard)/dashboard/apps/[id]/page.tsx | 48 + app/(dashboard)/dashboard/apps/new/page.tsx | 202 ++ app/(dashboard)/dashboard/page.tsx | 44 + app/api/apps/[id]/config/[entryId]/route.ts | 57 + app/api/apps/[id]/config/route.ts | 56 + app/api/apps/[id]/rotate-key/route.ts | 26 + app/api/apps/[id]/route.ts | 32 + app/api/apps/route.ts | 48 + app/api/auth/[...nextauth]/route.ts | 5 + app/api/v1/config/route.ts | 62 + app/globals.css | 81 + app/layout.tsx | 39 + app/page.tsx | 5 + components/AppDetailClient.tsx | 1106 ++++++++ components/AppIcon.tsx | 80 + components/DashboardClient.tsx | 239 ++ components/IconPicker.tsx | 197 ++ components/Navbar.tsx | 57 + components/SignOutButton.tsx | 17 + lib/auth.ts | 59 + lib/db.ts | 15 + lib/utils.ts | 80 + next.config.js | 7 + package-lock.json | 2469 ++++++++++++++++++ package.json | 34 + postcss.config.js | 6 + proxy.ts | 11 + public/.gitkeep | 1 + schema.sql | 112 + tailwind.config.ts | 68 + tsconfig.json | 41 + types/index.ts | 42 + 38 files changed, 5783 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 SETUP.md create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(dashboard)/dashboard/apps/[id]/page.tsx create mode 100644 app/(dashboard)/dashboard/apps/new/page.tsx create mode 100644 app/(dashboard)/dashboard/page.tsx create mode 100644 app/api/apps/[id]/config/[entryId]/route.ts create mode 100644 app/api/apps/[id]/config/route.ts create mode 100644 app/api/apps/[id]/rotate-key/route.ts create mode 100644 app/api/apps/[id]/route.ts create mode 100644 app/api/apps/route.ts create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/api/v1/config/route.ts create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components/AppDetailClient.tsx create mode 100644 components/AppIcon.tsx create mode 100644 components/DashboardClient.tsx create mode 100644 components/IconPicker.tsx create mode 100644 components/Navbar.tsx create mode 100644 components/SignOutButton.tsx create mode 100644 lib/auth.ts create mode 100644 lib/db.ts create mode 100644 lib/utils.ts create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 proxy.ts create mode 100644 public/.gitkeep create mode 100644 schema.sql create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json create mode 100644 types/index.ts 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. +

+
+ +
+ +