Django API Kimlik Doğrulama: Token Authentication


Modern uygulamalarda API güvenliği en kritik konulardan biridir. Web veya mobil uygulamalar API ile haberleşirken kullanıcı kimliğinin doğrulanması gerekir.

Django ekosisteminde API geliştirmek için en çok kullanılan araç:

  • Django REST Framework

Bu framework API'ler için birçok kimlik doğrulama yöntemi sunar. Bunlardan biri de Token Authentication sistemidir.

  • Token Authentication nedir

  • Nasıl çalışır

  • Django'da nasıl kurulur

  • Token ile API koruma

  • JWT ile farkı

  • Profesyonel kullanım önerileri


1️⃣ Token Authentication Nedir?

Token Authentication, kullanıcının giriş yaptıktan sonra benzersiz bir anahtar (token) alması prensibine dayanır.

Bu token daha sonra API isteklerinde kullanılır.

Çalışma mantığı:

Kullanıcı → Login olur
            ↓
API → Token üretir
            ↓
Kullanıcı → API isteğinde token gönderir
            ↓
API → Token doğrular

Örnek HTTP isteği:

Authorization: Token 2bb80d537b1da3e38bd30361aa855686

Bu token sayesinde API kullanıcıyı tanır.


2️⃣ Token Authentication Nerelerde Kullanılır?

Token sistemi özellikle şu uygulamalarda kullanılır:

  • Mobil uygulamalar

  • SPA uygulamaları

  • Flutter uygulamaları

  • React / Vue uygulamaları

  • harici API servisleri

Örnek mimari:

Flutter App
     │
     │ Token
     ▼
Django API
     │
     ▼
Database

3️⃣ Django'da Token Authentication Kurulumu

Token sistemi doğrudan:

  • Django REST Framework

tarafından sağlanır.

Kurulum adımları aşağıdadır.


4️⃣ Django REST Framework Kurulumu

Önce gerekli paketleri yükleyelim.

pip install djangorestframework

settings.py

INSTALLED_APPS = [
    'rest_framework',
]

5️⃣ Token Authentication Aktifleştirme

Token sistemi için şu uygulama eklenir:

settings.py

INSTALLED_APPS = [
    'rest_framework',
    'rest_framework.authtoken',
]

Migration çalıştırılır.

python manage.py migrate

Bu işlem veritabanında Token tablosu oluşturur.


6️⃣ Token Modeli

Django otomatik olarak şu modeli oluşturur:

Token
 ├── key
 ├── user
 └── created

Örnek:

user            token
admin                7d2b7b9e2a

7️⃣ Kullanıcı için Token Oluşturma

Token şu şekilde oluşturulur:

from rest_framework.authtoken.models import Token

token = Token.objects.create(user=user)

Alternatif olarak otomatik üretilebilir.

signals.py

from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

Böylece kullanıcı oluşturulunca token otomatik üretilir.


8️⃣ Token Login API

Django hazır login endpoint sağlar.

urls.py

from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    path('api/token/', obtain_auth_token),
]

Login isteği:

POST /api/token/

Body:

{
 "username": "admin",
 "password": "123456"
}

Response:

{
 "token": "1b7e6e9c4a..."
}

9️⃣ Token ile API Kullanımı

Artık kullanıcı token ile API kullanabilir.

HTTP Header:

Authorization: Token 1b7e6e9c4a

🔟 API Koruma

Bir view'ı korumak için:

from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view, permission_classes

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def profil(request):
    return Response({"user": request.user.username})

Artık sadece token gönderilen istekler çalışır.


11️⃣ Global Authentication Ayarı

settings.py

REST_FRAMEWORK = {
 'DEFAULT_AUTHENTICATION_CLASSES': [
  'rest_framework.authentication.TokenAuthentication',
 ],
 'DEFAULT_PERMISSION_CLASSES': [
  'rest_framework.permissions.IsAuthenticated',
 ],
}

Artık tüm API'ler token ister.


12️⃣ Token Authentication Sorunları

Bu sistemin bazı dezavantajları vardır.

❌ Token süresi yok

Token süresizdir.

❌ Logout sistemi yok

Token silinmezse çalışmaya devam eder.

