nc-user-terminator 0.1.0__tar.gz
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.
Potentially problematic release.
This version of nc-user-terminator might be problematic. Click here for more details.
- nc_user_terminator-0.1.0/PKG-INFO +11 -0
- nc_user_terminator-0.1.0/README.md +0 -0
- nc_user_terminator-0.1.0/nc-user-manager/__init__.py +10 -0
- nc_user_terminator-0.1.0/nc-user-manager/client.py +180 -0
- nc_user_terminator-0.1.0/nc-user-manager/exceptions.py +8 -0
- nc_user_terminator-0.1.0/nc-user-manager/models.py +48 -0
- nc_user_terminator-0.1.0/nc-user-manager/utils.py +39 -0
- nc_user_terminator-0.1.0/nc_user_terminator.egg-info/PKG-INFO +11 -0
- nc_user_terminator-0.1.0/nc_user_terminator.egg-info/SOURCES.txt +12 -0
- nc_user_terminator-0.1.0/nc_user_terminator.egg-info/dependency_links.txt +1 -0
- nc_user_terminator-0.1.0/nc_user_terminator.egg-info/top_level.txt +1 -0
- nc_user_terminator-0.1.0/pyproject.toml +13 -0
- nc_user_terminator-0.1.0/setup.cfg +4 -0
- nc_user_terminator-0.1.0/setup.py +19 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nc-user-terminator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OAuth client wrapper for multi-tenant projects
|
|
5
|
+
Author: bw_song
|
|
6
|
+
Author-email: m132777096902@gmail.com
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Dynamic: author-email
|
|
10
|
+
Dynamic: description-content-type
|
|
11
|
+
Dynamic: requires-python
|
|
File without changes
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
from models import UserResponse, OAuth2AuthorizeResponse, CallbackResponse
|
|
5
|
+
from exceptions import OAuthError
|
|
6
|
+
from utils import request
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
TENANT_HEADER = "X-Tenant-Code"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OAuthClient:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_url: str,
|
|
16
|
+
tenant_code: str,
|
|
17
|
+
redirect_url: Optional[str] = None,
|
|
18
|
+
single_session: bool = False,
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
OAuth 客户端
|
|
22
|
+
|
|
23
|
+
:param base_url: 服务端基础地址 (例如 http://localhost:8000)
|
|
24
|
+
:param tenant_code: 租户编码
|
|
25
|
+
:param redirect_url: 可选,重定向地址
|
|
26
|
+
:param single_session: 是否单会话登录
|
|
27
|
+
"""
|
|
28
|
+
self._base_url = base_url.rstrip("/")
|
|
29
|
+
self._tenant_code = tenant_code
|
|
30
|
+
self._redirect_url = redirect_url
|
|
31
|
+
self._single_session = single_session
|
|
32
|
+
|
|
33
|
+
# ----------------------
|
|
34
|
+
# 内部异步包装
|
|
35
|
+
# ----------------------
|
|
36
|
+
async def _arequest(self, *args, **kwargs) -> dict:
|
|
37
|
+
return await asyncio.to_thread(request, *args, **kwargs)
|
|
38
|
+
|
|
39
|
+
def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
|
40
|
+
headers = {TENANT_HEADER: self._tenant_code}
|
|
41
|
+
if extra:
|
|
42
|
+
headers.update(extra)
|
|
43
|
+
return headers
|
|
44
|
+
|
|
45
|
+
# ----------------------
|
|
46
|
+
# 授权
|
|
47
|
+
# ----------------------
|
|
48
|
+
def authorize(self, platform: str) -> OAuth2AuthorizeResponse:
|
|
49
|
+
"""同步获取授权地址"""
|
|
50
|
+
params = {}
|
|
51
|
+
if self._redirect_url:
|
|
52
|
+
params["redirect_url"] = self._redirect_url
|
|
53
|
+
res_dict = request(
|
|
54
|
+
"GET",
|
|
55
|
+
f"{self._base_url}/api/oauth/{platform}/authorize",
|
|
56
|
+
params=params,
|
|
57
|
+
headers=self._headers(),
|
|
58
|
+
)
|
|
59
|
+
return OAuth2AuthorizeResponse(res_dict)
|
|
60
|
+
|
|
61
|
+
async def authorize_async(self, platform: str) -> OAuth2AuthorizeResponse:
|
|
62
|
+
"""异步获取授权地址"""
|
|
63
|
+
params = {}
|
|
64
|
+
if self._redirect_url:
|
|
65
|
+
params["redirect_url"] = self._redirect_url
|
|
66
|
+
res_dict = await self._arequest(
|
|
67
|
+
"GET",
|
|
68
|
+
f"{self._base_url}/api/oauth/{platform}/authorize",
|
|
69
|
+
params=params,
|
|
70
|
+
headers=self._headers(),
|
|
71
|
+
)
|
|
72
|
+
return OAuth2AuthorizeResponse(res_dict)
|
|
73
|
+
|
|
74
|
+
# ----------------------
|
|
75
|
+
# 普通回调
|
|
76
|
+
# ----------------------
|
|
77
|
+
def callback(self, platform: str, query_params: Dict[str, Any]) -> CallbackResponse:
|
|
78
|
+
"""同步处理 OAuth2 回调 (非 One Tap)"""
|
|
79
|
+
if query_params.get("credential"):
|
|
80
|
+
raise OAuthError("Use google_one_tap() for One Tap login")
|
|
81
|
+
|
|
82
|
+
params = {"single_session": str(self._single_session).lower()}
|
|
83
|
+
if self._redirect_url:
|
|
84
|
+
params["redirect_url"] = self._redirect_url
|
|
85
|
+
params.update(query_params or {})
|
|
86
|
+
res_dict = request(
|
|
87
|
+
"GET",
|
|
88
|
+
f"{self._base_url}/api/oauth/{platform}/callback",
|
|
89
|
+
params=params,
|
|
90
|
+
headers=self._headers(),
|
|
91
|
+
)
|
|
92
|
+
return CallbackResponse(res_dict)
|
|
93
|
+
|
|
94
|
+
async def callback_async(self, platform: str, query_params: Dict[str, Any]) -> CallbackResponse:
|
|
95
|
+
"""异步处理 OAuth2 回调 (非 One Tap)"""
|
|
96
|
+
if query_params.get("credential"):
|
|
97
|
+
raise OAuthError("Use google_one_tap_async() for One Tap login")
|
|
98
|
+
|
|
99
|
+
params = {"single_session": str(self._single_session).lower()}
|
|
100
|
+
if self._redirect_url:
|
|
101
|
+
params["redirect_url"] = self._redirect_url
|
|
102
|
+
params.update(query_params or {})
|
|
103
|
+
res_dict = await self._arequest(
|
|
104
|
+
"GET",
|
|
105
|
+
f"{self._base_url}/api/oauth/{platform}/callback",
|
|
106
|
+
params=params,
|
|
107
|
+
headers=self._headers(),
|
|
108
|
+
)
|
|
109
|
+
return CallbackResponse(res_dict)
|
|
110
|
+
|
|
111
|
+
# ----------------------
|
|
112
|
+
# Google One Tap
|
|
113
|
+
# ----------------------
|
|
114
|
+
def google_one_tap(self, credential: str) -> CallbackResponse:
|
|
115
|
+
"""同步 Google One Tap 登录"""
|
|
116
|
+
params = {"credential": credential}
|
|
117
|
+
if self._redirect_url:
|
|
118
|
+
params["redirect_url"] = self._redirect_url
|
|
119
|
+
res_dict = request(
|
|
120
|
+
"GET",
|
|
121
|
+
f"{self._base_url}/api/oauth/google/callback",
|
|
122
|
+
params=params,
|
|
123
|
+
headers=self._headers(),
|
|
124
|
+
)
|
|
125
|
+
return CallbackResponse(res_dict)
|
|
126
|
+
|
|
127
|
+
async def google_one_tap_async(self, credential: str) -> CallbackResponse:
|
|
128
|
+
"""异步 Google One Tap 登录"""
|
|
129
|
+
params = {"credential": credential}
|
|
130
|
+
if self._redirect_url:
|
|
131
|
+
params["redirect_url"] = self._redirect_url
|
|
132
|
+
res_dict = await self._arequest(
|
|
133
|
+
"GET",
|
|
134
|
+
f"{self._base_url}/api/oauth/google/callback",
|
|
135
|
+
params=params,
|
|
136
|
+
headers=self._headers(),
|
|
137
|
+
)
|
|
138
|
+
return CallbackResponse(res_dict)
|
|
139
|
+
|
|
140
|
+
# ----------------------
|
|
141
|
+
# 获取用户信息
|
|
142
|
+
# ----------------------
|
|
143
|
+
def say_my_name(self, token: str) -> UserResponse:
|
|
144
|
+
"""同步获取当前用户"""
|
|
145
|
+
headers = self._headers({"Authorization": f"Bearer {token}"})
|
|
146
|
+
res_dict = request("GET", f"{self._base_url}/api/me", headers=headers)
|
|
147
|
+
return UserResponse(res_dict)
|
|
148
|
+
|
|
149
|
+
async def say_my_name_async(self, token: str) -> UserResponse:
|
|
150
|
+
"""异步获取当前用户"""
|
|
151
|
+
headers = self._headers({"Authorization": f"Bearer {token}"})
|
|
152
|
+
res_dict = await self._arequest("GET", f"{self._base_url}/api/me", headers=headers)
|
|
153
|
+
return UserResponse(res_dict)
|
|
154
|
+
|
|
155
|
+
# 刷新过期时间
|
|
156
|
+
def reborn(self, token: str, extend_seconds: Optional[int] = None) -> dict:
|
|
157
|
+
"""重新设置过期时间"""
|
|
158
|
+
headers = self._headers({"Authorization": f"Bearer {token}"})
|
|
159
|
+
if extend_seconds:
|
|
160
|
+
return request("POST", f"{self._base_url}/api/refresh-me", headers=headers, json_body={"extend_seconds": extend_seconds})
|
|
161
|
+
else:
|
|
162
|
+
return request("POST", f"{self._base_url}/api/refresh-me", headers=headers)
|
|
163
|
+
|
|
164
|
+
async def reborn_async(self, token: str, extend_seconds: Optional[int] = None) -> dict:
|
|
165
|
+
"""异步重新设置过期时间"""
|
|
166
|
+
headers = self._headers({"Authorization": f"Bearer {token}"})
|
|
167
|
+
if extend_seconds:
|
|
168
|
+
return await self._arequest("POST", f"{self._base_url}/api/refresh-me", headers=headers, json_body={"extend_seconds": extend_seconds})
|
|
169
|
+
else:
|
|
170
|
+
return await self._arequest("POST", f"{self._base_url}/api/refresh-me", headers=headers)
|
|
171
|
+
|
|
172
|
+
# 登出
|
|
173
|
+
def logout(self, token: str):
|
|
174
|
+
headers = self._headers({"Authorization": f"Bearer {token}"})
|
|
175
|
+
return request("POST", f"{self._base_url}/api/auth/logout", headers=headers)
|
|
176
|
+
|
|
177
|
+
async def logout_async(self, token: str) -> dict:
|
|
178
|
+
"""异步获取当前用户"""
|
|
179
|
+
headers = self._headers({"Authorization": f"Bearer {token}"})
|
|
180
|
+
return await self._arequest("POST", f"{self._base_url}/api/auth/logout", headers=headers)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class OAuthError(Exception):
|
|
2
|
+
"""统一封装 OAuth 调用错误"""
|
|
3
|
+
|
|
4
|
+
def __init__(self, message: str, status_code: int = None, response: str = None):
|
|
5
|
+
self.message = message
|
|
6
|
+
self.status_code = status_code
|
|
7
|
+
self.response = response
|
|
8
|
+
super().__init__(f"[{status_code}] {message}" if status_code else message)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
class OAuth2AuthorizeResponse(dict):
|
|
4
|
+
@property
|
|
5
|
+
def url(self) -> str:
|
|
6
|
+
return self.get("authorization_url")
|
|
7
|
+
|
|
8
|
+
class UserResponse(dict):
|
|
9
|
+
@property
|
|
10
|
+
def id(self) -> str:
|
|
11
|
+
return self.get("id")
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def email(self) -> str:
|
|
15
|
+
return self.get("email")
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def picture(self) -> Optional[str]:
|
|
19
|
+
return self.get("picture")
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def full_name(self) -> Optional[str]:
|
|
23
|
+
return self.get("full_name")
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def role(self) -> Optional[str]:
|
|
27
|
+
return self.get("role")
|
|
28
|
+
|
|
29
|
+
class CallbackResponse(dict):
|
|
30
|
+
@property
|
|
31
|
+
def token(self) -> str:
|
|
32
|
+
return self.get("access_token")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def token_type(self) -> str:
|
|
36
|
+
return self.get("token_type")
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def user_name(self) -> str:
|
|
40
|
+
return self.get("user_name")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def email(self) -> str:
|
|
44
|
+
return self.get("email")
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def user_picture(self) -> str:
|
|
48
|
+
return self.get("user_picture")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import urllib.parse
|
|
3
|
+
import urllib.request
|
|
4
|
+
import urllib.error
|
|
5
|
+
|
|
6
|
+
from exceptions import OAuthError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def request(method: str, url: str, params=None, headers=None, json_body=None) -> dict:
|
|
10
|
+
# 拼接 GET 参数
|
|
11
|
+
if params:
|
|
12
|
+
query = urllib.parse.urlencode(params)
|
|
13
|
+
url = f"{url}?{query}"
|
|
14
|
+
|
|
15
|
+
data = None
|
|
16
|
+
if json_body is not None:
|
|
17
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
18
|
+
if headers is None:
|
|
19
|
+
headers = {}
|
|
20
|
+
headers["Content-Type"] = "application/json"
|
|
21
|
+
|
|
22
|
+
req = urllib.request.Request(url, method=method.upper(), data=data)
|
|
23
|
+
|
|
24
|
+
if headers:
|
|
25
|
+
for k, v in headers.items():
|
|
26
|
+
req.add_header(k, v)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
with urllib.request.urlopen(req) as resp:
|
|
30
|
+
body = resp.read().decode()
|
|
31
|
+
try:
|
|
32
|
+
return json.loads(body)
|
|
33
|
+
except json.JSONDecodeError:
|
|
34
|
+
raise OAuthError("Invalid JSON response", resp.getcode(), body)
|
|
35
|
+
except urllib.error.HTTPError as e:
|
|
36
|
+
body = e.read().decode() if e.fp else None
|
|
37
|
+
raise OAuthError("HTTP request failed", e.code, body)
|
|
38
|
+
except urllib.error.URLError as e:
|
|
39
|
+
raise OAuthError(f"Network error: {e.reason}")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nc-user-terminator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OAuth client wrapper for multi-tenant projects
|
|
5
|
+
Author: bw_song
|
|
6
|
+
Author-email: m132777096902@gmail.com
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Dynamic: author-email
|
|
10
|
+
Dynamic: description-content-type
|
|
11
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
nc-user-manager/__init__.py
|
|
5
|
+
nc-user-manager/client.py
|
|
6
|
+
nc-user-manager/exceptions.py
|
|
7
|
+
nc-user-manager/models.py
|
|
8
|
+
nc-user-manager/utils.py
|
|
9
|
+
nc_user_terminator.egg-info/PKG-INFO
|
|
10
|
+
nc_user_terminator.egg-info/SOURCES.txt
|
|
11
|
+
nc_user_terminator.egg-info/dependency_links.txt
|
|
12
|
+
nc_user_terminator.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nc-user-manager
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nc-user-terminator"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "OAuth client wrapper for multi-tenant projects"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "bw_song" }
|
|
11
|
+
]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = []
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="nc-user-terminator",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=[
|
|
8
|
+
],
|
|
9
|
+
author="bw_song",
|
|
10
|
+
author_email="m132777096902@gmail.com",
|
|
11
|
+
description="OAuth client wrapper for multi-tenant projects",
|
|
12
|
+
long_description=open("README.md", encoding="utf-8").read(),
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
classifiers=[
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
],
|
|
18
|
+
python_requires=">=3.8",
|
|
19
|
+
)
|