Ana içeriğe geç

Registration

Durum: Adım 3 tamamlandı. CreateUser endpoint (backend + Flutter) tamamlandı. register_details_screen.dart tam işlevsel hale getirildi. Şifre hash'leme (Argon2id), username/password validasyonu, transaction'lı kullanıcı oluşturma ve başarıda Login ekranına yönlendirme tamamlandı. IsEmailRegisteredInUsers kontrolü eklendi — kayıtlı email RegisterEmail aşamasında ErrEmailAlreadyRegistered ile reddedilir, doğrulama koduna ulaşılmaz. Email input backend'de normalize edilir (trim + lowercase) ve display-name/angle-bracket email formatları reddedilir. Son güncelleme: 13 Temmuz 2026


User Flow

Bölüm 1: Kullanıcı adımları, ekranlar, senaryo. Teknik detay yok.

Ekran 1: Email Input

  • Kullanıcı üniversite e-posta adresini girer
  • "Devam Et" butonuna basar
  • E-posta doğrulanır ve kod gönderilir
  • Başarı: Ekran 2'ye yönlendir (10 dakika timer başlar)
  • Hata: Jenerik mesaj göster ve tekrar dene

Ekran 2: Email Verification Code

  • 10 dakikalık timer gösterilir
  • Kullanıcı email'den aldığı 6 haneli kodu girer
  • "Doğrula" butonuna basar
  • Başarı: Ekran 3'e yönlendir (önceki kodlar silinir)
  • Hata (yanlış kod): "Kod yanlış. Tekrar deneyin." (max 5 deneme)
  • Max deneme aşıldıysa: "Çok fazla deneme. Yeni kod isteyin."
  • "Yeni Kod Al" butonu: 10 dakika timer sıfırlanır, yeni kod gönderilir
  • Resend max 3 kez / 10 dakika spam koruması backend'de henüz uygulanmadı; rate-limit/abuse protection aşamasında ele alınacak
  • "Geri" butonu: Ekran 1'e dönüş (yeni kod gönderilir, oturum iptal)

Ekran 3: Username & Password

  • Kullanıcı adı (3-15 karakter, küçük harf/rakam/alt çizgi/nokta)
  • Şifre (ortak kural için docs/features/authentication/password_validation.md)
  • Şifre baştan yazı (confirm)
  • "Kullanım Koşulları" checkbox ✓ (zorunlu)
  • "Gizlilik Politikası" checkbox ✓ (zorunlu)
  • Her iki checkbox işaretlenince "Kayıt Ol" butonu aktif olur
  • Başarı: Login ekranına yönlendir
  • Hata: SnackBar ile hata mesajı göster
  • Geri tuşu: Engellendi (PopScope(canPop: false)) — geri dönüş yok

Edge Cases

Bölüm 2: Tüm "ya şöyle olursa" durumları. Normal akış dışındaki senaryolar.