❌ Mobil uygulamalarda risk

Token çalınırsa sürekli kullanılabilir.


13️⃣ Modern Alternatif: JWT Authentication

Modern projelerde daha çok kullanılan sistem:

  • JSON Web Token

Django için popüler kütüphane:

  • Simple JWT

JWT yapısı:

HEADER
PAYLOAD
SIGNATURE

JWT avantajları:

✔ Token süresi vardır
✔ Refresh token vardır
✔ Stateless çalışır
✔ Mikroservis uyumludur


14️⃣ Token vs JWT Karşılaştırma

Özellik        Token        JWT
Kurulum        Kolay        Orta
Güvenlik        Orta        Yüksek
Expire        Yok        Var
Refresh        Yok        Var
Mobil        Orta        Çok iyi

15️⃣ Profesyonel API Güvenlik Mimari

Gerçek projelerde şu yapı önerilir:

Mobile App
     │
     │ JWT Token
     ▼
API Gateway
     │
     ▼
Django REST API
     │
     ▼
Database

Ek güvenlikler:

  • Rate limiting

  • Token blacklist

  • Refresh rotation

  • Permission sistemi


16️⃣ Flutter + Django Token Kullanımı

Mobil uygulamada akış şu şekildedir:

1 Login
2 Token al
3 Token cihazda sakla
4 API isteğinde gönder

Flutter header örneği:

Authorization: Token 1b7e6e9c4a

17️⃣ Sonuç

Django API projelerinde Token Authentication basit bir çözümdür.

Küçük projeler için uygundur.

Ancak büyük projelerde şu sistem önerilir:

Django REST Framework
+
JWT Authentication
+
Refresh Token


1️⃣ Django API Güvenlik Mimarisi
2️⃣ Django Refresh Token Sistemi
3️⃣ Django API Rate Limit Sistemi

Bu konular özellikle mobil uygulama + API projelerinde (Flutter + Django gibi) gerçek üretim ortamında kullanılan tekniklerdir.

Kullandığımız temel framework:

  • Django

  • Django REST Framework


1️⃣ Django API Güvenlik Mimarisi

Bir API'nin güvenliği sadece login sistemi değildir. Gerçek projelerde güvenlik katmanlı mimari ile sağlanır.

Profesyonel API güvenlik katmanları:

Client (Flutter / Web)
        │
        ▼
Authentication (JWT)
        │
        ▼
Authorization (Permissions)
        │
        ▼
Rate Limiting
        │
        ▼
Input Validation
        │
        ▼
Database

1.1 Authentication (Kimlik Doğrulama)

API'nin ilk güvenlik katmanı kimlik doğrulamadır.

En yaygın yöntem:

  • JSON Web Token

Django için en popüler paket:

  • Simple JWT

JWT çalışma mantığı:

Login
   │
   ▼
Access Token + Refresh Token
   │
   ▼
API Request
   │
   ▼
Token Verification

1.2 Authorization (Yetkilendirme)

Kimlik doğrulama sadece kullanıcının kim olduğunu doğrular.

Yetkilendirme ise ne yapabileceğini belirler.

Örnek:

RolYetki
Admin        Tüm işlemler
Öğretmen        Not girişi
Öğrenci        Not görüntüleme

DRF permission örneği:

from rest_framework.permissions import IsAuthenticated

class DersView(APIView):
    permission_classes = [IsAuthenticated]

1.3 Input Validation

API saldırılarının çoğu hatalı veri gönderme üzerinden olur.

Serializer kullanımı:

class OgrenciSerializer(serializers.Serializer):
    ad = serializers.CharField(max_length=100)
    yas = serializers.IntegerField()

1.4 HTTPS Kullanımı

API her zaman HTTPS üzerinden çalışmalıdır.

http://api.site.com  ❌
https://api.site.com ✅

1.5 CORS Güvenliği

Frontend uygulamaları için:

Paket:

  • django-cors-headers

Kurulum:

pip install django-cors-headers

settings.py

CORS_ALLOWED_ORIGINS = [
 "https://mobiluygulama.com"
]

2️⃣ Django Refresh Token Sistemi

