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 randomrefand 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):
getUserViaUserContacts($identifier)→contactRepository->getPrimaryByValue($identifier)→ collect user IDsuserRepository->getCustomersWithAccess(userIds: $userIds, email: $identifier)— this is the gate- 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 NULLConsequences:
- Only active customer users with at least one approved school (
has_content_accessORhas_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 afindActiveByEmailfallback 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-yappage)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:
- Successfully get a “code sent” response (fake success)
- Receive no actual SMS/email
- 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.phponly (semantic fit). - (b) Backend-extend OTP to regular users (new
getUsersWithContactquery 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.
Related
- 2026-04-14-customer-vs-user-login-rules — Laravel-side customer login rules (still valid)
- 2026-04-14-school-api-routing-csrf — how school project calls the API
- 2026-04-14-otp-value-field-validation — FE validation helpers