Senaryo Davranış
Email Ekranında:
Email zaten users tablosunda kayıtlı (tam kayıt tamamlanmış) codes.AlreadyExistsErrEmailAlreadyRegistered — RegisterEmail aşamasında erken yakalanır, doğrulama koduna ulaşılmaz
Üniversite emaili değil (domain veya subdomaini universities tablosunda yok) Hata: codes.InvalidArgument — "universite domaini degil: "
Boş email Client-side: "Bu alan zorunludur"
Doğrulama Kodu Ekranında:
Kod yanlış girilirse (1-4. deneme) "Kod yanlış. Kalan deneme: X"
Max 5 deneme aşılırsa Kod iptal: "Çok fazla deneme. 'Yeni Kod Al' butonuna basın"
Doğrulama kodu süresi dolduysa (10 dakika) Otom. "Kodunuz süresi doldu. Yeni kod isteyin"
"Yeni Kod Al" 3 kez çıkartıldıktan sonra Bekleme süresi: "10 dakika içinde tekrar deneyin"
Uygulama kapanıp tekrar açılırsa Oturum devam: Aynı kod ekranında (kalan süre korunur)
Geri tuşuna basıp email girerse Yeni kod gönderilir, oturum sıfırlanır, timer sıfırlanır
Username/Password Ekranında:
Username zaten alınmışsa codes.AlreadyExists — "Bu kullanıcı adı kullanılıyor. Başka bir tane deneyin"
Username kuralları (3-15 küçük harf/rakam/alt çizgi/nokta) ihlal codes.InvalidArgument — username canonical kuralı için docs/features/profile/validation.md
Şifre confirm eşleşmezse Client-side SnackBar — "Şifreler eşleşmiyor"
Şifre kuralları ihlal codes.InvalidArgument — ortak password kuralı için docs/features/authentication/password_validation.md
KVKK checkbox işaretlemezse Kayıt butonu disabled kalır
Ekran 3'te sistem geri tuşuna basılırsa PopScope(canPop: false) — geri git engellendi, kullanıcı Ekran 3'te kalır
Email doğrulanmamışken CreateUser çağrılırsa codes.FailedPreconditionErrEmailNotVerified
Email email_verification tablosunda yoksa codes.NotFoundErrEmailNotFound
Email zaten users tablosundaysa codes.AlreadyExistsErrEmailAlreadyRegistered
Üniversite domain eşleşmezse codes.NotFoundErrUniversityNotFound
Network/System:
Internet kesilirse (herhangi bir adımda) Hata: "Bağlantı hatası. Lütfen tekrar deneyin"
Backend timeout Hata: "İstek zaman aşımına uğradı. Tekrar deneyin"
Uygulama crash/kill olursa Tekrar açılınca: En son seçilen email adımından başla
2 cihazdan aynı emaille aynı anda kayıt Son gelen kod geçerli, önceki iptal

Implementation

Durum: Adım 3 tamamlandı. Adım 1 tamamlandı: Email kayıt (domain doğrulama + subdomain desteği). ON CONFLICT DO UPDATE: aynı email tekrar gelirse kayıt üstüne yazılır, yeni kod gönderilir. Adım 2 tamamlandı: VerifyCode proto, backend ve Flutter tarafı tamamen çalışıyor. remaining_attempts Flutter'a doğru aktarılıyor. code_validation.go eklendi (backend girdi validasyonu). Frontend validasyonu geçici olarak kaldırıldı (backend testi için); yayına alınmadan önce tekrar eklenecek. Countdown Timer tamamlandı: RegisterEmailResponse'a int64 expires_at_unix eklendi. Backend her RegisterEmail çağrısında expiresAt = time.Now() + verificationCodeTTL hesaplar, Flutter'a döndürür. Flutter DateTime.fromMillisecondsSinceEpoch(expiresAtUnix * 1000) ile Timer.periodic(1s) countdown başlatır. Yeni Kod Al (Resend) tamamlandı: Süre dolunca "Yeni kod almak için geri dönün." text'i kaldırıldı, "Yeni Kod Al" butonu eklendi. ResendCodeLoading/Success/Failure state'leri ve resendCode() cubit metodu eklendi. Backend RegisterEmail endpoint'i ON CONFLICT DO UPDATE ile kod/süre/attempt'i sıfırlar — ayrı bir RPC gerekmez. Adım 3 tamamlandı: CreateUser RPC eklendi (proto + backend + Flutter). register_details_screen.dart tam işlevsel hale getirildi — form validasyonu, gRPC çağrısı, başarıda Login ekranına yönlendirme tamamlandı. Şifre ortak auth/password paketiyle Argon2id olarak hash'leniyor. username_validation.go ve password_validation.go eklendi. repository.go'ya GetUniversityIDByDomain + transaction'lı CreateUser metodları eklendi. email_verification kaydı kullanıcı oluşturulduktan sonra transaction içinde siliniyor. IsEmailRegisteredInUsers eklendi: repository.go'ya SELECT EXISTS(...FROM users...) sorgusu, service.go'da RegisterEmail akışına domain kontrolünden sonra users tablosu kontrolü eklendi. Kayıtlı email artık ErrEmailAlreadyRegisteredcodes.AlreadyExists döndürür; silent success kaldırıldı. Email normalize/sıkı format kontrolü eklendi: NormalizeEmail() email'i trim + lowercase yapar; RegisterEmail, VerifyCode, CreateUser normalize email kullanır; display-name/angle-bracket formatları reddedilir. Username canonical kuralı docs/features/profile/validation.md içinde tanımlıdır. Registration akışı aynı NormalizeUsername() kuralını kullanır. Password ortak kuralı docs/features/authentication/password_validation.md içinde tanımlıdır. Registration akışı aynı ValidatePasswordFormat() ve Argon2id hashleme kuralını kullanır. Son güncelleme: 13 Temmuz 2026