Modern API'lerde iki token sistemi kullanılır.

Access Token
Refresh Token

Bu yapı özellikle mobil uygulamalar için standarttır.


2.1 Token Türleri

Access Token

  • kısa ömürlü

  • API erişimi sağlar

Örnek süre:

15 dakika

Refresh Token

  • uzun ömürlü

  • yeni access token üretir

Örnek süre:

7 gün

2.2 Django Refresh Token Kurulumu

Kullanılan paket:

  • djangorestframework-simplejwt

Kurulum:

pip install djangorestframework-simplejwt

2.3 settings.py

from datetime import timedelta

SIMPLE_JWT = {

"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),

"REFRESH_TOKEN_LIFETIME": timedelta(days=7),

"AUTH_HEADER_TYPES": ("Bearer",),

}

2.4 Token Endpoint

urls.py

from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)

urlpatterns = [

path('api/token/', TokenObtainPairView.as_view()),
path('api/token/refresh/', TokenRefreshView.as_view()),

]

2.5 Login Response

POST /api/token/

Response:

{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
}

2.6 Refresh Token Kullanımı

Access token süresi dolunca:

POST /api/token/refresh/

Body:

{
"refresh": "REFRESH_TOKEN"
}

Response:

Yeni Access Token

2.7 Token Rotation (Profesyonel Güvenlik)

Ek güvenlik için:

SIMPLE_JWT = {

"ROTATE_REFRESH_TOKENS": True,

"BLACKLIST_AFTER_ROTATION": True,

}

Bu sistem:

  • eski refresh token'ı iptal eder

  • token çalınmasını engeller


3️⃣ Django API Rate Limit Sistemi

Rate limit, bir kullanıcının API'yi aşırı kullanmasını engeller.

Amaç:

  • brute force saldırılarını önlemek

  • API abuse engellemek

  • sunucu yükünü azaltmak


3.1 En Popüler Paket

  • django-ratelimit

Kurulum:

pip install django-ratelimit

3.2 Kullanım

from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='5/m')
def login_view(request):
    return HttpResponse("Login")

Bu ayar:

1 dakikada 5 istek

3.3 Django REST Framework Throttling

DRF kendi rate limit sistemine sahiptir.

settings.py

REST_FRAMEWORK = {

'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.UserRateThrottle',
],

'DEFAULT_THROTTLE_RATES': {
'user': '100/day',
}

}

3.4 Endpoint Bazlı Limit

from rest_framework.throttling import UserRateThrottle

class LoginThrottle(UserRateThrottle):
    rate = '5/min'

View:

class LoginView(APIView):

    throttle_classes = [LoginThrottle]

4️⃣ Profesyonel Django API Güvenlik Mimarisi

Gerçek üretim ortamlarında şu mimari kullanılır:

Client (Flutter / Web)
       │
       ▼
HTTPS
       │
       ▼
JWT Authentication
       │
       ▼
Refresh Token
       │
       ▼
Permission System
       │
       ▼
Rate Limiting
       │
       ▼
Django API
       │
       ▼
Database

5️⃣ Büyük Projelerde Ek Güvenlik

Profesyonel sistemlerde ayrıca:

API Gateway
WAF (Web Application Firewall)
IP Whitelist
Audit Log
Two Factor Authentication

kullanılır.


Sonuç

Modern bir Django API güvenliği için şu yapı önerilir:

Django REST Framework
+
JWT Authentication
+
Refresh Token
+
Rate Limiting
+
Permissions
+
HTTPS

Bu yapı:

✔ mobil uygulamalar için güvenli
✔ ölçeklenebilir
✔ production ready


Aşağıda Django REST Framework ile API geliştirirken kullanılan 25 önemli güvenlik tekniğini kapsamlı ve öğretici şekilde anlatıyorum. Bu liste gerçek üretim sistemlerinde kullanılan backend güvenlik checklist mantığıyla hazırlanmıştır.


Django API Security Checklist (25 Güvenlik Tekniği)

Modern API’ler sadece login sistemi ile güvenli olmaz. Güvenlik birden fazla katmandan oluşur.

Genel güvenlik mimarisi:

Client
   │
   ▼
