Compare commits
3 Commits
3f35f7dc17
...
0c992404ea
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c992404ea | |||
| d796d9e6a1 | |||
| 2ba0cfffc8 |
+2
-22
@@ -7,25 +7,5 @@
|
|||||||
- Phone-first auth works, but `USERNAME_FIELD` is email; align identifier strategy to avoid future auth confusion.
|
- Phone-first auth works, but `USERNAME_FIELD` is email; align identifier strategy to avoid future auth confusion.
|
||||||
|
|
||||||
## Near-Term Focus
|
## Near-Term Focus
|
||||||
- Hardening Authentica integration (timeouts, retries, async delivery) and aligning notification provider choices.
|
- finalize otp testing
|
||||||
|
- work on authentication and complete it
|
||||||
**Authentica E2E**
|
|
||||||
Run the real Authentica OTP flow only when explicitly enabled.
|
|
||||||
|
|
||||||
Env vars (in `backend/.env` or shell):
|
|
||||||
- `AUTHENTICA_E2E=1`
|
|
||||||
- `AUTHENTICA_API_KEY=...`
|
|
||||||
- `AUTHENTICA_E2E_PHONE=...` (must receive OTP)
|
|
||||||
- `AUTHENTICA_E2E_CODE=...` (required; no interactive prompt)
|
|
||||||
|
|
||||||
Command:
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
PYTEST_ADDOPTS='' python3 -m pytest apps/accounts/tests -m external
|
|
||||||
```
|
|
||||||
|
|
||||||
Suggested flow:
|
|
||||||
1. Trigger the E2E test to send the OTP, then set `AUTHENTICA_E2E_CODE` and re-run if needed.
|
|
||||||
- Decide and document payment lifecycle scope (capture/refund supported vs explicitly out of scope).
|
|
||||||
- Add timeouts/logging for external calls or introduce minimal async jobs for OTP/notifications.
|
|
||||||
- Keep booking, payment, and notification orchestration in service layers, not views.
|
|
||||||
@@ -57,57 +57,6 @@ class ConsoleOtpProvider(BaseOtpProvider):
|
|||||||
logger.info("OTP WhatsApp to %s: %s", to_number, message)
|
logger.info("OTP WhatsApp to %s: %s", to_number, message)
|
||||||
|
|
||||||
|
|
||||||
class TwilioOtpProvider(BaseOtpProvider):
|
|
||||||
"""Twilio provider for SMS and WhatsApp OTP delivery. Requires TWILIO_* env vars."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.account_sid = os.getenv("TWILIO_ACCOUNT_SID")
|
|
||||||
self.auth_token = os.getenv("TWILIO_AUTH_TOKEN")
|
|
||||||
self.from_number = os.getenv("TWILIO_FROM_NUMBER")
|
|
||||||
self.whatsapp_from = os.getenv("TWILIO_WHATSAPP_FROM")
|
|
||||||
|
|
||||||
def _assert_config(self) -> None:
|
|
||||||
if not self.account_sid or not self.auth_token or not self.from_number:
|
|
||||||
raise ValueError(_("Twilio credentials are not configured"))
|
|
||||||
|
|
||||||
def _get_client(self):
|
|
||||||
from twilio.rest import Client
|
|
||||||
self._assert_config()
|
|
||||||
return Client(self.account_sid, self.auth_token)
|
|
||||||
|
|
||||||
def send_sms(self, to_number: str, message: str) -> None:
|
|
||||||
client = self._get_client()
|
|
||||||
client.messages.create(body=message, from_=self.from_number, to=to_number)
|
|
||||||
|
|
||||||
def send_whatsapp(self, to_number: str, message: str) -> None:
|
|
||||||
self._assert_config()
|
|
||||||
if not self.whatsapp_from:
|
|
||||||
raise ValueError(_("Twilio WhatsApp sender is not configured"))
|
|
||||||
client = self._get_client()
|
|
||||||
from_ = f"whatsapp:{self.whatsapp_from}"
|
|
||||||
to = f"whatsapp:{to_number}"
|
|
||||||
client.messages.create(body=message, from_=from_, to=to)
|
|
||||||
|
|
||||||
|
|
||||||
class UnifonicOtpProvider(BaseOtpProvider):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.app_sid = os.getenv("UNIFONIC_APP_SID")
|
|
||||||
self.sender_id = os.getenv("UNIFONIC_SENDER_ID")
|
|
||||||
self.whatsapp_sender = os.getenv("UNIFONIC_WHATSAPP_SENDER")
|
|
||||||
|
|
||||||
def _assert_config(self) -> None:
|
|
||||||
if not self.app_sid or not self.sender_id:
|
|
||||||
raise ValueError(_("Unifonic credentials are not configured"))
|
|
||||||
|
|
||||||
def send_sms(self, to_number: str, message: str) -> None:
|
|
||||||
self._assert_config()
|
|
||||||
raise NotImplementedError(_("Unifonic SMS adapter not implemented yet"))
|
|
||||||
|
|
||||||
def send_whatsapp(self, to_number: str, message: str) -> None:
|
|
||||||
self._assert_config()
|
|
||||||
if not self.whatsapp_sender:
|
|
||||||
raise ValueError(_("Unifonic WhatsApp sender is not configured"))
|
|
||||||
raise NotImplementedError(_("Unifonic WhatsApp adapter not implemented yet"))
|
|
||||||
|
|
||||||
|
|
||||||
class AuthenticaOtpProvider(BaseOtpProvider):
|
class AuthenticaOtpProvider(BaseOtpProvider):
|
||||||
@@ -197,8 +146,6 @@ class AuthenticaOtpProvider(BaseOtpProvider):
|
|||||||
|
|
||||||
PROVIDERS = {
|
PROVIDERS = {
|
||||||
"console": ConsoleOtpProvider,
|
"console": ConsoleOtpProvider,
|
||||||
"twilio": TwilioOtpProvider,
|
|
||||||
"unifonic": UnifonicOtpProvider,
|
|
||||||
"authentica": AuthenticaOtpProvider,
|
"authentica": AuthenticaOtpProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
"""Mocked end-to-end phone auth flow using Authentica OTP provider."""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.urls import reverse
|
|
||||||
|
|
||||||
from apps.accounts.models import User
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@override_settings(OTP_PROVIDER="authentica")
|
|
||||||
@patch("requests.post")
|
|
||||||
def test_phone_auth_flow_with_authentica_mock(mock_post, client):
|
|
||||||
def make_response(payload, ok=True):
|
|
||||||
response = MagicMock()
|
|
||||||
response.ok = ok
|
|
||||||
response.json.return_value = payload
|
|
||||||
response.text = ""
|
|
||||||
return response
|
|
||||||
|
|
||||||
def side_effect(url, headers=None, json=None, timeout=None):
|
|
||||||
assert headers and headers.get("X-Authorization") == "api-key"
|
|
||||||
assert timeout == 7.0
|
|
||||||
if url.endswith("/api/v2/send-otp"):
|
|
||||||
assert json == {"method": "sms", "phone": "+966512345678"}
|
|
||||||
return make_response({"success": True})
|
|
||||||
if url.endswith("/api/v2/verify-otp"):
|
|
||||||
if json == {"phone": "+966512345678", "otp": "123456"}:
|
|
||||||
return make_response({"verified": True})
|
|
||||||
return make_response({"verified": False})
|
|
||||||
raise AssertionError(f"Unexpected URL {url}")
|
|
||||||
|
|
||||||
with patch.dict(
|
|
||||||
os.environ,
|
|
||||||
{
|
|
||||||
"AUTHENTICA_API_KEY": "api-key",
|
|
||||||
"AUTHENTICA_TIMEOUT_SECONDS": "7",
|
|
||||||
},
|
|
||||||
):
|
|
||||||
mock_post.side_effect = side_effect
|
|
||||||
|
|
||||||
request_url = reverse("phone_auth_request")
|
|
||||||
verify_url = reverse("phone_auth_verify")
|
|
||||||
|
|
||||||
response = client.post(
|
|
||||||
request_url,
|
|
||||||
{"phone_number": "0512345678", "channel": "sms", "first_name": "Sara"},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert response.status_code == 201
|
|
||||||
request_id = response.json()["request_id"]
|
|
||||||
|
|
||||||
bad = client.post(
|
|
||||||
verify_url,
|
|
||||||
{"request_id": request_id, "code": "000000"},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert bad.status_code == 400
|
|
||||||
|
|
||||||
good = client.post(
|
|
||||||
verify_url,
|
|
||||||
{"request_id": request_id, "code": "123456"},
|
|
||||||
content_type="application/json",
|
|
||||||
)
|
|
||||||
assert good.status_code == 200
|
|
||||||
|
|
||||||
user = User.objects.filter(phone_number="+966512345678").first()
|
|
||||||
assert user is not None
|
|
||||||
assert user.is_phone_verified is True
|
|
||||||
Reference in New Issue
Block a user