Önemli mimari not — gRPC nil error kuralı: Backend, yanlış kod için error != nil döndürmüşse gRPC framework response body'yi silerdi; Flutter remaining_attempts'ı hiç alamazdı. Bu nedenle yanlış kod durumunda service.go nil error ile {success:false, remaining_attempts:N} döndürür. Gerçek hatalar (süre doldu, kilitlendi vb.) hala error != nil olarak döner.

Önemli mimari not — sentinel error + wrap pattern: Backend'de her hata durumu için sentinel error tanımlı (ErrInvalidUsername, ErrInvalidPassword, vb.). Validation hataları özel struct'lara (usernameFormatErr, passwordFormatErr) wrap'leniyor. Bu struct'lar .Error() ile kullanıcıya gösterilecek mesajı, .Unwrap() ile sentinel'i döndürür. Handler errors.Is() ile sentinel'i kontrol eder, gRPC status kodu belirler; mesajı ise err.Error() ile direkt alır.

Akış

Ekran 1: RegisterEmail

"Devam Et" butonu
  → EmailInputScreen
  → RegisterCubit.submitEmail(email)
  → RegisterRepository.registerEmail(email)
  → DefaultRegisterGrpcClient → GrpcClient.stub
  ─── gRPC / TCP :50051 ───
  → EmailHandler.RegisterEmail()
  → EmailService.RegisterEmail()
  → validation.NormalizeEmail(req.Email)        ← trim + lowercase
  → validation.ValidateEmailFormat(email)       ← pure format kontrolü; display-name/angle-bracket reddedilir
  → domain = strings.Split(email, "@")[1]       ← @ sonrası domain
  → EmailRepository.IsUniversityDomain(domain)  ← DB sorgusu
    SELECT EXISTS(
      SELECT 1 FROM universities
      WHERE ($1 = domain OR $1 LIKE '%.' || domain)
      AND is_active = true
    )
  → EmailRepository.IsEmailRegisteredInUsers(email)  ← DB sorgusu
    SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)
    → kayıtlıysa: ErrEmailAlreadyRegistered  (codes.AlreadyExists)
  → codeHasher.Hash(code)                         ← DB icin HMAC-SHA-256 ozeti
  → EmailRepository.SaveEmail()
  → INSERT INTO email_verification (email, verification_code_hash, expires_at)
    VALUES ($1, $2, $3)
    ON CONFLICT (email) DO UPDATE SET
        verification_code_hash = EXCLUDED.verification_code_hash,
        expires_at        = EXCLUDED.expires_at,
        attempt_count     = 0,
        is_verified       = FALSE

Başarı: {success: true, expires_at_unix: <unix_timestamp>}RegisterSuccess(expiresAtUnix: expiresAtUnix) emit edilir → Navigator.pushVerificationCodeScreen(email: email, expiresAtUnix: expiresAtUnix) Hata (geçersiz format): codes.InvalidArgument → SnackBar hata mesajı Hata (geçersiz domain): codes.InvalidArgument → SnackBar "universite domaini degil: " Hata (kayıtlı email): codes.AlreadyExists → SnackBar hata mesajı (doğrulama koduna erişilmez) Hata (DB/sistem hatası): codes.Internal → SnackBar hata mesajı

Not: DO UPDATE kullanıldığı için aynı email tekrar gönderilirse AlreadyExists hatası dönmez; kayıt sessizce güncellenir ve yeni kod gönderilir. Bu davranış Resend akışında da kullanılır.

Ekran 2: VerifyCode