HTTPS
   │
   ▼
Authentication
   │
   ▼
Authorization
   │
   ▼
Rate Limiting
   │
   ▼
Validation
   │
   ▼
Database

1️⃣ HTTPS Zorunlu Kullanımı

API her zaman HTTPS üzerinden çalışmalıdır.

https://api.site.com

Sebep:

  • Token çalınmasını engeller

  • MITM saldırılarını önler

Django ayarı:

SECURE_SSL_REDIRECT = True

2️⃣ Güçlü SECRET_KEY Kullanımı

Django güvenliğinin temeli:

SECRET_KEY

Güçlü olmalıdır.

Örnek üretme:

python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

3️⃣ DEBUG Modunu Kapalı Tutmak

Production ortamında:

DEBUG = False

Aksi halde:

  • hata mesajları

  • server bilgileri

sızabilir.


4️⃣ JWT Authentication Kullanımı

Token sistemi yerine modern yöntem:

  • JSON Web Token

Django için:

  • Simple JWT


5️⃣ Access Token Süresi

Access token kısa süreli olmalıdır.

Önerilen:

10 - 30 dakika

Örnek:

ACCESS_TOKEN_LIFETIME = timedelta(minutes=15)

6️⃣ Refresh Token Kullanımı

Refresh token ile yeni access token üretilebilir.

Avantaj:

  • güvenlik

  • kullanıcı deneyimi


7️⃣ Refresh Token Rotation

Her refresh isteğinde token değişir.

ROTATE_REFRESH_TOKENS = True

8️⃣ Token Blacklist Sistemi

Eski token'ları iptal eder.

BLACKLIST_AFTER_ROTATION = True

9️⃣ Rate Limiting

API spam saldırılarını engeller.

Örnek:

login endpoint
5 istek / dakika

🔟 Django REST Throttling

DRF içinde hazır gelir.

REST_FRAMEWORK = {

'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.UserRateThrottle',
],

'DEFAULT_THROTTLE_RATES': {
'user': '1000/day',
}

}

11️⃣ Login Endpoint Rate Limit

Login endpoint mutlaka limitlenmelidir.

Sebep:

Brute Force Attack

12️⃣ Password Hashing

Django güçlü hash algoritması kullanır.

Algoritma:

PBKDF2

13️⃣ Input Validation

Kullanıcıdan gelen veriler doğrulanmalıdır.

class KullaniciSerializer(serializers.Serializer):

    email = serializers.EmailField()
    yas = serializers.IntegerField()

14️⃣ SQL Injection Koruması

Django ORM zaten koruma sağlar.

Yanlış kullanım:

raw SQL

15️⃣ XSS Koruması

Django template sistemi otomatik escape eder.


16️⃣ CSRF Koruması

Web API için önemli.

Django middleware:

CsrfViewMiddleware

17️⃣ CORS Ayarları

Frontend için güvenli origin tanımlanır.

Paket:

  • django-cors-headers


18️⃣ Permission Sistemi

Kullanıcı yetkileri kontrol edilmelidir.

permission_classes = [IsAuthenticated]

19️⃣ Role Based Access Control

RBAC sistemi kurulabilir.

Örnek roller:

admin
teacher
student

20️⃣ API Logging

API logları tutulmalıdır.

Sebep:

security audit

21️⃣ Request Size Limiti

Çok büyük istekler engellenmelidir.

Sebep:

DoS Attack

22️⃣ IP Whitelist

Admin API'ler için kullanılabilir.

Örnek:

sadece ofis IP

23️⃣ Brute Force Koruması

Login saldırıları engellenmelidir.

Paket:

  • django-axes


24️⃣ API Versioning

API versiyonları ayrılmalıdır.

/api/v1/
/api/v2/

DRF destekler.


25️⃣ Güvenlik Header'ları

HTTP güvenlik header'ları önemlidir.

Django ayarları:

SECURE_BROWSER_XSS_FILTER = True

SECURE_CONTENT_TYPE_NOSNIFF = True

X_FRAME_OPTIONS = "DENY"

Profesyonel Django API Güvenlik Mimarisi

Gerçek sistemlerde mimari şöyle olur:

