Proje: Okul Platform

External API /auth/otp + /auth/login (OTP path) is customer-only

Source: ~/Desktop/project/api/okulcom-api (the external API repo that school.okul.com.tr calls).

OTP generation (POST /auth/otp)

Implemented in App\Http\Controllers\Customer\OtpCodeController@generate (note Customer namespace).

  • Accepts any value (email or phone)
  • Returns { data: { ref, expires_at, expires_in_seconds }, message }
  • User enumeration protection: on OtpUserNotFoundException, returns a fake success with a random ref and standard validity. A caller cannot tell from the response whether the value exists in the DB.

OTP login (POST /auth/login with {value, code})

Implemented in App\Http\Controllers\User\AuthController@login → dispatches to AuthService::attemptOtpLogin.

attemptOtpLogin flow (App\Services\Controllers\User\AuthService.php):

  1. getUserViaUserContacts($identifier)contactRepository->getPrimaryByValue($identifier) → collect user IDs
  2. userRepository->getCustomersWithAccess(userIds: $userIds, email: $identifier)this is the gate
  3. Verify OTP code against each candidate user; first to pass gets a token

The gate query (UserRepository::getCustomersWithAccess, line 168):

SELECT users.*
FROM users
JOIN users_types ut ON ut.id = users.user_type_id
JOIN customer_users cu ON cu.user_id = users.id
JOIN customer_user_schools cus ON cus.user_id = users.id
WHERE cu.status = TRUE
  AND users.is_active = TRUE
  AND ut.type = 'CUSTOMER'
  AND (cus.has_content_access = TRUE OR cus.has_lead_access = TRUE)
  AND cu.deleted_at IS NULL
  AND cus.deleted_at IS NULL

Consequences:

  • Only active customer users with at least one approved school (has_content_access OR has_lead_access) can log in via OTP.
  • Regular users (parents, students, general visitors) cannot use OTP login — they get LoginFailedException::failed().
  • Password login path (attemptPasswordLogin) DOES have a findActiveByEmail fallback that catches regular users — so password login is still open to everyone. Only the OTP path is customer-only.

Implications for the school project FE (PR 2000)

The FE integration lives in:

  • frontend/includes/login-modal.blade.php (public desktop modal, any visitor)
  • frontend/components/login.blade.php (public /giris-yap page)
  • mobile/components/login-popup.blade.php (public mobile popup)
  • mobile/components/login.blade.php (public mobile /giris-yap)

A regular visitor (parent) clicking “SMS/E-posta Kodu ile Giriş Yap” will:

  1. Successfully get a “code sent” response (fake success)
  2. Receive no actual SMS/email
  3. Enter any code → get “Giriş yapılamadı” error

This is a UX mismatch: OTP is prominently offered on the public login but only usable by customers. Two reasonable fixes:

  • (a) Remove OTP from public login; add it to customer/user/login.blade.php only (semantic fit).
  • (b) Backend-extend OTP to regular users (new getUsersWithContact query without customer filter) so the public OTP flow actually works.

expires_in_seconds

OTP response includes data.expires_in_seconds — a concrete validity. FE should prefer this over the hard-coded 180s default. Check both data.expires_in_seconds and data.data.expires_in_seconds in the extractor.