"Doğrula" butonu
  → VerificationCodeScreen
  → RegisterCubit.submitCode(email, code)
  → RegisterRepository.verifyCode(email, code)   ← Future<int?> döndürür
  → DefaultRegisterGrpcClient → GrpcClient.stub
  ─── gRPC / TCP :50051 ───
  → EmailHandler.VerifyCode()
  → EmailService.VerifyCode()
  → validation.ValidateCodeFormat(req.Code)         ← 6 hane, sadece rakam (pure function)
      → Hata: fmt.Errorf("%w", ErrInvalidCodeFormat) ← handler: codes.InvalidArgument
  → EmailRepository.GetVerificationRecord(email)    ← DB'den kaydı oku
  → is_verified == true?   → ErrAlreadyVerified  (codes.AlreadyExists)
  → expires_at geçti?     → ErrCodeExpired      (codes.DeadlineExceeded)
  → attempt_count >= 5?   → ErrMaxAttemptsExceeded (codes.ResourceExhausted)
  → codeHasher.Matches(code, verification_code_hash) false?
      → IncrementAttemptCount()
      → remaining = 5 - (attemptCount + 1)
      → return {success:false, remaining_attempts:N}, nil   ← nil error! (gRPC kuralı)
  → codeHasher.Matches(code, verification_code_hash) true?
      → MarkEmailVerified()
      → return {success:true, remaining_attempts:0}, nil

Flutter tarafında verifyCode dönüş değerine göre cubit davranışı:

Dönen Değer Durum Cubit Davranışı
null Başarılı CodeVerificationSuccess emit et
int > 0 (ör. 3) Yanlış kod CodeVerificationFailure('Yanlış kod. 3 deneme hakkınız kaldı.')
int == 0 Son hak da tukendi CodeVerificationFailure('Maksimum deneme sayısına ulaştınız.')
exception Gerçek hata CodeVerificationFailure(mesaj) — remainingAttempts null

Başarı: CodeVerificationSuccessNavigator.pushRegisterDetailsScreen(email: email) Hata (yanlış kod): CodeVerificationFailure → SnackBar "Yanlış kod. N deneme hakkınız kaldı." Hata (0 hak): CodeVerificationFailure → SnackBar "Maksimum deneme sayısına ulaştınız." Hata (süre doldu): CodeVerificationFailure → SnackBar backend mesajı Hata (format): codes.InvalidArgument → SnackBar backend mesajı (attempt_count artmaz)

Ekran 2: Countdown Timer

Backend RegisterEmail her çağrıldığında (ilk kayıt veya resend) expiresAt = time.Now().Add(verificationCodeTTL) hesaplar ve RegisterEmailResponse.expires_at_unix (Unix saniye) olarak Flutter'a döndürür.

Flutter VerificationCodeScreen._startTimer():

expiresAt = DateTime.fromMillisecondsSinceEpoch(_expiresAtUnix * 1000)
Timer.periodic(1s) → diff = expiresAt.difference(DateTime.now())
  diff > 0 → setState(_remaining = diff)   → MM:SS göster
  diff ≤ 0 → setState(_expired = true)       → timer cancel, TextField disabled, verifyButton disabled

_expiresAtUnix neden late int field'a taşındı: - widget.expiresAtUnix final — widget parametresi değiştirilemiyor - Resend gelince BlocListener state.expiresAtUnix'i _expiresAtUnix'e yazar ve _startTimer() yeniden çağrılır

Ekran 2: Yeni Kod Al (Resend)

Süre dolunca _expired = trueTextField ve verifyButton devre dışı kalır → resendButton ekranda belirir.

"Yeni Kod Al" butonuna basılır
  → RegisterCubit.resendCode(email)
  → emit(ResendCodeLoading)   → resendButton alanına CircularProgressIndicator
  → RegisterRepository.registerEmail(email)   ← submitEmail ile AYNI metod
  → DefaultRegisterGrpcClient → GrpcClient.stub
  ─── gRPC / TCP :50051 ───
  → EmailHandler.RegisterEmail()
  → EmailService.RegisterEmail()    ← yeni crypto/rand kodu + yeni expiresAt
  → SaveEmail() → ON CONFLICT DO UPDATE:
      verification_code_hash = yeni kodun HMAC özeti
      expires_at        = şimdi + verificationCodeTTL
      attempt_count     = 0
      is_verified       = FALSE
  ← {success:true, expires_at_unix: yeni_timestamp}
  → emit(ResendCodeSuccess(expiresAtUnix: yeniDeger))
  → BlocListener:
      _timer?.cancel()
      setState: _expiresAtUnix = state.expiresAtUnix, _expired = false
      _startTimer()         ← yeni süre başlar
      _codeController.clear()