Client (Flutter / Web)
        │
        ▼
HTTPS
        │
        ▼
JWT Authentication
        │
        ▼
Refresh Token
        │
        ▼
Permission System
        │
        ▼
Rate Limit
        │
        ▼
Django REST API
        │
        ▼
Database

Sonuç

Modern Django API güvenliği şu teknolojilerle kurulmalıdır:

Django REST Framework
JWT Authentication
Refresh Token
Rate Limiting
Permission System
HTTPS

Bu yapı:

✔ mobil uygulamalar için ideal
✔ güvenli
✔ ölçeklenebilir


Şimdi bu temel üzerine tam teşekküllü bir CRUD (Create, Read, Update, Delete - Oluştur, Oku, Güncelle, Sil) yapısı inşa edeceğiz.

Django'nun en güzel yanlarından biri, ModelViewSet kullandığımız için arka planda (backend) güncelleme ve silme işlemleri için ekstra bir kod yazmamıza neredeyse hiç gerek olmamasıdır. Django bu API uç noktalarını (endpoints) bizim için zaten hazırladı! Asıl işi Flutter tarafında, arayüzü tasarlayarak yapacağız.


Django ve Flutter ile Harcama Takip Uygulaması (CRUD İşlemleri Dahil)

BÖLÜM 1: BACKEND (Django Kurulumu ve API)

Öncelikle sanal ortamınızı (virtual environment) oluşturduğunuzu ve aktif ettiğinizi varsayarak başlıyoruz.

Gerekli Paketlerin Kurulumu ve Proje Başlatma

Terminalinizi açın ve şu komutları sırasıyla çalıştırın:

Bash:
pip install django djangorestframework django-cors-headers
django-admin startproject config .
python manage.py startapp expenses

1. Ayarlar (config/settings.py)

Aldığımız IP hatasını yaşamamak için ALLOWED_HOSTS kısmını ['*'] yaparak geliştirme aşamasında her cihaza izin veriyoruz.

Python:
# config/settings.py (İlgili kısımlar)

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',
    'corsheaders',
    'expenses', # Kendi uygulamamız
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # En üste ekleyin
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# HER CİHAZDAN ERİŞİME İZİN VER (Geliştirme için)
ALLOWED_HOSTS = ['*']
CORS_ALLOW_ALL_ORIGINS = True

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
}

2. Veritabanı Modeli (expenses/models.py)

Python:
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token

class Expense(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expenses')
    title = models.CharField(max_length=100)
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.CharField(max_length=50, default='Diğer')
    date = models.DateField(auto_now_add=True)

    def __str__(self):
        return f"{self.user.username} - {self.title} ({self.amount} ₺)"

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

3. Serializer (expenses/serializers.py)

Python:
from rest_framework import serializers
from .models import Expense

class ExpenseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Expense
        fields = ['id', 'title', 'amount', 'category', 'date', 'user']
        read_only_fields = ['user'] # Flutter'dan eklerken user ID göndermemize gerek kalmaz

4. Görünümler (expenses/views.py)

Bu yapı sayesinde POST (Ekleme), PUT (Güncelleme) ve DELETE (Silme) işlemleri otomatik olarak çalışır.

Python:
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import Expense
from .serializers import ExpenseSerializer

class ExpenseViewSet(viewsets.ModelViewSet):
    serializer_class = ExpenseSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        # Sadece istek atan kullanıcının harcamaları
        return Expense.objects.filter(user=self.request.user).order_by('-date')

    def perform_create(self, serializer):
        # Yeni kayıtta kullanıcıyı otomatik ata
        serializer.save(user=self.request.user)

5. URL Yönlendirmeleri (expenses/urls.py ve config/urls.py)

Python:
# expenses/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ExpenseViewSet
from rest_framework.authtoken import views as authtoken_views

router = DefaultRouter()
router.register(r'harcamalar', ExpenseViewSet, basename='expense')

urlpatterns = [
    path('', include(router.urls)),
    path('token-al/', authtoken_views.obtain_auth_token, name='api_token_auth'),
]
Python:
# config/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('expenses.urls')),
]

(Backend tarafında python manage.py makemigrations ve migrate yapıp runserver ile çalıştırdığından emin ol).


