Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,108 changes: 1,108 additions & 0 deletions ADMIN_PANEL_PLAN.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@

| Sahə | Dəyər |
|---|---|
| **Son tamamlanan tapşırıq** | Mərhələ 8Deploy və CI/CD |
| **Aktiv branch** | `feature/m08-deploy` |
| **Son tamamlanan tapşırıq** | Mərhələ 9Admin Panel (App.tsx routes) |
| **Aktiv branch** | `feature/m09-admin-panel` |
| **Növbəti branch** | Yoxdur |
| **Növbəti tapşırıq** | Bütün layihə 100% tamamlandı! |
| **Növbəti tapşırıq** | Lint + type-check yoxlanışları |
| **Bloklanmış tapşırıq** | Yoxdur |
| **Qeyd** | Flaro tətbiqi 100% tamamlandı. Supabase production qurulumu, migration-lar, Storage, Auth provider-lər, Stripe Canlı Webhook-u, Vercel Production Deploy-u (CSP header-ləri, SSL, Custom Domain), GitHub Actions CI/CD pipeline (`ci.yml`), premium ErrorBoundary, və 145/145 tapşırıq uğurla başa çatdırıldı. |
| **Qeyd** | Admin Panel implementasiyası davam edir. DB migration (020_admin_system.sql), seed script, tip tərifləri, Zustand store, hooks, AdminRoute, AdminLayout, AdminLogin (brute-force), AdminDashboard (real stats), AdminUsers (CRUD), AdminSubscriptions, AdminSettings, i18n (4 dil), Edge Function (admin-user-delete), Session Timeout, App.tsx route-ları — hamısı yaradıldı. Son qalan: lint + type-check yoxlanışı. |

---

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext ts,tsx",
Expand All @@ -19,7 +19,8 @@
"functions:serve": "supabase functions serve",
"supabase:start": "supabase start",
"supabase:stop": "supabase stop",
"supabase:status": "supabase status"
"supabase:status": "supabase status",
"seed:admin": "tsx scripts/seed-admin.ts"
},
"dependencies": {
"@stripe/stripe-js": "^4.0.0",
Expand Down
138 changes: 138 additions & 0 deletions scripts/seed-admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// scripts/seed-admin.ts
// ─────────────────────────────────────────────────────────
// İlk admin hesabı yaratmaq üçün seed script.
// Yalnız development/staging üçün.
// Production-da Supabase Dashboard-dan manual işlət.
// ─────────────────────────────────────────────────────────

import { createClient } from '@supabase/supabase-js'
import * as readline from 'readline'
import * as fs from 'fs'
import * as path from 'path'

// Load environment variables from .env.local or .env
function loadEnv() {
const envPaths = ['.env.local', '.env']
for (const envFile of envPaths) {
const fullPath = path.resolve(process.cwd(), envFile)
if (fs.existsSync(fullPath)) {
const content = fs.readFileSync(fullPath, 'utf-8')
for (const line of content.split('\n')) {
const trimmed = line.trim()
if (trimmed && !trimmed.startsWith('#')) {
const firstEqual = trimmed.indexOf('=')
if (firstEqual !== -1) {
const key = trimmed.substring(0, firstEqual).trim()
let val = trimmed.substring(firstEqual + 1).trim()
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
val = val.slice(1, -1)
}
if (!process.env[key]) {
process.env[key] = val
}
}
}
}
}
}
}

loadEnv()

const supabaseUrl = process.env.VITE_SUPABASE_URL!
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY! // Admin üçün service role

if (!supabaseUrl || !serviceKey) {
console.error('❌ VITE_SUPABASE_URL və SUPABASE_SERVICE_ROLE_KEY .env-də olmalıdır')
process.exit(1)
}

// Service role client — RLS bypass edir (yalnız server-side!)
const supabase = createClient(supabaseUrl, serviceKey, {
auth: { autoRefreshToken: false, persistSession: false }
})

const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const ask = (q: string) => new Promise<string>(res => rl.question(q, res))