Neden ResendCodeSuccess ayrı state (neden RegisterSuccess emit edilmez): - email_input_screen.dart'daki BlocListener RegisterSuccess dinler → Navigator.push tetiklenir → Ekran 2 üstüne tekrar Ekran 2 açılırdı (hatalı navigasyon) - ResendCodeSuccess yalnızca VerificationCodeScreen'in kendi BlocListener'ı tarafından işlenir

Ekran 3: CreateUser

"Kayıt Ol" butonu
  → RegisterDetailsScreen
  → (client-side) şifre eşleşmiyorsa → SnackBar "Şifreler eşleşmiyor", backend'e gidilmez
  → RegisterCubit.submitDetails(email, username, password, termsAccepted, privacyAccepted)
  → emit(CreateUserLoading)
  → RegisterRepository.createUser(email, username, password, termsAccepted, privacyAccepted)
  → DefaultRegisterGrpcClient.createUser() → GrpcClient.stub
  ─── gRPC / TCP :50051 ───
  → EmailHandler.CreateUser()
  → EmailService.CreateUser()
  → validation.NormalizeUsername(req.Username)        ← trim + lowercase canonical username
  → validation.ValidateUsernameFormat(username)       ← 3-15 karakter, küçük harf/rakam/alt çizgi/nokta
      → Hata: usernameFormatErr{} wraps ErrInvalidUsername → handler: codes.InvalidArgument
  → validation.ValidatePasswordFormat(req.Password)   ← ortak password kuralı: docs/features/authentication/password_validation.md
      → Hata: passwordFormatErr{} wraps ErrInvalidPassword → handler: codes.InvalidArgument
  → req.TermsAccepted == false?  → ErrTermsNotAccepted    (codes.InvalidArgument)
  → req.PrivacyAccepted == false? → ErrPrivacyNotAccepted  (codes.InvalidArgument)
  → EmailRepository.GetUniversityIDByDomain(domain)
      SELECT id FROM universities
      WHERE ($1 = domain OR $1 LIKE '%.' || domain)
      AND is_active = true
      → bulunamazsa: ErrUniversityNotFound (codes.NotFound)
  → password.Hash(req.Password)   ← Argon2id (m=19456, t=2, p=1)
  → EmailRepository.CreateUser(ctx, email, normalizedUsername, hashedPassword, universityID)
      BEGIN TRANSACTION
        SELECT id, is_verified FROM email_verification WHERE email = $1 FOR UPDATE
        → kayıt yoksa:    ErrEmailNotFound        (codes.NotFound)
        → is_verified=false: ErrEmailNotVerified  (codes.FailedPrecondition)
        INSERT INTO users (email, username, password_hash, university_id)
        VALUES ($1, $2, $3, $4)
        → email çakışması (23505):    ErrEmailAlreadyRegistered   (codes.AlreadyExists)
        → username çakışması (23505): ErrUsernameAlreadyExists    (codes.AlreadyExists)
        DELETE FROM email_verification WHERE email = $1
      COMMIT
  ← {success:true, message:"Kayit basarili"}
  → emit(CreateUserSuccess)

Başarı: CreateUserSuccessNavigator.pushReplacementLoginScreen Hata: CreateUserFailure(message) → SnackBar hata mesajı

Handler error mapping (CreateUser):

Sentinel Error gRPC Code
ErrInvalidUsername codes.InvalidArgument
ErrInvalidPassword codes.InvalidArgument
ErrTermsNotAccepted codes.InvalidArgument
ErrPrivacyNotAccepted codes.InvalidArgument
ErrEmailNotVerified codes.FailedPrecondition
ErrUniversityNotFound codes.NotFound
ErrEmailNotFound codes.NotFound
ErrEmailAlreadyRegistered codes.AlreadyExists
ErrUsernameAlreadyExists codes.AlreadyExists
diğer codes.Internal

Dosyalar