BÖLÜM 2: FRONTEND (Flutter) Tam Kodları

Flutter tarafında http paketini kurduğunu varsayıyoruz (flutter pub add http). Arayüzü daha temiz tutmak için 3 ayrı dosyada çalışacağız.

1. Model Sınıfı (lib/expense_model.dart)

Dart:
class Expense {
  final int id;
  final String title;
  final String amount;
  final String category;
  final String date;

  Expense({
    required this.id,
    required this.title,
    required this.amount,
    required this.category,
    required this.date,
  });

  factory Expense.fromJson(Map<String, dynamic> json) {
    return Expense(
      id: json['id'],
      title: json['title'],
      amount: json['amount'].toString(),
      category: json['category'],
      date: json['date'],
    );
  }
}

2. API Servisi (lib/api_service.dart)

Ekleme, güncelleme ve silme metotlarını buraya ekledik.

Dart:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'expense_model.dart';

class ApiService {
  static const String baseUrl = 'http://10.0.2.2:8000/api'; 

  // --- 1. GİRİŞ YAP ---
  Future<String?> login(String username, String password) async {
    try {
      final response = await http.post(
        Uri.parse('$baseUrl/token-al/'),
        body: {'username': username, 'password': password},
      );
      if (response.statusCode == 200) return json.decode(response.body)['token'];
    } catch (e) {
      print("Login Hatası: $e");
    }
    return null;
  }

  // --- 2. HARCAMALARI GETİR (READ) ---
  Future<List<Expense>> getExpenses(String token) async {
    final response = await http.get(
      Uri.parse('$baseUrl/harcamalar/'),
      headers: {'Authorization': 'Token $token'},
    );
    if (response.statusCode == 200) {
      List jsonResponse = json.decode(utf8.decode(response.bodyBytes));
      return jsonResponse.map((data) => Expense.fromJson(data)).toList();
    } else {
      throw Exception('Harcamalar yüklenemedi.');
    }
  }

  // --- 3. HARCAMA EKLE (CREATE) ---
  Future<bool> addExpense(String token, String title, String amount, String category) async {
    final response = await http.post(
      Uri.parse('$baseUrl/harcamalar/'),
      headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json'},
      body: json.encode({'title': title, 'amount': amount, 'category': category}),
    );
    return response.statusCode == 201; // 201 Created
  }

  // --- 4. HARCAMA GÜNCELLE (UPDATE) ---
  Future<bool> updateExpense(String token, int id, String title, String amount, String category) async {
    final response = await http.put(
      Uri.parse('$baseUrl/harcamalar/$id/'),
      headers: {'Authorization': 'Token $token', 'Content-Type': 'application/json'},
      body: json.encode({'title': title, 'amount': amount, 'category': category}),
    );
    return response.statusCode == 200; // 200 OK
  }

  // --- 5. HARCAMA SİL (DELETE) ---
  Future<bool> deleteExpense(String token, int id) async {
    final response = await http.delete(
      Uri.parse('$baseUrl/harcamalar/$id/'),
      headers: {'Authorization': 'Token $token'},
    );
    return response.statusCode == 204; // 204 No Content
  }
}

3. Ana Arayüz ve Ekranlar (lib/main.dart)

Tüm ekranları (Giriş, Liste ve Form ekranı) bu dosyada birleştirdim.

Dart:
import 'package:flutter/material.dart';
import 'api_service.dart';
import 'expense_model.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Harcama Takip',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.teal),
      home: const LoginScreen(),
    );
  }
}

// ==========================================
// 1. GİRİŞ EKRANI
// ==========================================
class LoginScreen extends StatefulWidget {
  const LoginScreen({super.key});

  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final ApiService apiService = ApiService();
  final TextEditingController _usernameController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();
  bool _isLoading = false;

