borsa / nextjs-app /src /app /api /admin /route.ts
veteroner's picture
fix: RLS infinite recursion on user_profiles blocking admin panel access
e4f1613
import { createClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'
export const dynamic = 'force-dynamic'
// Service role client - only for admin operations
function getAdminClient() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!url || !serviceKey) {
throw new Error('Supabase credentials missing')
}
return createClient(url, serviceKey, {
auth: { autoRefreshToken: false, persistSession: false }
})
}
// Verify the requesting user is admin
async function verifyAdmin(request: Request): Promise<{ isAdmin: boolean; userId: string | null; error?: string }> {
try {
const authHeader = request.headers.get('authorization')
if (!authHeader) {
return { isAdmin: false, userId: null, error: 'Authorization header eksik' }
}
const token = authHeader.replace('Bearer ', '')
const admin = getAdminClient()
const { data: { user }, error } = await admin.auth.getUser(token)
if (error || !user) {
return { isAdmin: false, userId: null, error: 'Geçersiz token' }
}
// Check if user is admin in user_profiles
const { data: profile, error: profileError } = await admin
.from('user_profiles')
.select('is_admin')
.eq('id', user.id)
.single()
if (!profile?.is_admin) {
return { isAdmin: false, userId: user.id, error: 'Admin yetkisi yok' }
}
return { isAdmin: true, userId: user.id }
} catch {
return { isAdmin: false, userId: null, error: 'Doğrulama hatası' }
}
}
// ─── GET: Dashboard stats & user list ───
export async function GET(request: Request) {
const auth = await verifyAdmin(request)
if (!auth.isAdmin) {
return NextResponse.json({ error: auth.error }, { status: 403 })
}
const { searchParams } = new URL(request.url)
const action = searchParams.get('action') || 'dashboard'
const admin = getAdminClient()
try {
if (action === 'dashboard') {
// Parallel fetch all stats
const [usersRes, stocksRes, newsRes, predictionsRes, settingsRes, logsRes] = await Promise.all([
admin.auth.admin.listUsers({ perPage: 1000 }),
admin.from('stocks').select('id', { count: 'exact', head: true }),
admin.from('news_articles').select('id', { count: 'exact', head: true }),
admin.from('ml_predictions').select('id', { count: 'exact', head: true }),
admin.from('app_settings').select('*'),
admin.from('admin_logs').select('*').order('created_at', { ascending: false }).limit(20),
])
const users = usersRes.data?.users || []
const now = new Date()
const last24h = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const last7d = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
const recentUsers = users.filter(u => new Date(u.created_at) > last7d)
const activeUsers = users.filter(u => u.last_sign_in_at && new Date(u.last_sign_in_at) > last24h)
return NextResponse.json({
stats: {
totalUsers: users.length,
recentUsers: recentUsers.length,
activeUsersLast24h: activeUsers.length,
totalStocks: stocksRes.count || 0,
totalNews: newsRes.count || 0,
totalPredictions: predictionsRes.count || 0,
},
settings: settingsRes.data || [],
logs: logsRes.data || [],
})
}
if (action === 'users') {
const { data: { users }, error } = await admin.auth.admin.listUsers({ perPage: 1000 })
if (error) throw error
// Get profiles for admin flags
const { data: profiles } = await admin.from('user_profiles').select('id, is_admin, is_banned, display_name, last_login_at, login_count')
const profileMap = new Map((profiles || []).map(p => [p.id, p]))
const enrichedUsers = users.map(u => ({
id: u.id,
email: u.email,
created_at: u.created_at,
last_sign_in_at: u.last_sign_in_at,
email_confirmed_at: u.email_confirmed_at,
is_admin: profileMap.get(u.id)?.is_admin || false,
is_banned: profileMap.get(u.id)?.is_banned || false,
display_name: profileMap.get(u.id)?.display_name || null,
login_count: profileMap.get(u.id)?.login_count || 0,
}))
return NextResponse.json({ users: enrichedUsers })
}
if (action === 'settings') {
const { data, error } = await admin.from('app_settings').select('*')
if (error) throw error
return NextResponse.json({ settings: data })
}
if (action === 'logs') {
const limit = parseInt(searchParams.get('limit') || '50', 10)
const { data, error } = await admin
.from('admin_logs')
.select('*')
.order('created_at', { ascending: false })
.limit(limit)
if (error) throw error
return NextResponse.json({ logs: data })
}
return NextResponse.json({ error: 'Geçersiz aksiyon' }, { status: 400 })
} catch (e: unknown) {
return NextResponse.json({ error: e instanceof Error ? e.message : 'Sunucu hatası' }, { status: 500 })
}
}
// ─── POST: Admin actions ───
export async function POST(request: Request) {
const auth = await verifyAdmin(request)
if (!auth.isAdmin) {
return NextResponse.json({ error: auth.error }, { status: 403 })
}
const admin = getAdminClient()
const body = await request.json()
const { action } = body
try {
// Log the action
const logAction = async (actionName: string, targetType: string, targetId: string, details: Record<string, unknown>) => {
await admin.from('admin_logs').insert({
admin_id: auth.userId,
action: actionName,
target_type: targetType,
target_id: targetId,
details,
})
}
// UUID v4 format validation
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
// ─── User Management ───
if (action === 'ban_user') {
const { userId } = body
if (!userId || !UUID_REGEX.test(userId)) {
return NextResponse.json({ error: 'Geçersiz userId formatı (UUID bekleniyor)' }, { status: 400 })
}
// Upsert profile with is_banned
await admin.from('user_profiles').upsert(
{ id: userId, is_banned: true },
{ onConflict: 'id' }
)
await logAction('ban_user', 'user', userId, { banned: true })
return NextResponse.json({ success: true, message: 'Kullanıcı engellendi' })
}
if (action === 'unban_user') {
const { userId } = body
if (!userId || !UUID_REGEX.test(userId)) {
return NextResponse.json({ error: 'Geçersiz userId formatı (UUID bekleniyor)' }, { status: 400 })
}
await admin.from('user_profiles').upsert(
{ id: userId, is_banned: false },
{ onConflict: 'id' }
)
await logAction('unban_user', 'user', userId, { banned: false })
return NextResponse.json({ success: true, message: 'Engel kaldırıldı' })
}
if (action === 'make_admin') {
const { userId } = body
if (!userId || !UUID_REGEX.test(userId)) {
return NextResponse.json({ error: 'Geçersiz userId formatı (UUID bekleniyor)' }, { status: 400 })
}
await admin.from('user_profiles').upsert(
{ id: userId, is_admin: true },
{ onConflict: 'id' }
)
await logAction('make_admin', 'user', userId, { admin: true })
return NextResponse.json({ success: true, message: 'Admin yetkisi verildi' })
}
if (action === 'remove_admin') {
const { userId } = body
if (!userId || !UUID_REGEX.test(userId)) {
return NextResponse.json({ error: 'Geçersiz userId formatı (UUID bekleniyor)' }, { status: 400 })
}
if (userId === auth.userId) {
return NextResponse.json({ error: 'Kendi admin yetkinizi kaldıramazsınız' }, { status: 400 })
}
await admin.from('user_profiles').upsert(
{ id: userId, is_admin: false },
{ onConflict: 'id' }
)
await logAction('remove_admin', 'user', userId, { admin: false })
return NextResponse.json({ success: true, message: 'Admin yetkisi kaldırıldı' })
}
if (action === 'delete_user') {
const { userId } = body
if (!userId || !UUID_REGEX.test(userId)) {
return NextResponse.json({ error: 'Geçersiz userId formatı (UUID bekleniyor)' }, { status: 400 })
}
if (userId === auth.userId) {
return NextResponse.json({ error: 'Kendi hesabınızı silemezsiniz' }, { status: 400 })
}
await admin.auth.admin.deleteUser(userId)
await logAction('delete_user', 'user', userId, {})
return NextResponse.json({ success: true, message: 'Kullanıcı silindi' })
}
if (action === 'reset_password') {
const { userId, email } = body
if (!userId || !UUID_REGEX.test(userId)) {
return NextResponse.json({ error: 'Geçersiz userId formatı (UUID bekleniyor)' }, { status: 400 })
}
if (!email || typeof email !== 'string' || !email.includes('@')) {
return NextResponse.json({ error: 'Geçerli bir email adresi gerekli' }, { status: 400 })
}
// Using Supabase admin to generate a recovery link
const { data, error } = await admin.auth.admin.generateLink({
type: 'recovery',
email,
})
if (error) throw error
await logAction('reset_password', 'user', userId, { email })
// SECURITY: Never expose the recovery link in API response.
// The link is sent to the user's email by Supabase.
return NextResponse.json({ success: true, message: 'Şifre sıfırlama linki oluşturuldu' })
}
// ─── Settings Management ───
if (action === 'update_setting') {
const { key, value } = body
const { error } = await admin.from('app_settings').upsert(
{ key, value: JSON.stringify(value), updated_by: auth.userId, updated_at: new Date().toISOString() },
{ onConflict: 'key' }
)
if (error) throw error
await logAction('update_setting', 'config', key, { value })
return NextResponse.json({ success: true, message: `${key} güncellendi` })
}
// ─── System Actions ───
if (action === 'send_announcement') {
const { message } = body
await admin.from('app_settings').upsert(
{ key: 'announcement', value: JSON.stringify(message), updated_by: auth.userId, updated_at: new Date().toISOString() },
{ onConflict: 'key' }
)
await logAction('announcement', 'system', 'announcement', { message })
return NextResponse.json({ success: true, message: 'Duyuru yayınlandı' })
}
return NextResponse.json({ error: 'Geçersiz aksiyon' }, { status: 400 })
} catch (e: unknown) {
return NextResponse.json({ error: e instanceof Error ? e.message : 'Sunucu hatası' }, { status: 500 })
}
}