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:
| Rol | Yetki |
|---|---|
| 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:
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.
# 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)
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)
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.
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)
# 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'),
]
# 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)
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.
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.
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)),
)
],
),
),
);
}
}
Django Kaynak Kodu : https://github.com/nuritiras/config.gitFlutter Kaynak Kodu : https://github.com/nuritiras/harcama_takip.git
Yenilikler ve Nasıl Çalışıyor?
Yana Kaydırarak Silme (Swipe-to-Delete): Flutter'ın muazzam
Dismissiblewidget'ını kullanarak listedeki bir harcamayı sağdan sola kaydırdığında silinmesini sağladık.Ekle / Güncelle Formu: Sağ alttaki
+butonuna basarsanExpenseFormScreenboş açılır (Ekleme Modu). Listeden bir elemana tıklarsan aynı form, o elemanın verileriyle dolu olarak açılır (Güncelleme Modu).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
Yorum Gönder