hata düzeltme
This commit is contained in:
@@ -10,6 +10,12 @@ const nextConfig: NextConfig = {
|
|||||||
port: '',
|
port: '',
|
||||||
pathname: '/storage/v1/object/public/**',
|
pathname: '/storage/v1/object/public/**',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'api-weeding.edoysoft.com',
|
||||||
|
port: '',
|
||||||
|
pathname: '/storage/v1/object/public/**',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'img.youtube.com',
|
hostname: 'img.youtube.com',
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
import { redirect } from 'next/navigation'
|
import { redirect } from 'next/navigation'
|
||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
import { sendOTP } from '../verify/actions'
|
import { logActivity } from '@/lib/security'
|
||||||
import { checkRateLimit, incrementRateLimit, logActivity } from '@/lib/security'
|
|
||||||
|
|
||||||
export type LoginState = {
|
export type LoginState = {
|
||||||
error?: string
|
error?: string
|
||||||
@@ -17,30 +16,19 @@ export async function login(prevState: LoginState, formData: FormData): Promise<
|
|||||||
const email = formData.get('email') as string
|
const email = formData.get('email') as string
|
||||||
const password = formData.get('password') as string
|
const password = formData.get('password') as string
|
||||||
|
|
||||||
// 1. Check Rate Limit
|
|
||||||
const { blocked, resetTime } = await checkRateLimit('login_attempt')
|
|
||||||
if (blocked) {
|
|
||||||
return { error: `Çok fazla hatalı deneme. Lütfen ${resetTime?.toLocaleTimeString()} sonrası tekrar deneyin.` }
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: { user }, error } = await supabase.auth.signInWithPassword({
|
const { data: { user }, error } = await supabase.auth.signInWithPassword({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
await incrementRateLimit('login_attempt')
|
|
||||||
await logActivity(null, 'login_failed', { email, error: error.message })
|
await logActivity(null, 'login_failed', { email, error: error.message })
|
||||||
return { error: 'Giriş yapılamadı. E-posta veya şifre hatalı.' }
|
return { error: 'Giriş yapılamadı. E-posta veya şifre hatalı.' }
|
||||||
}
|
}
|
||||||
|
|
||||||
await logActivity(user?.id || null, 'login_success', { email })
|
await logActivity(user?.id || null, 'login_success', { email })
|
||||||
|
|
||||||
revalidatePath('/', 'layout')
|
|
||||||
revalidatePath('/', 'layout')
|
revalidatePath('/', 'layout')
|
||||||
|
|
||||||
// Trigger OTP email
|
redirect('/dashboard')
|
||||||
await sendOTP()
|
|
||||||
|
|
||||||
redirect('/verify')
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { createHmac } from 'crypto'
|
|
||||||
|
|
||||||
export interface CaptchaData {
|
|
||||||
image: string // SVG string
|
|
||||||
hash: string // HMAC hash of the text
|
|
||||||
}
|
|
||||||
|
|
||||||
const CAPTCHA_SECRET = process.env.CAPTCHA_SECRET || 'default-secret-change-me'
|
|
||||||
|
|
||||||
export function generateCaptcha(width = 200, height = 80): { text: string, data: string } {
|
|
||||||
console.log('[Captcha] Generating new captcha...')
|
|
||||||
// 1. Generate random text (5 chars)
|
|
||||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // Removed confusing chars like I, 1, 0, O
|
|
||||||
let text = ''
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
text += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
||||||
}
|
|
||||||
console.log('[Captcha] Generated text:', text)
|
|
||||||
|
|
||||||
// 2. Create SVG
|
|
||||||
const bg = '#f3f4f6'
|
|
||||||
const fg = '#374151'
|
|
||||||
|
|
||||||
// Random noise lines
|
|
||||||
let noise = ''
|
|
||||||
for (let i = 0; i < 7; i++) {
|
|
||||||
const x1 = Math.random() * width
|
|
||||||
const y1 = Math.random() * height
|
|
||||||
const x2 = Math.random() * width
|
|
||||||
const y2 = Math.random() * height
|
|
||||||
noise += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${fg}" stroke-width="1" opacity="0.3" />`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random noise dots
|
|
||||||
for (let i = 0; i < 30; i++) {
|
|
||||||
const x = Math.random() * width
|
|
||||||
const y = Math.random() * height
|
|
||||||
noise += `<circle cx="${x}" cy="${y}" r="1" fill="${fg}" opacity="0.5" />`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Text with slight rotation/position randomization
|
|
||||||
let svgText = ''
|
|
||||||
const fontSize = 32
|
|
||||||
const startX = 20
|
|
||||||
const spacing = 35
|
|
||||||
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
const char = text[i]
|
|
||||||
const x = startX + (i * spacing) + (Math.random() * 10 - 5)
|
|
||||||
const y = (height / 2) + (fontSize / 3) + (Math.random() * 10 - 5)
|
|
||||||
const rotate = Math.random() * 40 - 20 // +/- 20 degrees
|
|
||||||
|
|
||||||
svgText += `<text x="${x}" y="${y}" font-family="monospace" font-weight="bold" font-size="${fontSize}" fill="${fg}" transform="rotate(${rotate}, ${x}, ${y})">${char}</text>`
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = `
|
|
||||||
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="100%" height="100%" fill="${bg}"/>
|
|
||||||
${noise}
|
|
||||||
${svgText}
|
|
||||||
</svg>`
|
|
||||||
|
|
||||||
return { text, data: svg }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function signCaptcha(text: string): string {
|
|
||||||
const expires = Date.now() + 5 * 60 * 1000 // 5 minutes
|
|
||||||
const data = `${text.toUpperCase()}|${expires}`
|
|
||||||
const signature = createHmac('sha256', CAPTCHA_SECRET).update(data).digest('hex')
|
|
||||||
return `${data}|${signature}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verifyCaptcha(input: string, hash: string): boolean {
|
|
||||||
if (!input || !hash) {
|
|
||||||
console.log('[Captcha] Missing input or hash')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = hash.split('|')
|
|
||||||
if (parts.length !== 3) {
|
|
||||||
console.log('[Captcha] Invalid hash format')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const [originalText, expiresStr, signature] = parts
|
|
||||||
const expires = parseInt(expiresStr, 10)
|
|
||||||
|
|
||||||
// Check expiration
|
|
||||||
if (Date.now() > expires) {
|
|
||||||
console.log('[Captcha] Expired. Now:', Date.now(), 'Expires:', expires)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check signature integrity
|
|
||||||
const expectedData = `${originalText}|${expiresStr}`
|
|
||||||
const expectedSignature = createHmac('sha256', CAPTCHA_SECRET).update(expectedData).digest('hex')
|
|
||||||
|
|
||||||
if (signature !== expectedSignature) {
|
|
||||||
console.log('[Captcha] Signature mismatch')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check content match
|
|
||||||
const isValid = input.toUpperCase() === originalText
|
|
||||||
if (!isValid) {
|
|
||||||
console.log('[Captcha] Text mismatch. Expected:', originalText, 'Got:', input.toUpperCase())
|
|
||||||
}
|
|
||||||
return isValid
|
|
||||||
}
|
|
||||||
@@ -33,100 +33,4 @@ export async function logActivity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkRateLimit(action: string): Promise<{ blocked: boolean, remaining?: number, resetTime?: Date }> {
|
|
||||||
const MAX_ATTEMPTS = 5
|
|
||||||
|
|
||||||
try {
|
|
||||||
const supabase = await createAdminClient() || await createClient()
|
|
||||||
const headersList = await headers()
|
|
||||||
let ip = headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || 'unknown'
|
|
||||||
if (ip.includes(',')) ip = ip.split(',')[0].trim()
|
|
||||||
if (ip === '::1') ip = '127.0.0.1'
|
|
||||||
|
|
||||||
// Clean up old limits
|
|
||||||
// Clean up old limits (logic simplified, variable unused)
|
|
||||||
|
|
||||||
// Check current limit
|
|
||||||
const { data: limit } = await supabase
|
|
||||||
.from('rate_limits')
|
|
||||||
.select('*')
|
|
||||||
.eq('ip_address', ip)
|
|
||||||
.eq('action', action)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (!limit) {
|
|
||||||
return { blocked: false, remaining: MAX_ATTEMPTS }
|
|
||||||
}
|
|
||||||
|
|
||||||
// If blocked
|
|
||||||
if (limit.blocked_until && new Date(limit.blocked_until) > new Date()) {
|
|
||||||
return { blocked: true, resetTime: new Date(limit.blocked_until) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// If window expired, reset code handling happens in increment logic usually.
|
|
||||||
// But here we just check.
|
|
||||||
// Actually, simpler logic:
|
|
||||||
// We will increment on failure. This function just checks if currently blocked.
|
|
||||||
|
|
||||||
return { blocked: false, remaining: MAX_ATTEMPTS - (limit.count || 0) }
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Rate limit check failed:', error)
|
|
||||||
return { blocked: false } // Fail open
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function incrementRateLimit(action: string) {
|
|
||||||
const BLOCK_DURATION_MINUTES = 15
|
|
||||||
|
|
||||||
try {
|
|
||||||
const supabase = await createAdminClient() || await createClient()
|
|
||||||
const headersList = await headers()
|
|
||||||
let ip = headersList.get("x-forwarded-for") || headersList.get("x-real-ip") || 'unknown'
|
|
||||||
if (ip.includes(',')) ip = ip.split(',')[0].trim()
|
|
||||||
if (ip === '::1') ip = '127.0.0.1'
|
|
||||||
|
|
||||||
const { data: limit } = await supabase
|
|
||||||
.from('rate_limits')
|
|
||||||
.select('*')
|
|
||||||
.eq('ip_address', ip)
|
|
||||||
.eq('action', action)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
// Check if we should reset (if last attempt was long ago)
|
|
||||||
const lastAttempt = new Date(limit.last_attempt)
|
|
||||||
const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000)
|
|
||||||
|
|
||||||
let newCount = limit.count + 1
|
|
||||||
let blockedUntil = null
|
|
||||||
|
|
||||||
if (lastAttempt < tenMinutesAgo) {
|
|
||||||
newCount = 1 // Reset if window passed
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCount >= 5) {
|
|
||||||
blockedUntil = new Date(Date.now() + BLOCK_DURATION_MINUTES * 60 * 1000).toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
await supabase
|
|
||||||
.from('rate_limits')
|
|
||||||
.update({
|
|
||||||
count: newCount,
|
|
||||||
last_attempt: new Date().toISOString(),
|
|
||||||
blocked_until: blockedUntil
|
|
||||||
})
|
|
||||||
.eq('id', limit.id)
|
|
||||||
} else {
|
|
||||||
await supabase.from('rate_limits').insert({
|
|
||||||
ip_address: ip,
|
|
||||||
action,
|
|
||||||
count: 1,
|
|
||||||
last_attempt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Rate limit increment failed:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,22 +36,17 @@ export async function updateSession(request: NextRequest) {
|
|||||||
if (
|
if (
|
||||||
!user &&
|
!user &&
|
||||||
!request.nextUrl.pathname.startsWith('/login') &&
|
!request.nextUrl.pathname.startsWith('/login') &&
|
||||||
!request.nextUrl.pathname.startsWith('/auth')
|
!request.nextUrl.pathname.startsWith('/auth') &&
|
||||||
|
request.nextUrl.pathname !== '/' &&
|
||||||
|
!request.nextUrl.pathname.startsWith('/galeri')
|
||||||
) {
|
) {
|
||||||
const url = request.nextUrl.clone()
|
const url = request.nextUrl.clone()
|
||||||
url.pathname = '/login'
|
url.pathname = '/login'
|
||||||
return NextResponse.redirect(url)
|
return NextResponse.redirect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2FA Enforcement
|
// 2FA Enforcement Removed
|
||||||
if (user && !request.nextUrl.pathname.startsWith('/verify')) {
|
|
||||||
const verifiedCookie = request.cookies.get('2fa_verified')
|
|
||||||
if (!verifiedCookie) {
|
|
||||||
const url = request.nextUrl.clone()
|
|
||||||
url.pathname = '/verify'
|
|
||||||
return NextResponse.redirect(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return supabaseResponse
|
return supabaseResponse
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user