Proto: - proto/yackr/v1/yackr.protoRegisterEmail, VerifyCode, CreateUser RPC tanımları - RegisterEmailResponse: success, int64 expires_at_unix - VerifyCodeRequest: email, code - VerifyCodeResponse: success, remaining_attempts - CreateUserRequest: email, username, password, terms_accepted, privacy_accepted (YENİ) - CreateUserResponse: success, message (YENİ) - Generated: backend/gen/go/yackr/v1/ (Go), app/lib/gen/yackr/v1/ (Dart)

Backend: - backend/cmd/server/main.go — sunucuyu başlatır - backend/internal/server/server.go — handler'ı oluşturur ve kaydeder - backend/internal/auth/register/handler.go — gRPC isteğini karşılar; RegisterEmail + VerifyCode + CreateUser handler'ları; sentinel error → gRPC status code eşlemesi; errors.Is() ile unwrap zinciri kontrol edilir; RegisterEmail'de ErrEmailAlreadyRegisteredcodes.AlreadyExists eşlemesi eklendi - backend/internal/auth/register/service.go — servis katmanı; RegisterEmail, VerifyCode, CreateUser metodları; üç metod da email'i validation.NormalizeEmail() ile normalize eder; CreateUser username'i validation.NormalizeUsername() ile normalize eder, username/password validasyonu + terms/privacy kontrolü + hash + repository çağrısı yapar; RegisterEmail'de domain kontrolünden sonra IsEmailRegisteredInUsers kontrolü eklendi; EmailRepositoryInterface'e IsEmailRegisteredInUsers metodu eklendi - backend/internal/auth/register/repository.go — veritabanı CRUD; GetVerificationRecord, IncrementAttemptCount, MarkEmailVerified, GetUniversityIDByDomain, CreateUser (transaction), IsEmailRegisteredInUsers metodları; son metod users tablosunda email varlığını kontrol eder - backend/internal/auth/password/password.goHash() ve Verify() ile ortak Argon2id password hashleme/doğrulama; ortak password saklama kuralı için docs/features/authentication/password_validation.md - backend/internal/auth/register/verification_code.gogenerateVerificationCode() (crypto/rand, 100000-999999) ve verificationCodeTTL - backend/internal/auth/verification_code/hasher.go — kayıt ve şifremi-unuttum kodları için ortak HMAC-SHA-256 özetleme/karşılaştırma - backend/internal/db/db.go — veritabanı bağlantısı - backend/internal/db/migrations/create_email_verification_table.sqlemail_verification tablosu - backend/internal/db/migrations/create_universities_table.sqluniversities tablosu - backend/internal/db/seeds/universities_tr.json — Türkiye üniversiteleri seed verisi - backend/internal/db/seeds/seed_universities.py — seed yükleme scripti - backend/internal/validation/email_validation.go — email normalize + format validator (pure function); NormalizeEmail() trim + lowercase yapar; ValidateEmailFormat() display-name ve angle-bracket formatlarını reddeder - backend/internal/validation/code_validation.go — doğrulama kodu format validator; boş, 6 haneli olmayan, rakam dışı karakter kontrolü - backend/internal/validation/username_validation.goYENİNormalizeUsername(): trim + lowercase canonical username; ValidateUsernameFormat(): 3-15 karakter, küçük harf/rakam/alt çizgi/nokta kuralı; usernameFormatErr struct ile wrap (sentinel: ErrInvalidUsername) - backend/internal/validation/password_validation.goYENİValidatePasswordFormat(): ortak password kuralını uygular; detaylar docs/features/authentication/password_validation.md