  void _login() async {
    setState(() => _isLoading = true);
    String? token = await apiService.login(_usernameController.text.trim(), _passwordController.text.trim());
    setState(() => _isLoading = false);

    if (token != null && mounted) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => ExpenseListScreen(token: token)),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Giriş başarısız!')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Giriş Yap', style: TextStyle(color: Colors.white)), backgroundColor: Colors.teal,),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.wallet, size: 80, color: Colors.teal),
            const SizedBox(height: 20),
            TextField(controller: _usernameController, decoration: const InputDecoration(labelText: 'Kullanıcı Adı', border: OutlineInputBorder())),
            const SizedBox(height: 16),
            TextField(controller: _passwordController, decoration: const InputDecoration(labelText: 'Şifre', border: OutlineInputBorder()), obscureText: true),
            const SizedBox(height: 24),
            _isLoading 
                ? const CircularProgressIndicator() 
                : ElevatedButton(
                    style: ElevatedButton.styleFrom(backgroundColor: Colors.teal, minimumSize: const Size(double.infinity, 50)),
                    onPressed: _login,
                    child: const Text('Giriş Yap', style: TextStyle(color: Colors.white, fontSize: 18)),
                  ),
          ],
        ),
      ),
    );
  }
}

// ==========================================
// 2. HARCAMA LİSTESİ EKRANI (Silme işlemi burada)
// ==========================================
class ExpenseListScreen extends StatefulWidget {
  final String token;
  const ExpenseListScreen({super.key, required this.token});

  @override
  _ExpenseListScreenState createState() => _ExpenseListScreenState();
}

class _ExpenseListScreenState extends State<ExpenseListScreen> {
  final ApiService apiService = ApiService();
  late Future<List<Expense>> futureExpenses;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  void _loadData() {
    setState(() {
      futureExpenses = apiService.getExpenses(widget.token);
    });
  }

  // Silme İşlemi
  void _deleteExpense(int id) async {
    bool success = await apiService.deleteExpense(widget.token, id);
    if (success) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Harcama silindi!')));
      _loadData(); // Listeyi yenile
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Silme işlemi başarısız!')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Harcamalarım', style: TextStyle(color: Colors.white)),
        backgroundColor: Colors.teal,
        actions: [
          IconButton(
            icon: const Icon(Icons.logout, color: Colors.white),
            onPressed: () => Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const LoginScreen())),
          )
        ],
      ),
      body: RefreshIndicator(
        onRefresh: () async => _loadData(),
        child: FutureBuilder<List<Expense>>(
          future: futureExpenses,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator());
            if (snapshot.hasError) return Center(child: Text("Hata: ${snapshot.error}"));
            if (!snapshot.hasData || snapshot.data!.isEmpty) return const Center(child: Text("Harcama bulunamadı."));

            return ListView.builder(
              itemCount: snapshot.data!.length,
              itemBuilder: (context, index) {
                var expense = snapshot.data![index];
                // Dismissible widget'ı yana kaydırarak silme (swipe-to-delete) özelliği sağlar
                return Dismissible(
                  key: Key(expense.id.toString()),
                  direction: DismissDirection.endToStart,
                  background: Container(
                    color: Colors.red,
                    alignment: Alignment.centerRight,
                    padding: const EdgeInsets.symmetric(horizontal: 20),
                    child: const Icon(Icons.delete, color: Colors.white),
                  ),
                  onDismissed: (direction) => _deleteExpense(expense.id),
                  child: Card(
                    margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
                    child: ListTile(
                      title: Text(expense.title, style: const TextStyle(fontWeight: FontWeight.bold)),
                      subtitle: Text('${expense.category} - ${expense.date}'),
                      trailing: Text('${expense.amount} ₺', style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold, fontSize: 16)),
                      // Tıklandığında Güncelleme Formuna Git
                      onTap: () async {
                        await Navigator.push(
                          context,
                          MaterialPageRoute(builder: (context) => ExpenseFormScreen(token: widget.token, expense: expense)),
                        );
                        _loadData(); // Formdan dönünce listeyi yenile
                      },
                    ),
                  ),
                );
              },
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.teal,
        child: const Icon(Icons.add, color: Colors.white),
        onPressed: () async {
          // Yeni ekleme formuna git (expense parametresi boş gidiyor)
          await Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => ExpenseFormScreen(token: widget.token)),
          );
          _loadData(); // Formdan dönünce listeyi yenile
        },
      ),
    );
  }
}

