Skip to main content
Vatandaş kaydı, telefon numarasıyla OTP tabanlı bir self-registration akışıdır. Kayıt isteği bir User aggregate’ı Draft durumunda oluşturur, bir OTP üretir ve SMS ile gönderir; kullanıcı kodu doğrulayınca telefon “verified” işaretlenir. Bu akış tamamen lokal (User aggregate) çalışır — Keycloak’a gerek yoktur.
İlgili dosyalar: src/DiyanetCleanArchitecture.API/Controllers/AuthController.cs · Application/Features/Authentication/Website/Commands/SignUpUser/* · Domain/Services/UserRegistrationService.cs · Domain/AggregatesModel/UserAggregate/User.cs · Application/DomainEventHandlers/UserOtpGenerated/SendOtpSmsDomainEventHandler.cs

Sequence — kayıt (sign-up)

Adım adım

1

Controller — ince giriş noktası

AuthController.SignUp yalnızca komutu MediatR’a iletir. Başarıda 201 Created döner.
[AllowAnonymous]
[HttpPost("sign-up")]
public async Task<IActionResult> SignUp([FromBody] SignUpUserCommand command)
{
    var result = await _mediator.Send(command);
    return result.IsSuccess ? Created("", result.Response) : BadRequest(result.Response);
}
Command şekli (SignUpUserCommand): Phone (zorunlu), FullName?, Consents?. Validator Phone için MustBeValidMobilePhone(), FullName için boş olamaz + max 100 karakter kuralını uygular.
2

Domain service — uniqueness + factory

Handler önce UserRegistrationService.RegisterNewUserAsync çağırır. Bu servis telefon/e-posta global benzersizliğini spec ile kontrol eder ve geçerse new User(...) döner:
var existingUserByPhone = await _userRepository
    .FirstOrDefaultAsync(new UserByPhoneNumberSpecification(phone), cancellationToken);

if (existingUserByPhone is not null)
{
    if (existingUserByPhone.IsPhoneNumberVerified == false)
        throw new DomainException("... zaten mevcut fakat üyelik onayı yapılmamış. Lütfen giriş yapınız");
    throw new DomainException("Bu telefon numarasına sahip bir kullanıcı zaten mevcut");
}
“Doğrulanmamış ama var olan” telefon ayrı mesajla ele alınır: kullanıcı kayıt yerine giriş akışına yönlendirilir. DomainException global exception handler tarafından 400’e çevrilir.
3

Aggregate — Draft kullanıcı + OTP challenge

User’ın internal ctor’u durumu Draft yapar ve UserCreatedDomainEvent biriktirir. Handler ardından OTP üretip aggregate’e ekler:
var otpCode = OtpCode.Generate();              // 4 hane, 1000-9999
var otpType = phone != null ? OtpType.Sms : OtpType.Email;
user.AddOtpChallenge(otpCode, otpType);
User.AddOtpChallenge içeride bir UserOtpChallenge (SMS için 1 dk geçerli) oluşturur ve UserOtpGeneratedDomainEvent biriktirir:
public void AddOtpChallenge(OtpCode code, OtpType type)
{
    if (CanRequestOtp == false)
        throw new DomainException("Kullanıcı durumu giriş yapmaya uygun değil.");

    var validityMinutes = type == OtpType.Sms ? 1 : 2;
    var challenge = new UserOtpChallenge(this, GuidFactory.New(), code, type,
        DateTime.UtcNow.AddMinutes(validityMinutes));
    OtpChallenges.Add(challenge);
    AddDomainEvent(new UserOtpGeneratedDomainEvent(this, code, type, this.Phone, this.Email));
}
CanRequestOtp, Draft veya Active durumlar için true’dur — yeni kayıt Draft olduğundan geçer.
4

Kaydet — event dispatch + SMS

await _repository.AddAsync(user);
bool result = await _unitOfWork.SaveEntitiesAsync(cancellationToken);
if (!result)
    throw new ApplicationException("Kayıt işlemi sırasında bir sorun oluştu.");
SaveEntitiesAsync, SaveChanges’ten sonra biriken domain event’leri dispatch eder. UserOtpGeneratedDomainEvent’i SendOtpSmsDomainEventHandler dinler:
public sealed class SendOtpSmsDomainEventHandler
    : INotificationHandler<UserOtpGeneratedDomainEvent>,
      INotificationHandler<UserOtpResentDomainEvent>
{
    private readonly IOtpSmsService _otpSmsService;

    private async Task SendIfSmsAsync(Phone? phone, OtpCode code, OtpType type)
    {
        if (phone is null || type != OtpType.Sms) return;   // guard
        await _otpSmsService.SendAsync(code, phone);        // NetGSM REST v2
    }
}
IOtpSmsService implementasyonu (OtpSmsService) NetGSM OTP endpoint’ine Phone.ToNetGsmFormat() ile gönderir. Bkz. SMS Servisi.
5

Challenge token üret + dön

Handler aktif challenge’ı bulur ve sonraki adımda kullanılacak kısa ömürlü challenge token’ı üretir:
var challenge = user?.GetActiveOtpChallenge(otpType);
if (challenge is null) throw new DomainException("Aktif OTP doğrulama kodu bulunamadı");

var challengeToken = _authService.CreateOtpChallengeToken(user, otpType, challenge.Id);

return ResponseWrapper<OtpChallengeDto>.Success(new OtpChallengeDto
{
    ChallengeToken = challengeToken
});
ChallengeToken, OTPChallenge JWT şemasıyla imzalanır ve userId + challengeId’yi taşır. SPA bunu saklar; SMS kodu girilince verify-otp çağrısında Authorization header’ında gönderir.
6

Aktivasyon — VerifyOtp

Kullanıcı SMS kodunu girer. SPA, challenge token’ı bearer olarak taşıyan verify-otp çağrısı yapar:
[Authorize(AuthenticationSchemes = AuthenticationSchemeTypes.OTPChallengeAuthenticationScheme)]
[HttpPost("verify-otp")]
public async Task<IActionResult> VerifyOtp([FromBody] VerifyOtpCommand command, CancellationToken ct)
{
    var result = await _mediator.Send(command, ct);
    if (result.IsSuccess == false) return BadRequest(result);
    Response.AppendRefreshToken(result.Response.RefreshToken);   // HttpOnly cookie
    return Ok(new VerifyOtpResponse(result.Response.AccessToken, result.Response.User));
}
VerifyOtpCommandHandler, userId/challengeId’yi IOtpChallengeContext üzerinden (challenge token’dan) okur, kodu doğrular, telefonu “verified” işaretler ve oturum başlatır:
if (!user.VerifyChallenge(challengeId, request.Code))
    throw new ApplicationException("Geçersiz veya süresi dolmuş kod");
// ...
user.StartSession(refreshTokenInfo, request.IsTrustedDevice, deviceInfo, clientInfo);
User.VerifyChallenge başarılıysa IsPhoneNumberVerified = true yapar ve UserPhoneNumberVerifiedDomainEvent yayar. Doğrulama detayları için OTP / TOTP Akışı.

Hata senaryoları

DurumNeredeSonuç
Telefon zaten kayıtlı (doğrulanmış)UserRegistrationServiceDomainException400
Telefon var ama doğrulanmamışUserRegistrationServiceDomainException (“giriş yapınız”) → 400
Geçersiz telefon formatıSignUpUserCommandValidatorValidationException422 + errors
SMS gönderilemedi (NetGSM)OtpSmsServiceSmsServiceException503 (kayıt yine de oluşmuştur; ResendOtp ile tekrar denenebilir)
Yanlış / süresi dolmuş OTP koduVerifyOtpCommandHandlerApplicationException400
OTP kodu DB’de UserOtpChallenge.Code (bir OtpCode value object) olarak tutulur ve SMS sadece bir kez gönderilir. Kullanıcıya kod ulaşmazsa yeni kayıt değil, resend-otp çağrılmalıdır (cooldown 60 sn, maksimum 3 yeniden gönderim — bkz. OTP playbook’u).

İlgili

OTP / TOTP Akışı

VerifyOtp, ResendOtp ve TOTP detayları.

SMS Servisi

NetGSM entegrasyonu, test numaraları, encoding.

User Aggregate

Durumlar, çocuk entity’ler, metodlar.

Domain Event'ler

AddDomainEvent → dispatch mekaniği.