Nexom 1.0.4__tar.gz → 1.0.6__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.
- {nexom-1.0.4/src/Nexom.egg-info → nexom-1.0.6}/PKG-INFO +6 -5
- {nexom-1.0.4 → nexom-1.0.6}/README.md +5 -4
- {nexom-1.0.4 → nexom-1.0.6}/pyproject.toml +1 -1
- {nexom-1.0.4 → nexom-1.0.6/src/Nexom.egg-info}/PKG-INFO +6 -5
- {nexom-1.0.4 → nexom-1.0.6}/src/Nexom.egg-info/SOURCES.txt +1 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/__init__.py +1 -1
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/__init__.py +1 -1
- nexom-1.0.6/src/nexom/app/auth.py +451 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/db.py +34 -6
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/path.py +2 -2
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/response.py +12 -3
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/template.py +21 -17
- nexom-1.0.6/src/nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom-1.0.6/src/nexom/assets/app/pages/_templates.py +7 -0
- nexom-1.0.6/src/nexom/assets/app/pages/default.py +25 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/router.py +6 -3
- nexom-1.0.6/src/nexom/assets/app/static/github.png +0 -0
- nexom-1.0.6/src/nexom/assets/app/static/style.css +686 -0
- nexom-1.0.6/src/nexom/assets/app/templates/default.html +18 -0
- nexom-1.0.6/src/nexom/assets/app/templates/document.html +115 -0
- nexom-1.0.6/src/nexom/assets/app/templates/footer.html +3 -0
- nexom-1.0.6/src/nexom/assets/app/templates/header.html +9 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom-1.0.6/src/nexom/assets/auth_page/login.html +235 -0
- nexom-1.0.6/src/nexom/assets/auth_page/signup.html +321 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/buildTools/build.py +7 -1
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/core/error.py +125 -32
- nexom-1.0.6/src/nexom/templates/auth.py +138 -0
- nexom-1.0.4/src/nexom/app/auth.py +0 -323
- nexom-1.0.4/src/nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
- nexom-1.0.4/src/nexom/assets/app/pages/_templates.py +0 -7
- nexom-1.0.4/src/nexom/assets/app/pages/default.py +0 -10
- nexom-1.0.4/src/nexom/assets/app/static/style.css +0 -39
- nexom-1.0.4/src/nexom/assets/app/templates/default.html +0 -7
- nexom-1.0.4/src/nexom/assets/app/templates/document.html +0 -169
- nexom-1.0.4/src/nexom/assets/app/templates/footer.html +0 -3
- nexom-1.0.4/src/nexom/assets/app/templates/header.html +0 -3
- nexom-1.0.4/src/nexom/assets/auth_page/login.html +0 -95
- nexom-1.0.4/src/nexom/assets/auth_page/signup.html +0 -106
- nexom-1.0.4/src/nexom/templates/auth.py +0 -72
- {nexom-1.0.4 → nexom-1.0.6}/LICENSE +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/setup.cfg +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/Nexom.egg-info/dependency_links.txt +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/Nexom.egg-info/entry_points.txt +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/Nexom.egg-info/requires.txt +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/Nexom.egg-info/top_level.txt +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/__main__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/cookie.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/http_status_codes.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/middleware.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/request.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/app/user.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/__init__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/config.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/gunicorn.conf.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/pages/__init__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/pages/document.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/static/dog.jpeg +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/templates/base.html +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/app/wsgi.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/auth/__init__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/auth/config.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/auth/gunicorn.conf.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/auth/wsgi.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/error_page/error.html +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/gateway/apache_app.conf +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/assets/gateway/nginx_app.conf +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/buildTools/__init__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/buildTools/run.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/core/__init__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/core/log.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/core/object_html_render.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/src/nexom/templates/__init__.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_buildtools.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_http_status_codes.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_middleware.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_path_routing.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_request.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_response.py +0 -0
- {nexom-1.0.4 → nexom-1.0.6}/tests/test_static.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Nexom
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Lightweight Python Web Framework (WSGI)
|
|
5
5
|
Author: TouriAida
|
|
6
6
|
License: MIT License
|
|
@@ -69,11 +69,12 @@ pip install
|
|
|
69
69
|
```
|
|
70
70
|
**プロジェクトのビルド**
|
|
71
71
|
|
|
72
|
-
プロジェクトディレクトリ上で、以下のコマンドを実行してください
|
|
72
|
+
プロジェクトディレクトリ上で、以下のコマンドを実行してください
|
|
73
|
+
|
|
73
74
|
もしNginxもしくはApacheを使用する場合 --gateway オプションにどちらか入力してください
|
|
74
75
|
|
|
75
76
|
```
|
|
76
|
-
$ python -m nexom start-project
|
|
77
|
+
$ python -m nexom start-project # --gateway nginx or apache
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
以下の構成でプロジェクトが生成されます。
|
|
@@ -117,7 +118,7 @@ $ python -m nexom run
|
|
|
117
118
|
ブラウザからアクセスできるようになります。
|
|
118
119
|
デフォルトのポートは8080です。
|
|
119
120
|
|
|
120
|
-
[
|
|
121
|
+
[http://localhost:8080](http://localhost:8080)
|
|
121
122
|
|
|
122
123
|
ポートなどの設定は `config.py` から変更してください。
|
|
123
124
|
|
|
@@ -191,4 +192,4 @@ sudo systemd start banana-project@banana2
|
|
|
191
192
|
sudo systemd start banana-project@banana3
|
|
192
193
|
```
|
|
193
194
|
|
|
194
|
-
2026 1/
|
|
195
|
+
2026 1/28
|
|
@@ -32,11 +32,12 @@ pip install
|
|
|
32
32
|
```
|
|
33
33
|
**プロジェクトのビルド**
|
|
34
34
|
|
|
35
|
-
プロジェクトディレクトリ上で、以下のコマンドを実行してください
|
|
35
|
+
プロジェクトディレクトリ上で、以下のコマンドを実行してください
|
|
36
|
+
|
|
36
37
|
もしNginxもしくはApacheを使用する場合 --gateway オプションにどちらか入力してください
|
|
37
38
|
|
|
38
39
|
```
|
|
39
|
-
$ python -m nexom start-project
|
|
40
|
+
$ python -m nexom start-project # --gateway nginx or apache
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
以下の構成でプロジェクトが生成されます。
|
|
@@ -80,7 +81,7 @@ $ python -m nexom run
|
|
|
80
81
|
ブラウザからアクセスできるようになります。
|
|
81
82
|
デフォルトのポートは8080です。
|
|
82
83
|
|
|
83
|
-
[
|
|
84
|
+
[http://localhost:8080](http://localhost:8080)
|
|
84
85
|
|
|
85
86
|
ポートなどの設定は `config.py` から変更してください。
|
|
86
87
|
|
|
@@ -154,4 +155,4 @@ sudo systemd start banana-project@banana2
|
|
|
154
155
|
sudo systemd start banana-project@banana3
|
|
155
156
|
```
|
|
156
157
|
|
|
157
|
-
2026 1/
|
|
158
|
+
2026 1/28
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Nexom
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Lightweight Python Web Framework (WSGI)
|
|
5
5
|
Author: TouriAida
|
|
6
6
|
License: MIT License
|
|
@@ -69,11 +69,12 @@ pip install
|
|
|
69
69
|
```
|
|
70
70
|
**プロジェクトのビルド**
|
|
71
71
|
|
|
72
|
-
プロジェクトディレクトリ上で、以下のコマンドを実行してください
|
|
72
|
+
プロジェクトディレクトリ上で、以下のコマンドを実行してください
|
|
73
|
+
|
|
73
74
|
もしNginxもしくはApacheを使用する場合 --gateway オプションにどちらか入力してください
|
|
74
75
|
|
|
75
76
|
```
|
|
76
|
-
$ python -m nexom start-project
|
|
77
|
+
$ python -m nexom start-project # --gateway nginx or apache
|
|
77
78
|
```
|
|
78
79
|
|
|
79
80
|
以下の構成でプロジェクトが生成されます。
|
|
@@ -117,7 +118,7 @@ $ python -m nexom run
|
|
|
117
118
|
ブラウザからアクセスできるようになります。
|
|
118
119
|
デフォルトのポートは8080です。
|
|
119
120
|
|
|
120
|
-
[
|
|
121
|
+
[http://localhost:8080](http://localhost:8080)
|
|
121
122
|
|
|
122
123
|
ポートなどの設定は `config.py` から変更してください。
|
|
123
124
|
|
|
@@ -191,4 +192,4 @@ sudo systemd start banana-project@banana2
|
|
|
191
192
|
sudo systemd start banana-project@banana3
|
|
192
193
|
```
|
|
193
194
|
|
|
194
|
-
2026 1/
|
|
195
|
+
2026 1/28
|
|
@@ -32,6 +32,7 @@ src/nexom/assets/app/pages/default.py
|
|
|
32
32
|
src/nexom/assets/app/pages/document.py
|
|
33
33
|
src/nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc
|
|
34
34
|
src/nexom/assets/app/static/dog.jpeg
|
|
35
|
+
src/nexom/assets/app/static/github.png
|
|
35
36
|
src/nexom/assets/app/static/style.css
|
|
36
37
|
src/nexom/assets/app/templates/base.html
|
|
37
38
|
src/nexom/assets/app/templates/default.html
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# src/nexom/app/auth.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
import secrets
|
|
7
|
+
import time
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from urllib.request import Request as UrlRequest, urlopen
|
|
13
|
+
from urllib.error import URLError, HTTPError
|
|
14
|
+
|
|
15
|
+
from .request import Request
|
|
16
|
+
from .response import JsonResponse
|
|
17
|
+
from .db import DatabaseManager
|
|
18
|
+
from .path import Path, Router
|
|
19
|
+
from ..core.log import AuthLogger
|
|
20
|
+
|
|
21
|
+
from ..core.error import (
|
|
22
|
+
NexomError,
|
|
23
|
+
AuthMissingFieldError, # A01
|
|
24
|
+
AuthUserIdAlreadyExistsError, # A02
|
|
25
|
+
AuthInvalidCredentialsError, # A03
|
|
26
|
+
AuthUserDisabledError, # A04
|
|
27
|
+
AuthTokenMissingError, # A05
|
|
28
|
+
AuthTokenInvalidError, # A06
|
|
29
|
+
AuthTokenExpiredError, # A07
|
|
30
|
+
AuthTokenRevokedError, # A08
|
|
31
|
+
AuthServiceUnavailableError, # A09
|
|
32
|
+
_status_for_auth_error,
|
|
33
|
+
|
|
34
|
+
DBError,
|
|
35
|
+
DBMConnectionInvalidError,
|
|
36
|
+
DBOperationalError,
|
|
37
|
+
DBIntegrityError,
|
|
38
|
+
DBProgrammingError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# --------------------
|
|
42
|
+
# utils
|
|
43
|
+
# --------------------
|
|
44
|
+
|
|
45
|
+
def _now() -> int:
|
|
46
|
+
return int(time.time())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _rand(nbytes: int = 24) -> str:
|
|
50
|
+
return secrets.token_urlsafe(nbytes)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _make_salt(nbytes: int = 16) -> str:
|
|
54
|
+
return secrets.token_hex(nbytes)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _hash_password(password: str, salt_hex: str) -> str:
|
|
58
|
+
salt = bytes.fromhex(salt_hex)
|
|
59
|
+
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
|
|
60
|
+
return dk.hex()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _token_hash(token: str) -> str:
|
|
64
|
+
return hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------
|
|
68
|
+
# variables (internal)
|
|
69
|
+
# --------------------
|
|
70
|
+
|
|
71
|
+
KEY_NAME = "_nxt"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------
|
|
75
|
+
# models (internal)
|
|
76
|
+
# --------------------
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class LocalSession:
|
|
80
|
+
sid: str
|
|
81
|
+
uid: str
|
|
82
|
+
user_id: str
|
|
83
|
+
public_name: str
|
|
84
|
+
token: str
|
|
85
|
+
expires_at: int
|
|
86
|
+
revoked_at: int | None
|
|
87
|
+
user_agent: str | None
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class Session:
|
|
91
|
+
pid: str
|
|
92
|
+
user_id: str
|
|
93
|
+
public_name: str
|
|
94
|
+
token: str
|
|
95
|
+
expires_at: int
|
|
96
|
+
user_agent: str | None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --------------------
|
|
100
|
+
# AuthService (API only)
|
|
101
|
+
# --------------------
|
|
102
|
+
|
|
103
|
+
class AuthService:
|
|
104
|
+
"""
|
|
105
|
+
Auth API service (JSON only).
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
db_path: str,
|
|
111
|
+
log_path: str,
|
|
112
|
+
*,
|
|
113
|
+
ttl_sec: int = 60 * 60 * 24 * 7,
|
|
114
|
+
prefix: str = "",
|
|
115
|
+
) -> None:
|
|
116
|
+
self.dbm = AuthDBM(db_path)
|
|
117
|
+
self.ttl_sec = ttl_sec
|
|
118
|
+
|
|
119
|
+
p = prefix.strip("/")
|
|
120
|
+
|
|
121
|
+
def _p(x: str) -> str:
|
|
122
|
+
return f"{p}/{x}".strip("/") if p else x
|
|
123
|
+
|
|
124
|
+
self.routing = Router(
|
|
125
|
+
Path(_p("signup"), self.signup, "AuthSignup"),
|
|
126
|
+
Path(_p("login"), self.login, "AuthLogin"),
|
|
127
|
+
Path(_p("logout"), self.logout, "AuthLogout"),
|
|
128
|
+
Path(_p("verify"), self.verify, "AuthVerify"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self.logger = AuthLogger(log_path)
|
|
132
|
+
|
|
133
|
+
def handler(self, environ: dict) -> JsonResponse:
|
|
134
|
+
req = Request(environ)
|
|
135
|
+
try:
|
|
136
|
+
route = self.routing.get(req.path)
|
|
137
|
+
return route.call_handler(req)
|
|
138
|
+
|
|
139
|
+
except NexomError as e:
|
|
140
|
+
# error code -> proper HTTP status
|
|
141
|
+
status = _status_for_auth_error(e.code)
|
|
142
|
+
return JsonResponse({"ok": False, "error": e.code}, status=status)
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return JsonResponse({"ok": False, "error": "InternalError"}, status=500)
|
|
146
|
+
|
|
147
|
+
# ---- handlers ----
|
|
148
|
+
|
|
149
|
+
def signup(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
150
|
+
if request.method != "POST":
|
|
151
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
152
|
+
|
|
153
|
+
data = request.json() or {}
|
|
154
|
+
user_id = str(data.get("user_id") or "").strip()
|
|
155
|
+
public_name = str(data.get("public_name") or "").strip()
|
|
156
|
+
password = str(data.get("password") or "")
|
|
157
|
+
|
|
158
|
+
self.dbm.signup(user_id=user_id, public_name=public_name, password=password)
|
|
159
|
+
return JsonResponse({"ok": True}, status=201)
|
|
160
|
+
|
|
161
|
+
def login(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
162
|
+
if request.method != "POST":
|
|
163
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
164
|
+
|
|
165
|
+
data = request.json() or {}
|
|
166
|
+
user_id = str(data.get("user_id") or "").strip()
|
|
167
|
+
password = str(data.get("password") or "")
|
|
168
|
+
|
|
169
|
+
lsess = self.dbm.login(
|
|
170
|
+
user_id,
|
|
171
|
+
password,
|
|
172
|
+
user_agent=request.headers.get("user-agent"),
|
|
173
|
+
ttl_sec=self.ttl_sec,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return JsonResponse(
|
|
177
|
+
{
|
|
178
|
+
"ok": True,
|
|
179
|
+
"pid":lsess.uid,
|
|
180
|
+
"user_id": lsess.user_id,
|
|
181
|
+
"public_name":lsess.public_name,
|
|
182
|
+
"token": lsess.token,
|
|
183
|
+
"expires_at": lsess.expires_at,
|
|
184
|
+
"user_agent": lsess.user_agent
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def logout(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
189
|
+
if request.method != "POST":
|
|
190
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
191
|
+
|
|
192
|
+
token = str((request.json() or {}).get("token") or "")
|
|
193
|
+
if token:
|
|
194
|
+
self.dbm.logout(token)
|
|
195
|
+
return JsonResponse({"ok": True})
|
|
196
|
+
|
|
197
|
+
def verify(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
|
|
198
|
+
if request.method != "POST":
|
|
199
|
+
return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
|
|
200
|
+
|
|
201
|
+
token = str((request.json() or {}).get("token") or "")
|
|
202
|
+
lsess = self.dbm.verify(token)
|
|
203
|
+
if not lsess:
|
|
204
|
+
return JsonResponse({"active": False}, status=200)
|
|
205
|
+
|
|
206
|
+
return JsonResponse(
|
|
207
|
+
{
|
|
208
|
+
"active": True,
|
|
209
|
+
"pid":lsess.uid,
|
|
210
|
+
"user_id": lsess.user_id,
|
|
211
|
+
"public_name":lsess.public_name,
|
|
212
|
+
"expires_at": lsess.expires_at,
|
|
213
|
+
"user_agent": lsess.user_agent
|
|
214
|
+
},
|
|
215
|
+
status=200,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# --------------------
|
|
220
|
+
# AuthClient (App側)
|
|
221
|
+
# --------------------
|
|
222
|
+
|
|
223
|
+
class AuthClient:
|
|
224
|
+
"""AuthService を HTTP で叩くクライアント"""
|
|
225
|
+
|
|
226
|
+
def __init__(self, auth_url: str, *, timeout: float = 3.0) -> None:
|
|
227
|
+
base = auth_url.rstrip("/")
|
|
228
|
+
self.signup_url = base + "/signup"
|
|
229
|
+
self.login_url = base + "/login"
|
|
230
|
+
self.logout_url = base + "/logout"
|
|
231
|
+
self.verify_url = base + "/verify"
|
|
232
|
+
self.timeout = timeout
|
|
233
|
+
|
|
234
|
+
def _post(self, url: str, body: dict) -> dict:
|
|
235
|
+
payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
236
|
+
req = UrlRequest(
|
|
237
|
+
url,
|
|
238
|
+
data=payload,
|
|
239
|
+
headers={
|
|
240
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
241
|
+
"Accept": "application/json",
|
|
242
|
+
},
|
|
243
|
+
method="POST",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
with urlopen(req, timeout=self.timeout) as r:
|
|
248
|
+
raw = r.read()
|
|
249
|
+
text = raw.decode("utf-8", errors="replace")
|
|
250
|
+
return json.loads(text) if text else {}
|
|
251
|
+
|
|
252
|
+
except HTTPError as e:
|
|
253
|
+
try:
|
|
254
|
+
raw = e.read()
|
|
255
|
+
text = raw.decode("utf-8", errors="replace")
|
|
256
|
+
return json.loads(text) if text else {"ok": False, "error": f"HTTP_{e.code}"}
|
|
257
|
+
except Exception:
|
|
258
|
+
return {"ok": False, "error": f"HTTP_{e.code}"}
|
|
259
|
+
|
|
260
|
+
except (URLError, TimeoutError):
|
|
261
|
+
raise AuthServiceUnavailableError()
|
|
262
|
+
|
|
263
|
+
except json.JSONDecodeError:
|
|
264
|
+
raise AuthServiceUnavailableError()
|
|
265
|
+
|
|
266
|
+
def signup(self, *, user_id: str, public_name: str, password: str) -> None:
|
|
267
|
+
d = self._post(
|
|
268
|
+
self.signup_url,
|
|
269
|
+
{"user_id": user_id, "public_name": public_name, "password": password},
|
|
270
|
+
)
|
|
271
|
+
if d.get("ok"):
|
|
272
|
+
return
|
|
273
|
+
self._raise_from_error_code(str(d.get("error") or ""))
|
|
274
|
+
|
|
275
|
+
def login(self, *, user_id: str, password: str) -> Session:
|
|
276
|
+
d = self._post(self.login_url, {"user_id": user_id, "password": password})
|
|
277
|
+
if not d.get("ok"):
|
|
278
|
+
self._raise_from_error_code(str(d.get("error") or ""))
|
|
279
|
+
|
|
280
|
+
return Session(str(d["pid"]), str(d["user_id"]), str(d["public_name"]), str(d["token"]), int(d["expires_at"]), str(d["user_agent"]))
|
|
281
|
+
|
|
282
|
+
def verify_token(self, token: str) -> Session | None:
|
|
283
|
+
d = self._post(self.verify_url, {"token": token})
|
|
284
|
+
|
|
285
|
+
if d.get("active") is True:
|
|
286
|
+
return Session(str(d["pid"]), str(d["user_id"]), str(d["public_name"]), token, int(d["expires_at"]), str(d["user_agent"]))
|
|
287
|
+
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def logout(self, *, token: str) -> None:
|
|
291
|
+
d = self._post(self.logout_url, {"token": token})
|
|
292
|
+
if d.get("ok"):
|
|
293
|
+
return
|
|
294
|
+
self._raise_from_error_code(str(d.get("error") or ""))
|
|
295
|
+
|
|
296
|
+
def _raise_from_error_code(self, code: str) -> None:
|
|
297
|
+
if code == "A01":
|
|
298
|
+
raise AuthMissingFieldError("unknown")
|
|
299
|
+
if code == "A02":
|
|
300
|
+
raise AuthUserIdAlreadyExistsError()
|
|
301
|
+
if code == "A03":
|
|
302
|
+
raise AuthInvalidCredentialsError()
|
|
303
|
+
if code == "A04":
|
|
304
|
+
raise AuthUserDisabledError()
|
|
305
|
+
if code == "A05":
|
|
306
|
+
raise AuthTokenMissingError()
|
|
307
|
+
if code == "A06":
|
|
308
|
+
raise AuthTokenInvalidError()
|
|
309
|
+
if code == "A07":
|
|
310
|
+
raise AuthTokenExpiredError()
|
|
311
|
+
if code == "A08":
|
|
312
|
+
raise AuthTokenRevokedError()
|
|
313
|
+
if code == "A09":
|
|
314
|
+
raise AuthServiceUnavailableError()
|
|
315
|
+
|
|
316
|
+
# 想定外レスポンス
|
|
317
|
+
raise AuthServiceUnavailableError()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# --------------------
|
|
321
|
+
# DB
|
|
322
|
+
# --------------------
|
|
323
|
+
|
|
324
|
+
class AuthDBM(DatabaseManager):
|
|
325
|
+
def _init(self) -> None:
|
|
326
|
+
self.execute_many(
|
|
327
|
+
[
|
|
328
|
+
(
|
|
329
|
+
"""
|
|
330
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
331
|
+
uid TEXT PRIMARY KEY,
|
|
332
|
+
user_id TEXT UNIQUE NOT NULL,
|
|
333
|
+
public_name TEXT NOT NULL,
|
|
334
|
+
password_hash TEXT NOT NULL,
|
|
335
|
+
password_salt TEXT NOT NULL,
|
|
336
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
337
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
338
|
+
);
|
|
339
|
+
""",
|
|
340
|
+
(),
|
|
341
|
+
),
|
|
342
|
+
(
|
|
343
|
+
"""
|
|
344
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
345
|
+
sid TEXT PRIMARY KEY,
|
|
346
|
+
uid TEXT NOT NULL REFERENCES users(uid),
|
|
347
|
+
token_hash TEXT UNIQUE NOT NULL,
|
|
348
|
+
expires_at INTEGER NOT NULL,
|
|
349
|
+
revoked_at INTEGER,
|
|
350
|
+
user_agent TEXT
|
|
351
|
+
);
|
|
352
|
+
""",
|
|
353
|
+
(),
|
|
354
|
+
),
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def signup(self, user_id: str, public_name: str, password: str) -> None:
|
|
359
|
+
if not user_id:
|
|
360
|
+
raise AuthMissingFieldError("user_id")
|
|
361
|
+
if not public_name:
|
|
362
|
+
raise AuthMissingFieldError("public_name")
|
|
363
|
+
if not password:
|
|
364
|
+
raise AuthMissingFieldError("password")
|
|
365
|
+
|
|
366
|
+
salt = _make_salt()
|
|
367
|
+
uid = _rand()
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
self.execute(
|
|
371
|
+
"INSERT INTO users VALUES(?,?,?,?,?,?,?)",
|
|
372
|
+
uid,
|
|
373
|
+
user_id,
|
|
374
|
+
public_name,
|
|
375
|
+
_hash_password(password, salt),
|
|
376
|
+
salt,
|
|
377
|
+
None,
|
|
378
|
+
1,
|
|
379
|
+
)
|
|
380
|
+
except DBIntegrityError:
|
|
381
|
+
raise AuthUserIdAlreadyExistsError()
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise AuthServiceUnavailableError()
|
|
384
|
+
|
|
385
|
+
def login(self, user_id: str, password: str, *, user_agent: str | None, ttl_sec: int) -> LocalSession:
|
|
386
|
+
if not user_id:
|
|
387
|
+
raise AuthMissingFieldError("user_id")
|
|
388
|
+
if not password:
|
|
389
|
+
raise AuthMissingFieldError("password")
|
|
390
|
+
|
|
391
|
+
rows = self.execute(
|
|
392
|
+
"SELECT uid, user_id, public_name, password_hash, password_salt, is_active FROM users WHERE user_id=?",
|
|
393
|
+
user_id,
|
|
394
|
+
)
|
|
395
|
+
if not rows:
|
|
396
|
+
raise AuthInvalidCredentialsError()
|
|
397
|
+
|
|
398
|
+
uid, user_id, public_name, pw_hash, salt, active = rows[0]
|
|
399
|
+
if not active:
|
|
400
|
+
raise AuthUserDisabledError()
|
|
401
|
+
|
|
402
|
+
if not hmac.compare_digest(_hash_password(password, str(salt)), str(pw_hash)):
|
|
403
|
+
raise AuthInvalidCredentialsError()
|
|
404
|
+
|
|
405
|
+
token = _rand()
|
|
406
|
+
exp = _now() + ttl_sec
|
|
407
|
+
sid = _rand()
|
|
408
|
+
|
|
409
|
+
self.execute(
|
|
410
|
+
"INSERT INTO sessions VALUES(?,?,?,?,?,?)",
|
|
411
|
+
sid,
|
|
412
|
+
uid,
|
|
413
|
+
_token_hash(token),
|
|
414
|
+
exp,
|
|
415
|
+
None,
|
|
416
|
+
user_agent,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return LocalSession(sid, uid, user_id, public_name, token, exp, None, user_agent)
|
|
420
|
+
|
|
421
|
+
def logout(self, token: str) -> None:
|
|
422
|
+
if not token:
|
|
423
|
+
raise AuthMissingFieldError("token")
|
|
424
|
+
|
|
425
|
+
self.execute(
|
|
426
|
+
"UPDATE sessions SET revoked_at=? WHERE token_hash=?",
|
|
427
|
+
_now(),
|
|
428
|
+
_token_hash(token),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
def verify(self, token: str | None) -> LocalSession | None:
|
|
432
|
+
if not token:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
rows = self.execute(
|
|
436
|
+
"""
|
|
437
|
+
SELECT s.sid, s.uid, u.user_id, u.public_name, s.expires_at, s.revoked_at, s.user_agent
|
|
438
|
+
FROM sessions s
|
|
439
|
+
JOIN users u ON u.uid=s.uid
|
|
440
|
+
WHERE s.token_hash=?
|
|
441
|
+
""",
|
|
442
|
+
_token_hash(token),
|
|
443
|
+
)
|
|
444
|
+
if not rows:
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
sid, uid, user_id, public_name, exp, rev, ua = rows[0]
|
|
448
|
+
if rev or int(exp) <= _now():
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
return LocalSession(str(sid), str(uid), str(user_id), str(public_name), str(token), int(exp), None, ua)
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from typing import Any, Iterable
|
|
4
|
-
from sqlite3 import
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
from sqlite3 import (
|
|
5
|
+
connect,
|
|
6
|
+
Connection,
|
|
7
|
+
Cursor,
|
|
8
|
+
|
|
9
|
+
Error,
|
|
10
|
+
OperationalError,
|
|
11
|
+
IntegrityError,
|
|
12
|
+
ProgrammingError
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from ..core.error import (
|
|
16
|
+
DBError,
|
|
17
|
+
|
|
18
|
+
DBMConnectionInvalidError,
|
|
19
|
+
DBOperationalError,
|
|
20
|
+
DBIntegrityError,
|
|
21
|
+
DBProgrammingError
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _call_error_handler(e: Error):
|
|
25
|
+
if isinstance(e, OperationalError):
|
|
26
|
+
raise DBOperationalError(str(e))
|
|
27
|
+
elif isinstance(e, ProgrammingError):
|
|
28
|
+
raise DBProgrammingError(str(e))
|
|
29
|
+
elif isinstance(e, IntegrityError):
|
|
30
|
+
raise DBIntegrityError(str(e))
|
|
31
|
+
else:
|
|
32
|
+
raise DBMConnectionInvalidError(str(e))
|
|
7
33
|
|
|
8
34
|
|
|
9
35
|
class DatabaseManager:
|
|
@@ -39,7 +65,7 @@ class DatabaseManager:
|
|
|
39
65
|
|
|
40
66
|
self.commit()
|
|
41
67
|
except Error as e:
|
|
42
|
-
|
|
68
|
+
_call_error_handler(e)
|
|
43
69
|
|
|
44
70
|
def rip_connection(self) -> None:
|
|
45
71
|
if self._conn is None:
|
|
@@ -70,7 +96,8 @@ class DatabaseManager:
|
|
|
70
96
|
|
|
71
97
|
except Error as e:
|
|
72
98
|
self._conn.rollback()
|
|
73
|
-
|
|
99
|
+
_call_error_handler(e)
|
|
100
|
+
|
|
74
101
|
|
|
75
102
|
def execute_many(self, sql_inserts: Iterable[ list[ tuple[str, tuple] ] ]) -> None:
|
|
76
103
|
if self._conn is None or self._cursor is None:
|
|
@@ -85,4 +112,5 @@ class DatabaseManager:
|
|
|
85
112
|
|
|
86
113
|
except Error as e:
|
|
87
114
|
self._conn.rollback()
|
|
88
|
-
|
|
115
|
+
_call_error_handler(e)
|
|
116
|
+
|
|
@@ -84,7 +84,7 @@ class Path:
|
|
|
84
84
|
return res
|
|
85
85
|
|
|
86
86
|
except TypeError as e:
|
|
87
|
-
# handler
|
|
87
|
+
# handler の引数不足
|
|
88
88
|
if re.search(r"takes \d+ positional arguments? but \d+ were given", str(e)):
|
|
89
89
|
raise PathHandlerMissingArgError()
|
|
90
90
|
raise
|
|
@@ -148,7 +148,7 @@ class Static(Path):
|
|
|
148
148
|
# Pathlib
|
|
149
149
|
# ====================
|
|
150
150
|
|
|
151
|
-
class
|
|
151
|
+
class Router(list[Path]):
|
|
152
152
|
"""Collection of Path objects with middleware support."""
|
|
153
153
|
|
|
154
154
|
def __init__(self, *paths: Path) -> None:
|