async function seedAdmin() {
console.log('\n🔐 Flaro Admin Seed\n')

const email = await ask('Admin email: ')
const password = await ask('Admin şifrəsi (min 12 simvol): ')
const fullName = await ask('Ad Soyad: ')

// Şifrə gücü yoxla
if (password.length < 12) {
console.error('❌ Şifrə minimum 12 simvol olmalıdır')
process.exit(1)
}

// 1. Auth user yarat
const { data: authData, error: authError } = await supabase.auth.admin.createUser({
email,
password,
email_confirm: true, // Email təsdiqinə ehtiyac yoxdur
user_metadata: { full_name: fullName, is_admin: true }
})

if (authError) {
// User artıq mövcuddursa — sadəcə admin et
if (authError.message.includes('already registered') || authError.message.includes('already exists')) {
console.log('⚠️ Bu email artıq qeydiyyatdadır. Admin flag əlavə edilir...')

const { data: existingUser, error: listError } = await supabase.auth.admin.listUsers()
if (listError) {
console.error('❌ İstifadəçi siyahısı gətirilə bilmədi:', listError.message)
process.exit(1)
}

const user = existingUser.users.find(u => u.email === email)

if (!user) {
console.error('❌ İstifadəçi tapılmadı')
process.exit(1)
}

const { error: updateError } = await supabase
.from('profiles')
.update({ is_admin: true, full_name: fullName })
.eq('id', user.id)

if (updateError) {
console.error('❌ Profile yenilənmədi:', updateError.message)
process.exit(1)
}

console.log(`✅ ${email} admin edildi!`)
} else {
console.error('❌ Auth xətası:', authError.message)
process.exit(1)
}
} else {
// 2. Profile-i is_admin=true ilə yenilə
// (handle_new_user trigger artıq profile yaratdı, lakin trigger is_admin-i meta-datadan götürür.
// Yenə də hər ehtimala qarşı manual update edirik)
const { error: profileError } = await supabase
.from('profiles')
.update({ is_admin: true })
.eq('id', authData.user.id)

if (profileError) {
console.error('❌ Profile update xətası:', profileError.message)
process.exit(1)
}

console.log(`\n✅ Admin hesab yaradıldı!`)
console.log(` Email: ${email}`)
console.log(` Ad: ${fullName}`)
console.log(` Login: /admin/login\n`)
}

rl.close()
}

seedAdmin().catch(err => {
console.error('Xəta:', err)
process.exit(1)
})
72 changes: 71 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Routes, Route } from 'react-router-dom'
import { Routes, Route, Navigate } from 'react-router-dom'
import { lazy, Suspense } from 'react'
import { ProtectedRoute } from '@/components/auth/ProtectedRoute'
import Landing from '@/pages/Landing'
import Login from '@/pages/Login'
Expand All @@ -8,6 +9,26 @@ import Pricing from '@/pages/Pricing'
import SharedView from '@/pages/SharedView'
import AuthCallback from '@/pages/AuthCallback'

// Admin pages with Lazy Loading (Code Splitting)
const AdminLogin = lazy(() => import('@/pages/admin/AdminLogin'))
const AdminDashboard = lazy(() => import('@/pages/admin/AdminDashboard'))
const AdminUsers = lazy(() => import('@/pages/admin/AdminUsers'))
const AdminSubscriptions = lazy(() => import('@/pages/admin/AdminSubscriptions'))
const AdminSettings = lazy(() => import('@/pages/admin/AdminSettings'))

// Admin Route & Layout
import { AdminRoute } from '@/components/admin/AdminRoute'
import { AdminLayout } from '@/components/admin/AdminLayout'

// Admin loading fallback
function AdminFallback() {
return (
<div className="flex h-screen items-center justify-center bg-slate-50">
<div className="w-8 h-8 border-4 border-orange-500 border-t-transparent rounded-full animate-spin" />
</div>
)
}

export default function App() {
return (
<Routes>
Expand All @@ -18,6 +39,55 @@ export default function App() {
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/s/:shareToken" element={<SharedView />} />

{/* Admin Login (Public) */}
<Route
path="/admin/login"
element={
<Suspense fallback={<AdminFallback />}>
<AdminLogin />
</Suspense>
}
/>

{/* Admin Protected Routes with layout */}
<Route element={<AdminRoute />}>
<Route element={<AdminLayout />}>
<Route
path="/admin/dashboard"
element={
<Suspense fallback={<AdminFallback />}>
<AdminDashboard />
</Suspense>
}
/>
<Route
path="/admin/users"
element={
<Suspense fallback={<AdminFallback />}>
<AdminUsers />
</Suspense>
}
/>
<Route
path="/admin/subscriptions"
element={
<Suspense fallback={<AdminFallback />}>
<AdminSubscriptions />
</Suspense>
}
/>
<Route
path="/admin/settings"
element={
<Suspense fallback={<AdminFallback />}>
<AdminSettings />
</Suspense>
}
/>
<Route path="/admin" element={<Navigate to="/admin/dashboard" replace />} />
</Route>
</Route>

{/* Protected — giriş tələb olunur */}
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<Dashboard />} />
Expand Down
Loading
Loading