// ==========================================
// 3. EKLEME VE GÜNCELLEME FORMU EKRANI
// ==========================================
class ExpenseFormScreen extends StatefulWidget {
  final String token;
  final Expense? expense; // Eğer null ise EKLEME modu, dolu ise GÜNCELLEME modu

  const ExpenseFormScreen({super.key, required this.token, this.expense});

  @override
  _ExpenseFormScreenState createState() => _ExpenseFormScreenState();
}

class _ExpenseFormScreenState extends State<ExpenseFormScreen> {
  final ApiService apiService = ApiService();
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _amountController = TextEditingController();
  String _selectedCategory = 'Diğer';
  final List<String> _categories = ['Yemek', 'Ulaşım', 'Fatura', 'Eğlence', 'Diğer'];
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    // Eğer güncelleme modundaysak, mevcut verileri kutulara doldur
    if (widget.expense != null) {
      _titleController.text = widget.expense!.title;
      _amountController.text = widget.expense!.amount;
      _selectedCategory = _categories.contains(widget.expense!.category) ? widget.expense!.category : 'Diğer';
    }
  }

  void _saveExpense() async {
    setState(() => _isLoading = true);
    bool success;

    if (widget.expense == null) {
      // YENİ EKLEME
      success = await apiService.addExpense(widget.token, _titleController.text, _amountController.text, _selectedCategory);
    } else {
      // GÜNCELLEME
      success = await apiService.updateExpense(widget.token, widget.expense!.id, _titleController.text, _amountController.text, _selectedCategory);
    }

    setState(() => _isLoading = false);

    if (success) {
      Navigator.pop(context); // İşlem başarılıysa önceki sayfaya dön
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('İşlem başarısız oldu.')));
    }
  }

  @override
  Widget build(BuildContext context) {
    bool isEditing = widget.expense != null;

    return Scaffold(
      appBar: AppBar(
        title: Text(isEditing ? 'Harcamayı Güncelle' : 'Yeni Harcama Ekle', style: const TextStyle(color: Colors.white)),
        backgroundColor: Colors.teal,
        iconTheme: const IconThemeData(color: Colors.white),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(controller: _titleController, decoration: const InputDecoration(labelText: 'Harcama Başlığı')),
            const SizedBox(height: 10),
            TextField(controller: _amountController, decoration: const InputDecoration(labelText: 'Tutar (₺)'), keyboardType: TextInputType.number),
            const SizedBox(height: 10),
            DropdownButtonFormField<String>(
              value: _selectedCategory,
              decoration: const InputDecoration(labelText: 'Kategori'),
              items: _categories.map((cat) => DropdownMenuItem(value: cat, child: Text(cat))).toList(),
              onChanged: (val) => setState(() => _selectedCategory = val!),
            ),
            const SizedBox(height: 30),
            _isLoading 
              ? const CircularProgressIndicator()
              : ElevatedButton(
                  style: ElevatedButton.styleFrom(backgroundColor: Colors.teal, minimumSize: const Size(double.infinity, 50)),
                  onPressed: _saveExpense,
                  child: Text(isEditing ? 'Güncelle' : 'Kaydet', style: const TextStyle(color: Colors.white, fontSize: 18)),
                )
          ],
        ),
      ),
    );
  }
}

Yenilikler ve Nasıl Çalışıyor?

  1. Yana Kaydırarak Silme (Swipe-to-Delete): Flutter'ın muazzam Dismissible widget'ını kullanarak listedeki bir harcamayı sağdan sola kaydırdığında silinmesini sağladık.

  2. Ekle / Güncelle Formu: Sağ alttaki + butonuna basarsan ExpenseFormScreen boş açılır (Ekleme Modu). Listeden bir elemana tıklarsan aynı form, o elemanın verileriyle dolu olarak açılır (Güncelleme Modu).

  3. Sayfa Yenileme: Yeni bir kayıt eklediğinde, güncellediğinde veya sildiğinde _loadData() metodu otomatik olarak çalışır ve liste saniyesinde güncellenir.







Yorumlar

Bu blogdaki popüler yayınlar

Pardus Üzerine Django Kurulumu

Python ile Web Geliştirme: Django App Oluşturma