307 lines
7.9 KiB
Markdown
307 lines
7.9 KiB
Markdown
# Anonim Geziler Güvenlik Açığı Düzeltmesi
|
||
|
||
## 🔴 Sorun
|
||
|
||
### Güvenlik Açığı
|
||
Önceki RLS politikaları, `user_id IS NULL` olan tüm anonim gezilere herkesin erişmesine izin veriyordu:
|
||
|
||
```sql
|
||
-- ❌ ESKİ VE GÜVENSİZ
|
||
USING (is_public = true OR auth.uid() = user_id OR user_id IS NULL);
|
||
```
|
||
|
||
Bu politika şu sorunlara yol açıyordu:
|
||
1. **Herkes başkasının anonim gezisini görebilir, düzenleyebilir ve silebilirdi**
|
||
2. **Anonim geziler sahipsiz kalıyordu** - kullanıcı giriş yaptıktan sonra gezisi kayboluyordu
|
||
3. **Veri tabanı kirliliği** - eski anonim geziler hiç temizlenmiyordu
|
||
|
||
### Etki
|
||
- Gizlilik ihlali: Kullanıcıların anonim gezileri herkese açık
|
||
- Veri kaybı: Login sonrası kullanıcı kendi gezisini bulamıyor
|
||
- Performans: Sahipsiz geziler birikerek veritabanını şişiriyor
|
||
|
||
---
|
||
|
||
## ✅ Çözüm
|
||
|
||
### 1. Token Tabanlı Erişim Sistemi
|
||
|
||
Her anonim gezi için benzersiz bir token oluşturulur ve localStorage'da saklanır:
|
||
|
||
```typescript
|
||
// Gezi oluşturulurken
|
||
const anonymousToken = crypto.randomUUID();
|
||
localStorage.setItem(`trip_token_${tripId}`, anonymousToken);
|
||
```
|
||
|
||
### 2. Güvenli RLS Politikaları
|
||
|
||
Yeni politikalar sadece token sahibinin erişimine izin verir:
|
||
|
||
```sql
|
||
-- ✅ YENİ VE GÜVENLİ
|
||
CREATE POLICY "Seyahatleri görüntüleme"
|
||
ON trips FOR SELECT
|
||
USING (
|
||
is_public = true
|
||
OR user_id = auth.uid()
|
||
);
|
||
```
|
||
|
||
Anonim geziler için özel RPC fonksiyonları:
|
||
- `update_anonymous_trip(trip_id, token, updates)` - Token ile güncelleme
|
||
- `delete_anonymous_trip(trip_id, token)` - Token ile silme
|
||
- `get_anonymous_trip(trip_id, token)` - Token ile okuma
|
||
|
||
### 3. Otomatik Ownership Transfer
|
||
|
||
Kullanıcı giriş yaptığında, anonim gezisi otomatik olarak hesabına bağlanır:
|
||
|
||
```typescript
|
||
// AuthContext.tsx - onAuthStateChange
|
||
if (event === 'SIGNED_IN') {
|
||
const currentTripId = localStorage.getItem('currentTripId');
|
||
if (currentTripId) {
|
||
await tripsApi.claimAnonymousTrip(currentTripId);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. Otomatik Temizlik
|
||
|
||
7 günden eski anonim geziler otomatik olarak silinir:
|
||
|
||
```sql
|
||
CREATE FUNCTION cleanup_old_anonymous_trips()
|
||
RETURNS INTEGER
|
||
AS $$
|
||
DELETE FROM trips
|
||
WHERE user_id IS NULL
|
||
AND created_at < NOW() - INTERVAL '7 days'
|
||
$$;
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 Değişiklikler
|
||
|
||
### Database Migrations
|
||
|
||
#### `00053_add_anonymous_trip_token_system.sql`
|
||
- `trips` tablosuna `anonymous_token` sütunu eklendi
|
||
- Unique index oluşturuldu
|
||
- Güvenli RLS politikaları eklendi
|
||
- RPC fonksiyonları oluşturuldu:
|
||
- `claim_anonymous_trip(trip_id, token)` - Ownership transfer
|
||
- `update_anonymous_trip(trip_id, token, updates)` - Token ile güncelleme
|
||
- `delete_anonymous_trip(trip_id, token)` - Token ile silme
|
||
- `get_anonymous_trip(trip_id, token)` - Token ile okuma
|
||
- `cleanup_old_anonymous_trips()` - Eski gezileri temizle
|
||
|
||
#### `00054_fix_anonymous_trip_rls_policies.sql`
|
||
- RLS politikalarını optimize etti
|
||
- Application layer'da token kontrolü için yapı oluşturdu
|
||
|
||
### Frontend Değişiklikleri
|
||
|
||
#### `src/db/api.ts`
|
||
|
||
**`tripsApi.create()`**
|
||
```typescript
|
||
// Anonim kullanıcı için token oluştur
|
||
if (!user) {
|
||
anonymousToken = crypto.randomUUID();
|
||
tripData = { ...trip, user_id: null, anonymous_token: anonymousToken };
|
||
localStorage.setItem(`trip_token_${data.id}`, anonymousToken);
|
||
}
|
||
```
|
||
|
||
**`tripsApi.update()`**
|
||
```typescript
|
||
// Anonim kullanıcı için RPC kullan
|
||
if (!user) {
|
||
const token = localStorage.getItem(`trip_token_${tripId}`);
|
||
return await supabase.rpc('update_anonymous_trip', {
|
||
trip_id_param: tripId,
|
||
token_param: token,
|
||
updates: updates
|
||
});
|
||
}
|
||
```
|
||
|
||
**`tripsApi.delete()`**
|
||
```typescript
|
||
// Anonim kullanıcı için RPC kullan
|
||
if (!user) {
|
||
const token = localStorage.getItem(`trip_token_${tripId}`);
|
||
await supabase.rpc('delete_anonymous_trip', {
|
||
trip_id_param: tripId,
|
||
token_param: token
|
||
});
|
||
localStorage.removeItem(`trip_token_${tripId}`);
|
||
}
|
||
```
|
||
|
||
**Yeni Fonksiyonlar:**
|
||
- `claimAnonymousTrip(tripId)` - Anonim geziyi kullanıcıya transfer et
|
||
- `cleanupOldAnonymousTrips()` - Eski anonim gezileri temizle
|
||
- `canAccessTrip(tripId)` - Geziye erişim kontrolü
|
||
|
||
#### `src/contexts/AuthContext.tsx`
|
||
|
||
```typescript
|
||
// Login sonrası otomatik ownership transfer
|
||
if (event === 'SIGNED_IN') {
|
||
const currentTripId = localStorage.getItem('currentTripId');
|
||
if (currentTripId) {
|
||
await tripsApi.claimAnonymousTrip(currentTripId);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### `src/types/trip-ui.ts`
|
||
|
||
```typescript
|
||
export interface TripDataRaw {
|
||
// ...
|
||
anonymous_token?: string; // Yeni alan
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔒 Güvenlik Özellikleri
|
||
|
||
### 1. Token Güvenliği
|
||
- **UUID v4** kullanılır (128-bit rastgele)
|
||
- **localStorage**'da saklanır (sadece aynı origin erişebilir)
|
||
- **Tek kullanımlık**: Transfer sonrası token silinir
|
||
|
||
### 2. RLS Koruması
|
||
- Kayıtlı kullanıcılar sadece kendi gezilerini görebilir
|
||
- Anonim geziler sadece token ile erişilebilir
|
||
- Public geziler herkese açık
|
||
|
||
### 3. Veri Temizliği
|
||
- 7 günden eski anonim geziler otomatik silinir
|
||
- Orphan data birikmesi önlenir
|
||
|
||
### 4. Ownership Transfer
|
||
- Login sonrası otomatik transfer
|
||
- Token doğrulaması ile güvenli transfer
|
||
- Transfer sonrası token temizlenir
|
||
|
||
---
|
||
|
||
## 🧪 Test Senaryoları
|
||
|
||
### Senaryo 1: Anonim Kullanıcı
|
||
1. ✅ Kullanıcı giriş yapmadan gezi oluşturur
|
||
2. ✅ Token localStorage'a kaydedilir
|
||
3. ✅ Kullanıcı geziyi düzenleyebilir
|
||
4. ✅ Kullanıcı geziyi silebilir
|
||
5. ✅ Başka bir tarayıcıdan geziye erişilemez
|
||
|
||
### Senaryo 2: Login Sonrası Transfer
|
||
1. ✅ Anonim kullanıcı gezi oluşturur
|
||
2. ✅ Kullanıcı giriş yapar
|
||
3. ✅ Gezi otomatik olarak kullanıcıya transfer edilir
|
||
4. ✅ Token temizlenir
|
||
5. ✅ Gezi "Seyahat Planlarım"da görünür
|
||
|
||
### Senaryo 3: Güvenlik Testi
|
||
1. ✅ Kullanıcı A anonim gezi oluşturur
|
||
2. ✅ Kullanıcı B aynı trip_id ile erişmeye çalışır
|
||
3. ✅ Erişim reddedilir (token yok)
|
||
4. ✅ Kullanıcı B geziyi düzenleyemez
|
||
5. ✅ Kullanıcı B geziyi silemez
|
||
|
||
### Senaryo 4: Temizlik
|
||
1. ✅ 7 günden eski anonim gezi oluşturulur
|
||
2. ✅ `cleanup_old_anonymous_trips()` çağrılır
|
||
3. ✅ Eski gezi silinir
|
||
4. ✅ Yeni anonim geziler korunur
|
||
|
||
---
|
||
|
||
## 📊 Performans
|
||
|
||
### Öncesi
|
||
- Tüm anonim geziler herkese açık
|
||
- N+1 query problemi
|
||
- Gereksiz veri transferi
|
||
|
||
### Sonrası
|
||
- Token kontrolü ile hızlı erişim
|
||
- RPC fonksiyonları ile optimize edilmiş sorgular
|
||
- Otomatik temizlik ile veritabanı boyutu kontrolü
|
||
|
||
---
|
||
|
||
## 🚀 Kullanım
|
||
|
||
### Anonim Gezi Oluşturma
|
||
```typescript
|
||
const trip = await tripsApi.create({
|
||
title: 'Kapadokya Gezim',
|
||
destination: 'Kapadokya',
|
||
// ...
|
||
});
|
||
// Token otomatik oluşturulur ve kaydedilir
|
||
```
|
||
|
||
### Anonim Gezi Güncelleme
|
||
```typescript
|
||
await tripsApi.update(tripId, {
|
||
title: 'Yeni Başlık'
|
||
});
|
||
// Token otomatik kontrol edilir
|
||
```
|
||
|
||
### Anonim Gezi Silme
|
||
```typescript
|
||
await tripsApi.delete(tripId);
|
||
// Token otomatik kontrol edilir ve temizlenir
|
||
```
|
||
|
||
### Manuel Ownership Transfer
|
||
```typescript
|
||
const success = await tripsApi.claimAnonymousTrip(tripId);
|
||
if (success) {
|
||
console.log('Gezi başarıyla transfer edildi');
|
||
}
|
||
```
|
||
|
||
### Eski Gezileri Temizleme (Admin)
|
||
```typescript
|
||
const deletedCount = await tripsApi.cleanupOldAnonymousTrips();
|
||
console.log(`${deletedCount} adet eski gezi temizlendi`);
|
||
```
|
||
|
||
---
|
||
|
||
## ⚠️ Önemli Notlar
|
||
|
||
1. **Token Kaybı**: Kullanıcı localStorage'ı temizlerse token kaybolur ve geziye erişemez
|
||
2. **Tarayıcı Değişimi**: Farklı tarayıcıda token olmadığı için geziye erişilemez
|
||
3. **7 Gün Sınırı**: Anonim geziler 7 gün sonra otomatik silinir
|
||
4. **Public Geziler**: Public yapılan geziler token olmadan da görüntülenebilir (ama düzenlenemez)
|
||
|
||
---
|
||
|
||
## 🔄 Migration Sırası
|
||
|
||
1. `00053_add_anonymous_trip_token_system.sql` - Token sistemi ve RPC fonksiyonları
|
||
2. `00054_fix_anonymous_trip_rls_policies.sql` - RLS politikalarını optimize et
|
||
|
||
---
|
||
|
||
## 📝 Sonuç
|
||
|
||
Bu güvenlik düzeltmesi ile:
|
||
- ✅ Anonim geziler artık güvenli
|
||
- ✅ Kullanıcılar giriş sonrası gezilerini kaybetmiyor
|
||
- ✅ Veritabanı temiz kalıyor
|
||
- ✅ Performans optimize edildi
|
||
- ✅ Kullanıcı deneyimi iyileştirildi
|