authly-sso 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- authly/__init__.py +37 -0
- authly/client.py +178 -0
- authly/exceptions.py +28 -0
- authly/services.py +281 -0
- authly/urls.py +16 -0
- authly/views.py +587 -0
- authly_sso-0.4.0.dist-info/METADATA +400 -0
- authly_sso-0.4.0.dist-info/RECORD +11 -0
- authly_sso-0.4.0.dist-info/WHEEL +5 -0
- authly_sso-0.4.0.dist-info/licenses/LICENSE +21 -0
- authly_sso-0.4.0.dist-info/top_level.txt +1 -0
authly/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
AuthlyClient,
|
|
3
|
+
calculate_signature,
|
|
4
|
+
verify_webhook_signature,
|
|
5
|
+
)
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
AuthlySDKError,
|
|
8
|
+
AuthlyTokenExchangeError,
|
|
9
|
+
AuthlyProfileFetchError,
|
|
10
|
+
AuthlyWebhookSignatureError,
|
|
11
|
+
)
|
|
12
|
+
from .services import (
|
|
13
|
+
AuthlyService,
|
|
14
|
+
get_authly_client,
|
|
15
|
+
populate_user_fields,
|
|
16
|
+
find_or_create_user,
|
|
17
|
+
generate_user_jwt_tokens,
|
|
18
|
+
extract_user_payload,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "0.4.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AuthlyClient",
|
|
25
|
+
"AuthlyService",
|
|
26
|
+
"get_authly_client",
|
|
27
|
+
"populate_user_fields",
|
|
28
|
+
"find_or_create_user",
|
|
29
|
+
"generate_user_jwt_tokens",
|
|
30
|
+
"extract_user_payload",
|
|
31
|
+
"AuthlySDKError",
|
|
32
|
+
"AuthlyTokenExchangeError",
|
|
33
|
+
"AuthlyProfileFetchError",
|
|
34
|
+
"AuthlyWebhookSignatureError",
|
|
35
|
+
"calculate_signature",
|
|
36
|
+
"verify_webhook_signature",
|
|
37
|
+
]
|
authly/client.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import requests
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
from typing import Dict, Any, Optional, Tuple
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
AuthlySDKError,
|
|
8
|
+
AuthlyTokenExchangeError,
|
|
9
|
+
AuthlyProfileFetchError,
|
|
10
|
+
AuthlyWebhookSignatureError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def calculate_signature(secret: str, payload_bytes: bytes) -> str:
|
|
15
|
+
"""HMAC-SHA256 yordamida payload imzosini hisoblaydi."""
|
|
16
|
+
digest = hmac.new(
|
|
17
|
+
key=secret.encode("utf-8"),
|
|
18
|
+
msg=payload_bytes,
|
|
19
|
+
digestmod=hashlib.sha256,
|
|
20
|
+
).hexdigest()
|
|
21
|
+
return f"sha256={digest}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def verify_webhook_signature(secret: str, payload_bytes: bytes, signature: str) -> bool:
|
|
25
|
+
"""Webhook so'rovidan kelgan imzoni tekshiradi."""
|
|
26
|
+
if not secret or not signature:
|
|
27
|
+
return False
|
|
28
|
+
expected = calculate_signature(secret, payload_bytes)
|
|
29
|
+
return hmac.compare_digest(expected, signature)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthlyClient:
|
|
33
|
+
"""
|
|
34
|
+
Authly SSO (OAuth2) bilan aloqa o'rnatuvchi mijoz klassi.
|
|
35
|
+
Sof Python, FastAPI, Flask yoki Django loyihalarida to'g'ridan-to'g'ri ishlatilishi mumkin.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
backend_url: Optional[str] = None,
|
|
41
|
+
frontend_url: Optional[str] = None,
|
|
42
|
+
client_id: str = "",
|
|
43
|
+
client_secret: str = "",
|
|
44
|
+
redirect_uri: str = "",
|
|
45
|
+
timeout: int = 10,
|
|
46
|
+
webhook_secret: Optional[str] = None,
|
|
47
|
+
):
|
|
48
|
+
clean_backend = (backend_url or "").rstrip("/")
|
|
49
|
+
clean_frontend = (frontend_url or clean_backend).rstrip("/")
|
|
50
|
+
|
|
51
|
+
self.backend_url = clean_backend
|
|
52
|
+
self.frontend_url = clean_frontend
|
|
53
|
+
self.client_id = client_id
|
|
54
|
+
self.client_secret = client_secret
|
|
55
|
+
self.redirect_uri = redirect_uri
|
|
56
|
+
self.timeout = timeout
|
|
57
|
+
self.webhook_secret = webhook_secret
|
|
58
|
+
|
|
59
|
+
def get_authorization_url(
|
|
60
|
+
self,
|
|
61
|
+
scope: str = "read",
|
|
62
|
+
state: Optional[str] = None,
|
|
63
|
+
redirect_uri: Optional[str] = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Foydalanuvchini Authly SSO login sahifasiga yo'naltiruvchi havola yasaydi."""
|
|
66
|
+
target_redirect_uri = redirect_uri or self.redirect_uri
|
|
67
|
+
if not target_redirect_uri:
|
|
68
|
+
raise AuthlySDKError(
|
|
69
|
+
"'redirect_uri' ko'rsatilmadi. Metodga 'redirect_uri' uzating yoki sozlamalarda 'redirect_uri' ni belgilang."
|
|
70
|
+
)
|
|
71
|
+
params = {
|
|
72
|
+
"client_id": self.client_id,
|
|
73
|
+
"redirect_uri": target_redirect_uri,
|
|
74
|
+
"scope": scope,
|
|
75
|
+
}
|
|
76
|
+
if state:
|
|
77
|
+
params["state"] = state
|
|
78
|
+
return f"{self.frontend_url}/login?{urlencode(params)}"
|
|
79
|
+
|
|
80
|
+
def exchange_code_for_token(
|
|
81
|
+
self,
|
|
82
|
+
code: str,
|
|
83
|
+
code_verifier: Optional[str] = None,
|
|
84
|
+
redirect_uri: Optional[str] = None,
|
|
85
|
+
) -> Dict[str, Any]:
|
|
86
|
+
"""Bir martalik avtorizatsiya kodini OAuth tokenlarga almashtiradi."""
|
|
87
|
+
target_redirect_uri = redirect_uri or self.redirect_uri
|
|
88
|
+
if not target_redirect_uri:
|
|
89
|
+
raise AuthlyTokenExchangeError(
|
|
90
|
+
"'redirect_uri' ko'rsatilmadi. Metodga 'redirect_uri' uzating yoki sozlamalarda 'redirect_uri' ni belgilang."
|
|
91
|
+
)
|
|
92
|
+
token_endpoint = f"{self.backend_url}/oauth/token/"
|
|
93
|
+
payload = {
|
|
94
|
+
"grant_type": "authorization_code",
|
|
95
|
+
"code": code,
|
|
96
|
+
"redirect_uri": target_redirect_uri,
|
|
97
|
+
"client_id": self.client_id,
|
|
98
|
+
"client_secret": self.client_secret,
|
|
99
|
+
}
|
|
100
|
+
if code_verifier:
|
|
101
|
+
payload["code_verifier"] = code_verifier
|
|
102
|
+
|
|
103
|
+
headers = {
|
|
104
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
105
|
+
"User-Agent": "Authly-Python-SDK/0.4.0",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = requests.post(
|
|
110
|
+
token_endpoint, data=payload, headers=headers, timeout=self.timeout
|
|
111
|
+
)
|
|
112
|
+
if response.status_code != 200:
|
|
113
|
+
raise AuthlyTokenExchangeError(
|
|
114
|
+
message="Token olishda xatolik yuz berdi",
|
|
115
|
+
status_code=response.status_code,
|
|
116
|
+
details=response.text,
|
|
117
|
+
)
|
|
118
|
+
return response.json()
|
|
119
|
+
except requests.RequestException as e:
|
|
120
|
+
raise AuthlyTokenExchangeError(
|
|
121
|
+
f"Authly serveriga bog'lanishda xatolik: {str(e)}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_user_profile(self, access_token: str) -> Dict[str, Any]:
|
|
125
|
+
"""OAuth access_token orqali foydalanuvchi ma'lumotlarini yuklaydi."""
|
|
126
|
+
profile_endpoint = f"{self.backend_url}/api/v1/sso/profile/"
|
|
127
|
+
headers = {
|
|
128
|
+
"Authorization": f"Bearer {access_token}",
|
|
129
|
+
"User-Agent": "Authly-Python-SDK/0.4.0",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
response = requests.get(
|
|
134
|
+
profile_endpoint, headers=headers, timeout=self.timeout
|
|
135
|
+
)
|
|
136
|
+
if response.status_code != 200:
|
|
137
|
+
raise AuthlyProfileFetchError(
|
|
138
|
+
message="Profil ma'lumotlarini olish imkoni bo'lmadi",
|
|
139
|
+
status_code=response.status_code,
|
|
140
|
+
details=response.text,
|
|
141
|
+
)
|
|
142
|
+
data = response.json()
|
|
143
|
+
if isinstance(data, dict):
|
|
144
|
+
if "data" in data and isinstance(data["data"], dict):
|
|
145
|
+
return data["data"]
|
|
146
|
+
if "user" in data and isinstance(data["user"], dict):
|
|
147
|
+
return data["user"]
|
|
148
|
+
return data
|
|
149
|
+
except requests.RequestException as e:
|
|
150
|
+
raise AuthlyProfileFetchError(
|
|
151
|
+
f"Authly serveriga bog'lanishda xatolik: {str(e)}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def authenticate_code(
|
|
155
|
+
self,
|
|
156
|
+
code: str,
|
|
157
|
+
code_verifier: Optional[str] = None,
|
|
158
|
+
redirect_uri: Optional[str] = None,
|
|
159
|
+
) -> Tuple[Dict[str, Any], str]:
|
|
160
|
+
"""
|
|
161
|
+
Kodni berib, foydalanuvchi profili va Authly access_token ni qaytaruvchi metod.
|
|
162
|
+
Qaytaradi: (profile_dict, access_token_str)
|
|
163
|
+
"""
|
|
164
|
+
token_data = self.exchange_code_for_token(
|
|
165
|
+
code, code_verifier=code_verifier, redirect_uri=redirect_uri
|
|
166
|
+
)
|
|
167
|
+
access_token = token_data.get("access_token")
|
|
168
|
+
profile = self.get_user_profile(access_token)
|
|
169
|
+
return profile, access_token
|
|
170
|
+
|
|
171
|
+
def verify_webhook_signature(
|
|
172
|
+
self, payload_bytes: bytes, signature: str, secret: Optional[str] = None
|
|
173
|
+
) -> bool:
|
|
174
|
+
"""Kelgan webhook so'rovining imzosini tekshiradi."""
|
|
175
|
+
target_secret = secret or self.webhook_secret
|
|
176
|
+
return verify_webhook_signature(target_secret, payload_bytes, signature)
|
|
177
|
+
|
|
178
|
+
|
authly/exceptions.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class AuthlySDKError(Exception):
|
|
2
|
+
"""Asosiy SDK xatoligi."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthlyTokenExchangeError(AuthlySDKError):
|
|
8
|
+
"""Kodni tokenga almashtirishda xatolik yuz berganda."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message, status_code=None, details=None):
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.details = details
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthlyProfileFetchError(AuthlySDKError):
|
|
17
|
+
"""Foydalanuvchi profilini olishda xatolik yuz berganda."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message, status_code=None, details=None):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status_code = status_code
|
|
22
|
+
self.details = details
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthlyWebhookSignatureError(AuthlySDKError):
|
|
26
|
+
"""Webhook imzosini tekshirishda xatolik yuz berganda."""
|
|
27
|
+
|
|
28
|
+
pass
|
authly/services.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Tuple, List, Dict, Any, Optional
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
except ImportError:
|
|
11
|
+
settings = None
|
|
12
|
+
|
|
13
|
+
from .client import AuthlyClient
|
|
14
|
+
from .exceptions import AuthlySDKError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_authly_client() -> AuthlyClient:
|
|
20
|
+
"""AuthlyClient instansiyasini Django sozlamalari asosida qaytaradi."""
|
|
21
|
+
if settings is None:
|
|
22
|
+
raise AuthlySDKError("Django o'rnatilmagan yoki sozlanmagan.")
|
|
23
|
+
|
|
24
|
+
backend_url = getattr(settings, "AUTHLY_BACKEND_URL", "")
|
|
25
|
+
frontend_url = getattr(settings, "AUTHLY_FRONTEND_URL", None) or backend_url
|
|
26
|
+
client_id = getattr(settings, "AUTHLY_CLIENT_ID", "")
|
|
27
|
+
client_secret = getattr(settings, "AUTHLY_CLIENT_SECRET", "")
|
|
28
|
+
redirect_uri = getattr(settings, "AUTHLY_REDIRECT_URI", "")
|
|
29
|
+
timeout = getattr(settings, "AUTHLY_TIMEOUT", 10)
|
|
30
|
+
webhook_secret = getattr(settings, "AUTHLY_WEBHOOK_SECRET", None)
|
|
31
|
+
|
|
32
|
+
return AuthlyClient(
|
|
33
|
+
backend_url=backend_url,
|
|
34
|
+
frontend_url=frontend_url,
|
|
35
|
+
client_id=client_id,
|
|
36
|
+
client_secret=client_secret,
|
|
37
|
+
redirect_uri=redirect_uri,
|
|
38
|
+
timeout=timeout,
|
|
39
|
+
webhook_secret=webhook_secret,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_user_payload(data: dict) -> dict:
|
|
44
|
+
"""Authly API yoki webhook payload ichidan user ma'lumotlarini ajratib oladi."""
|
|
45
|
+
if not isinstance(data, dict):
|
|
46
|
+
return {}
|
|
47
|
+
if "data" in data and isinstance(data["data"], dict):
|
|
48
|
+
return data["data"]
|
|
49
|
+
if "user" in data and isinstance(data["user"], dict):
|
|
50
|
+
return data["user"]
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def populate_user_fields(user, user_data: dict) -> List[str]:
|
|
55
|
+
"""
|
|
56
|
+
Authly SSO dan kelgan ma'lumotlarni User obyektiga moslab to'ldiradi.
|
|
57
|
+
Lokal modelda mavjud bo'lgan barcha mos maydonlar avtomatik to'ldiriladi:
|
|
58
|
+
- authly_id
|
|
59
|
+
- first_name, last_name, email
|
|
60
|
+
- phone / phone_number
|
|
61
|
+
- avatar
|
|
62
|
+
- birth_date
|
|
63
|
+
"""
|
|
64
|
+
clean_data = extract_user_payload(user_data)
|
|
65
|
+
updated_fields = []
|
|
66
|
+
|
|
67
|
+
raw_authly_id = clean_data.get("authly_id") or clean_data.get("id")
|
|
68
|
+
authly_id = (
|
|
69
|
+
str(raw_authly_id).strip()
|
|
70
|
+
if raw_authly_id and str(raw_authly_id).strip()
|
|
71
|
+
else None
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if authly_id and hasattr(user, "authly_id") and getattr(user, "authly_id") != authly_id:
|
|
75
|
+
user.authly_id = authly_id
|
|
76
|
+
updated_fields.append("authly_id")
|
|
77
|
+
|
|
78
|
+
first_name = (clean_data.get("first_name") or "").strip()
|
|
79
|
+
if (
|
|
80
|
+
first_name
|
|
81
|
+
and hasattr(user, "first_name")
|
|
82
|
+
and getattr(user, "first_name") != first_name
|
|
83
|
+
):
|
|
84
|
+
user.first_name = first_name
|
|
85
|
+
updated_fields.append("first_name")
|
|
86
|
+
|
|
87
|
+
last_name = (clean_data.get("last_name") or "").strip()
|
|
88
|
+
if (
|
|
89
|
+
last_name
|
|
90
|
+
and hasattr(user, "last_name")
|
|
91
|
+
and getattr(user, "last_name") != last_name
|
|
92
|
+
):
|
|
93
|
+
user.last_name = last_name
|
|
94
|
+
updated_fields.append("last_name")
|
|
95
|
+
|
|
96
|
+
raw_email = clean_data.get("email")
|
|
97
|
+
email = (
|
|
98
|
+
raw_email.strip()
|
|
99
|
+
if isinstance(raw_email, str) and raw_email.strip()
|
|
100
|
+
else None
|
|
101
|
+
)
|
|
102
|
+
if email and hasattr(user, "email") and getattr(user, "email") != email:
|
|
103
|
+
user.email = email
|
|
104
|
+
updated_fields.append("email")
|
|
105
|
+
|
|
106
|
+
raw_phone = clean_data.get("phone") or clean_data.get("phone_number")
|
|
107
|
+
phone = (
|
|
108
|
+
str(raw_phone).strip() if raw_phone and str(raw_phone).strip() else None
|
|
109
|
+
)
|
|
110
|
+
if phone:
|
|
111
|
+
if hasattr(user, "phone_number") and getattr(user, "phone_number") != phone:
|
|
112
|
+
user.phone_number = phone
|
|
113
|
+
updated_fields.append("phone_number")
|
|
114
|
+
elif hasattr(user, "phone") and getattr(user, "phone") != phone:
|
|
115
|
+
user.phone = phone
|
|
116
|
+
updated_fields.append("phone")
|
|
117
|
+
|
|
118
|
+
avatar = (
|
|
119
|
+
clean_data.get("avatar")
|
|
120
|
+
or clean_data.get("image")
|
|
121
|
+
or clean_data.get("photo")
|
|
122
|
+
or ""
|
|
123
|
+
)
|
|
124
|
+
avatar_field_name = None
|
|
125
|
+
for fname in ["avatar", "image", "photo", "profile_image", "profile_photo"]:
|
|
126
|
+
if hasattr(user, fname):
|
|
127
|
+
avatar_field_name = fname
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
if avatar and avatar_field_name:
|
|
131
|
+
is_file_field = False
|
|
132
|
+
try:
|
|
133
|
+
from django.db.models import FileField
|
|
134
|
+
|
|
135
|
+
field = user._meta.get_field(avatar_field_name)
|
|
136
|
+
is_file_field = isinstance(field, FileField)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
if is_file_field:
|
|
141
|
+
if isinstance(avatar, str) and (
|
|
142
|
+
avatar.startswith("http://") or avatar.startswith("https://")
|
|
143
|
+
):
|
|
144
|
+
try:
|
|
145
|
+
res = requests.get(avatar, timeout=5)
|
|
146
|
+
if res.status_code == 200:
|
|
147
|
+
from django.core.files.base import ContentFile
|
|
148
|
+
|
|
149
|
+
path = urlparse(avatar).path
|
|
150
|
+
ext = os.path.splitext(path)[1] or ".jpg"
|
|
151
|
+
filename = f"authly_{authly_id or user.pk or 'avatar'}{ext}"
|
|
152
|
+
getattr(user, avatar_field_name).save(
|
|
153
|
+
filename, ContentFile(res.content), save=False
|
|
154
|
+
)
|
|
155
|
+
updated_fields.append(avatar_field_name)
|
|
156
|
+
except Exception as exc:
|
|
157
|
+
logger.warning("Avatar yuklab olishda xatolik yuz berdi: %s", exc)
|
|
158
|
+
else:
|
|
159
|
+
if getattr(user, avatar_field_name) != avatar:
|
|
160
|
+
setattr(user, avatar_field_name, avatar)
|
|
161
|
+
updated_fields.append(avatar_field_name)
|
|
162
|
+
|
|
163
|
+
birth_date = clean_data.get("birth_date")
|
|
164
|
+
if (
|
|
165
|
+
birth_date
|
|
166
|
+
and hasattr(user, "birth_date")
|
|
167
|
+
and getattr(user, "birth_date") != birth_date
|
|
168
|
+
):
|
|
169
|
+
user.birth_date = birth_date
|
|
170
|
+
updated_fields.append("birth_date")
|
|
171
|
+
|
|
172
|
+
return updated_fields
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def find_or_create_user(user_model, user_data: dict) -> Tuple[Any, bool]:
|
|
176
|
+
"""
|
|
177
|
+
Authly ma'lumotlari orqali foydalanuvchini topadi yoki yangi yaratadi.
|
|
178
|
+
Qidirish tartibi:
|
|
179
|
+
1. authly_id
|
|
180
|
+
2. username ("authly_{authly_id}")
|
|
181
|
+
3. phone / phone_number
|
|
182
|
+
4. email
|
|
183
|
+
"""
|
|
184
|
+
clean_data = extract_user_payload(user_data)
|
|
185
|
+
raw_authly_id = clean_data.get("authly_id") or clean_data.get("id")
|
|
186
|
+
authly_id = (
|
|
187
|
+
str(raw_authly_id).strip()
|
|
188
|
+
if raw_authly_id and str(raw_authly_id).strip()
|
|
189
|
+
else None
|
|
190
|
+
)
|
|
191
|
+
user = None
|
|
192
|
+
|
|
193
|
+
if authly_id and hasattr(user_model, "authly_id"):
|
|
194
|
+
user = user_model.objects.filter(authly_id=authly_id).first()
|
|
195
|
+
|
|
196
|
+
if not user and authly_id and hasattr(user_model, "username"):
|
|
197
|
+
user = user_model.objects.filter(username=f"authly_{authly_id}").first()
|
|
198
|
+
|
|
199
|
+
raw_phone = clean_data.get("phone") or clean_data.get("phone_number")
|
|
200
|
+
phone = (
|
|
201
|
+
str(raw_phone).strip() if raw_phone and str(raw_phone).strip() else None
|
|
202
|
+
)
|
|
203
|
+
if not user and phone:
|
|
204
|
+
if hasattr(user_model, "phone_number"):
|
|
205
|
+
user = user_model.objects.filter(phone_number=phone).first()
|
|
206
|
+
elif hasattr(user_model, "phone"):
|
|
207
|
+
user = user_model.objects.filter(phone=phone).first()
|
|
208
|
+
|
|
209
|
+
raw_email = clean_data.get("email")
|
|
210
|
+
email = (
|
|
211
|
+
raw_email.strip()
|
|
212
|
+
if isinstance(raw_email, str) and raw_email.strip()
|
|
213
|
+
else None
|
|
214
|
+
)
|
|
215
|
+
if not user and email and hasattr(user_model, "email"):
|
|
216
|
+
user = user_model.objects.filter(email=email).first()
|
|
217
|
+
|
|
218
|
+
if user:
|
|
219
|
+
created = False
|
|
220
|
+
updated = populate_user_fields(user, clean_data)
|
|
221
|
+
if updated:
|
|
222
|
+
user.save()
|
|
223
|
+
return user, created
|
|
224
|
+
|
|
225
|
+
created = True
|
|
226
|
+
user = user_model()
|
|
227
|
+
if hasattr(user, "username"):
|
|
228
|
+
user.username = (
|
|
229
|
+
f"authly_{authly_id}"
|
|
230
|
+
if authly_id
|
|
231
|
+
else (clean_data.get("username") or phone or email)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
populate_user_fields(user, clean_data)
|
|
235
|
+
if hasattr(user, "set_unusable_password"):
|
|
236
|
+
user.set_unusable_password()
|
|
237
|
+
|
|
238
|
+
user.save()
|
|
239
|
+
return user, created
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def generate_user_jwt_tokens(user) -> Dict[str, str]:
|
|
243
|
+
"""
|
|
244
|
+
Agar rest_framework_simplejwt o'rnatilgan bo'lsa,
|
|
245
|
+
user uchun access va refresh tokenlarni generatsiya qiladi.
|
|
246
|
+
"""
|
|
247
|
+
tokens = {}
|
|
248
|
+
try:
|
|
249
|
+
from rest_framework_simplejwt.tokens import RefreshToken
|
|
250
|
+
|
|
251
|
+
refresh = RefreshToken.for_user(user)
|
|
252
|
+
tokens = {
|
|
253
|
+
"refresh": str(refresh),
|
|
254
|
+
"access": str(refresh.access_token),
|
|
255
|
+
}
|
|
256
|
+
except ImportError:
|
|
257
|
+
pass
|
|
258
|
+
return tokens
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class AuthlyService:
|
|
262
|
+
"""
|
|
263
|
+
Authly SSO uchun servis klassi.
|
|
264
|
+
Biznes mantiqini bitta joyda jamlaydi.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def get_client() -> AuthlyClient:
|
|
269
|
+
return get_authly_client()
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def populate_user(user, user_data: dict) -> List[str]:
|
|
273
|
+
return populate_user_fields(user, user_data)
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def get_or_create_user(user_model, user_data: dict) -> Tuple[Any, bool]:
|
|
277
|
+
return find_or_create_user(user_model, user_data)
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def generate_tokens(user) -> Dict[str, str]:
|
|
281
|
+
return generate_user_jwt_tokens(user)
|
authly/urls.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
from .views import (
|
|
3
|
+
AuthlyAuthorizeURLAPIView,
|
|
4
|
+
AuthlyCallbackAPIView,
|
|
5
|
+
AuthlyWebhookAPIView,
|
|
6
|
+
AuthlySyncProfileAPIView,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
app_name = "authly"
|
|
10
|
+
|
|
11
|
+
urlpatterns = [
|
|
12
|
+
path("login-url/", AuthlyAuthorizeURLAPIView.as_view(), name="authly_login_url"),
|
|
13
|
+
path("callback/", AuthlyCallbackAPIView.as_view(), name="authly_callback"),
|
|
14
|
+
path("webhook/", AuthlyWebhookAPIView.as_view(), name="authly_webhook"),
|
|
15
|
+
path("sync/", AuthlySyncProfileAPIView.as_view(), name="authly_sync"),
|
|
16
|
+
]
|