Backend testler: - backend/internal/validation/email_validation_test.go — email format unit testler (table-driven); uppercase normalize, display-name/angle-bracket red ve NormalizeEmail() testleri dahil - backend/internal/validation/code_validation_test.goValidateCodeFormat unit testler - backend/internal/validation/username_validation_test.goYENİValidateUsernameFormat ve NormalizeUsername unit testleri (table-driven); geçerli/geçersiz senaryolar, hata mesajı doğrulama; 100% coverage - backend/internal/validation/password_validation_test.goYENİValidatePasswordFormat unit testler (table-driven); geçerli/geçersiz senaryolar, hata mesajı doğrulama; 100% coverage - backend/internal/auth/register/handler_test.go — handler unit; TestHandler_CreateUser_Success, TestHandler_CreateUser_ErrorMapping (9 sentinel → gRPC code tablosu), TestHandler_CreateUser_WrappedUsernameError, TestHandler_CreateUser_WrappedPasswordError eklendi - backend/internal/auth/register/service_test.go — service unit; RegisterEmail, VerifyCode, CreateUser email ve username normalize testleri dahil; 11 CreateUser senaryosu eklendi: başarı, geçersiz email/username/password, terms/privacy kabul edilmemesi, üniversite bulunamadı, repository hatası, validation sıralama, wrapped hata mesajları - backend/internal/auth/password/password_test.goHash() ve Verify() unit testleri: PHC format, unique salt, boş/uzun password, doğru/yanlış password, Unicode/boşluk, bozuk PHC/salt/hash - backend/internal/auth/register/verification_code_test.go — kod üretimi unit (6 hane, 100000–999999 aralığı, sadece rakam, TTL pozitif) - backend/internal/auth/verification_code/hasher_test.go — HMAC anahtar doğrulama, doğru/yanlış kod ve farklı anahtar karşılaştırma unit testleri - backend/internal/auth/register/repository_integration_test.go — DB integration; TestRepo_CreateUser_Success, TestRepo_CreateUser_EmailNotVerified, TestRepo_CreateUser_EmailNotFound, TestRepo_CreateUser_DuplicateEmail, TestRepo_CreateUser_DuplicateUsername, TestRepo_GetUniversityIDByDomain_Exists, TestRepo_GetUniversityIDByDomain_NotExists, TestRepo_GetUniversityIDByDomain_Subdomain (tüm fonksiyon isimleri TestRepo_ prefix'i aldı — aynı paketteki service_test.go ile isim çakışmasını önlemek için)

Flutter: - app/lib/main.dart — entry point, BlocProvider kurulumu - app/lib/core/config.dart — gRPC bağlantı ayarları - app/lib/core/grpc_client.dart — gRPC client - app/lib/features/auth/register/register_repository.dart — gRPC çağrısı ve hata yönetimi; registerEmail Future<int> döndürür (expiresAtUnix); verifyCode Future<int?> döndürür; createUser YENİFuture<void> döndürür, success=false gelirse exception fırlatır - app/lib/features/auth/register/cubit/register_cubit.dart — state yönetimi; submitEmail, submitCode, resendCode metodları; submitDetails(email, username, password, termsAccepted, privacyAccepted) YENİCreateUserLoading/Success/Failure emit eder - app/lib/features/auth/register/cubit/register_state.dart — state tanımları; RegisterSuccess(expiresAtUnix), CodeVerificationFailure(remainingAttempts), ResendCodeLoading, ResendCodeSuccess(expiresAtUnix), ResendCodeFailure(message); CreateUserLoading, CreateUserSuccess, CreateUserFailure(message) YENİ - app/lib/features/auth/register/ui/email_input_screen.dart — email giriş ekranı - app/lib/features/auth/register/ui/verification_code_screen.dart — doğrulama kodu ekranı (Ekran 2); resend sistemi dahil - app/lib/features/auth/register/ui/register_details_screen.dartTAM İŞLEVSEL HALE GETİRİLDİ — kullanıcı adı + şifre + şifre tekrar + 2 checkbox; BlocListener ile CreateUserSuccessLoginScreen'e Navigator.pushReplacement; CreateUserFailure → SnackBar; şifre uyuşmazlığı client-side kontrol; PopScope(canPop: false) + AppBar(automaticallyImplyLeading: false)

Flutter testler: - app/test/features/auth/register/email_input_screen_test.dart — widget testleri - app/test/features/auth/register/verification_code_screen_test.dart — widget testleri; resend dahil - app/test/features/auth/register/register_repository_test.dart — repository unit; createUser başarılı, createUser hata testleri eklendi - app/test/features/auth/register/register_cubit_test.dart — cubit unit; submitDetails başarılı: CreateUserLoading → CreateUserSuccess, submitDetails hata: CreateUserLoading → CreateUserFailure testleri eklendi - app/test/features/auth/register/register_details_screen_test.dart — widget; mock shouldSucceed parametreli hale getirildi; Şifre eşleşmiyorsa SnackBar gösterir, Başarılı kayıt LoginScreen'e yönlendirir, Backend hatası SnackBar gösterir testleri eklendi