Nexom 1.0.4__tar.gz → 1.0.5__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.
Files changed (79) hide show
  1. {nexom-1.0.4/src/Nexom.egg-info → nexom-1.0.5}/PKG-INFO +2 -2
  2. {nexom-1.0.4 → nexom-1.0.5}/README.md +1 -1
  3. {nexom-1.0.4 → nexom-1.0.5}/pyproject.toml +1 -1
  4. {nexom-1.0.4 → nexom-1.0.5/src/Nexom.egg-info}/PKG-INFO +2 -2
  5. {nexom-1.0.4 → nexom-1.0.5}/src/Nexom.egg-info/SOURCES.txt +1 -0
  6. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/__init__.py +1 -1
  7. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/auth.py +184 -71
  8. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/db.py +34 -6
  9. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/path.py +1 -1
  10. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/response.py +12 -3
  11. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/template.py +21 -17
  12. nexom-1.0.5/src/nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
  13. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/pages/__pycache__/__init__.cpython-313.pyc +0 -0
  14. nexom-1.0.5/src/nexom/assets/app/pages/_templates.py +7 -0
  15. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/router.py +2 -2
  16. nexom-1.0.5/src/nexom/assets/app/static/github.png +0 -0
  17. nexom-1.0.5/src/nexom/assets/app/static/style.css +636 -0
  18. nexom-1.0.5/src/nexom/assets/app/templates/default.html +11 -0
  19. nexom-1.0.5/src/nexom/assets/app/templates/document.html +125 -0
  20. nexom-1.0.5/src/nexom/assets/app/templates/footer.html +3 -0
  21. nexom-1.0.5/src/nexom/assets/app/templates/header.html +9 -0
  22. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/auth/__pycache__/__init__.cpython-313.pyc +0 -0
  23. nexom-1.0.5/src/nexom/assets/auth_page/login.html +235 -0
  24. nexom-1.0.5/src/nexom/assets/auth_page/signup.html +321 -0
  25. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/buildTools/build.py +1 -1
  26. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/core/error.py +125 -32
  27. nexom-1.0.5/src/nexom/templates/auth.py +138 -0
  28. nexom-1.0.4/src/nexom/assets/app/__pycache__/__init__.cpython-313.pyc +0 -0
  29. nexom-1.0.4/src/nexom/assets/app/pages/_templates.py +0 -7
  30. nexom-1.0.4/src/nexom/assets/app/static/style.css +0 -39
  31. nexom-1.0.4/src/nexom/assets/app/templates/default.html +0 -7
  32. nexom-1.0.4/src/nexom/assets/app/templates/document.html +0 -169
  33. nexom-1.0.4/src/nexom/assets/app/templates/footer.html +0 -3
  34. nexom-1.0.4/src/nexom/assets/app/templates/header.html +0 -3
  35. nexom-1.0.4/src/nexom/assets/auth_page/login.html +0 -95
  36. nexom-1.0.4/src/nexom/assets/auth_page/signup.html +0 -106
  37. nexom-1.0.4/src/nexom/templates/auth.py +0 -72
  38. {nexom-1.0.4 → nexom-1.0.5}/LICENSE +0 -0
  39. {nexom-1.0.4 → nexom-1.0.5}/setup.cfg +0 -0
  40. {nexom-1.0.4 → nexom-1.0.5}/src/Nexom.egg-info/dependency_links.txt +0 -0
  41. {nexom-1.0.4 → nexom-1.0.5}/src/Nexom.egg-info/entry_points.txt +0 -0
  42. {nexom-1.0.4 → nexom-1.0.5}/src/Nexom.egg-info/requires.txt +0 -0
  43. {nexom-1.0.4 → nexom-1.0.5}/src/Nexom.egg-info/top_level.txt +0 -0
  44. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/__init__.py +0 -0
  45. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/__main__.py +0 -0
  46. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/cookie.py +0 -0
  47. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/http_status_codes.py +0 -0
  48. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/middleware.py +0 -0
  49. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/request.py +0 -0
  50. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/app/user.py +0 -0
  51. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/__init__.py +0 -0
  52. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/config.py +0 -0
  53. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/gunicorn.conf.py +0 -0
  54. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/pages/__init__.py +0 -0
  55. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/pages/default.py +0 -0
  56. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/pages/document.py +0 -0
  57. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/static/dog.jpeg +0 -0
  58. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/templates/base.html +0 -0
  59. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/app/wsgi.py +0 -0
  60. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/auth/__init__.py +0 -0
  61. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/auth/config.py +0 -0
  62. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/auth/gunicorn.conf.py +0 -0
  63. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/auth/wsgi.py +0 -0
  64. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/error_page/error.html +0 -0
  65. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/gateway/apache_app.conf +0 -0
  66. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/assets/gateway/nginx_app.conf +0 -0
  67. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/buildTools/__init__.py +0 -0
  68. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/buildTools/run.py +0 -0
  69. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/core/__init__.py +0 -0
  70. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/core/log.py +0 -0
  71. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/core/object_html_render.py +0 -0
  72. {nexom-1.0.4 → nexom-1.0.5}/src/nexom/templates/__init__.py +0 -0
  73. {nexom-1.0.4 → nexom-1.0.5}/tests/test_buildtools.py +0 -0
  74. {nexom-1.0.4 → nexom-1.0.5}/tests/test_http_status_codes.py +0 -0
  75. {nexom-1.0.4 → nexom-1.0.5}/tests/test_middleware.py +0 -0
  76. {nexom-1.0.4 → nexom-1.0.5}/tests/test_path_routing.py +0 -0
  77. {nexom-1.0.4 → nexom-1.0.5}/tests/test_request.py +0 -0
  78. {nexom-1.0.4 → nexom-1.0.5}/tests/test_response.py +0 -0
  79. {nexom-1.0.4 → nexom-1.0.5}/tests/test_static.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Nexom
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Lightweight Python Web Framework (WSGI)
5
5
  Author: TouriAida
6
6
  License: MIT License
@@ -191,4 +191,4 @@ sudo systemd start banana-project@banana2
191
191
  sudo systemd start banana-project@banana3
192
192
  ```
193
193
 
194
- 2026 1/25
194
+ 2026 1/27
@@ -154,4 +154,4 @@ sudo systemd start banana-project@banana2
154
154
  sudo systemd start banana-project@banana3
155
155
  ```
156
156
 
157
- 2026 1/25
157
+ 2026 1/27
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "Nexom"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "Lightweight Python Web Framework (WSGI)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Nexom
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Lightweight Python Web Framework (WSGI)
5
5
  Author: TouriAida
6
6
  License: MIT License
@@ -191,4 +191,4 @@ sudo systemd start banana-project@banana2
191
191
  sudo systemd start banana-project@banana3
192
192
  ```
193
193
 
194
- 2026 1/25
194
+ 2026 1/27
@@ -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
@@ -16,7 +16,7 @@ from .response import (
16
16
  )
17
17
 
18
18
  # ---- Routing ----
19
- from .path import Path, Static, Pathlib
19
+ from .path import Path, Static, Router
20
20
 
21
21
  # ---- Cookie ----
22
22
  from .cookie import Cookie, RequestCookies
@@ -8,24 +8,36 @@ import time
8
8
  import hashlib
9
9
  import hmac
10
10
  import json
11
+ import sqlite3
11
12
  from urllib.request import Request as UrlRequest, urlopen
12
13
  from urllib.error import URLError, HTTPError
13
14
 
14
15
  from .request import Request
15
16
  from .response import JsonResponse
16
17
  from .db import DatabaseManager
17
- from .path import Path, Pathlib
18
+ from .path import Path, Router
18
19
  from ..core.log import AuthLogger
19
20
 
20
21
  from ..core.error import (
21
22
  NexomError,
22
- AuthMissingFieldError,
23
- AuthInvalidCredentialsError,
24
- AuthUserDisabledError,
25
- AuthTokenInvalidError,
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,
26
39
  )
27
40
 
28
-
29
41
  # --------------------
30
42
  # utils
31
43
  # --------------------
@@ -33,20 +45,32 @@ from ..core.error import (
33
45
  def _now() -> int:
34
46
  return int(time.time())
35
47
 
48
+
36
49
  def _rand(nbytes: int = 24) -> str:
37
50
  return secrets.token_urlsafe(nbytes)
38
51
 
52
+
39
53
  def _make_salt(nbytes: int = 16) -> str:
40
54
  return secrets.token_hex(nbytes)
41
55
 
56
+
42
57
  def _hash_password(password: str, salt_hex: str) -> str:
43
58
  salt = bytes.fromhex(salt_hex)
44
59
  dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
45
60
  return dk.hex()
46
61
 
62
+
47
63
  def _token_hash(token: str) -> str:
48
64
  return hashlib.sha256(token.encode("utf-8")).hexdigest()
49
65
 
66
+
67
+ # --------------------
68
+ # variables (internal)
69
+ # --------------------
70
+
71
+ KEY_NAME = "_nxt"
72
+
73
+
50
74
  # --------------------
51
75
  # models (internal)
52
76
  # --------------------
@@ -61,6 +85,7 @@ class Session:
61
85
  revoked_at: int | None
62
86
  user_agent: str | None
63
87
 
88
+
64
89
  # --------------------
65
90
  # AuthService (API only)
66
91
  # --------------------
@@ -70,15 +95,23 @@ class AuthService:
70
95
  Auth API service (JSON only).
71
96
  """
72
97
 
73
- def __init__(self, db_path: str, log_path: str, *, ttl_sec: int = 60 * 60 * 24 * 7, prefix: str = "") -> None:
98
+ def __init__(
99
+ self,
100
+ db_path: str,
101
+ log_path: str,
102
+ *,
103
+ ttl_sec: int = 60 * 60 * 24 * 7,
104
+ prefix: str = "",
105
+ ) -> None:
74
106
  self.dbm = AuthDBM(db_path)
75
107
  self.ttl_sec = ttl_sec
76
108
 
77
109
  p = prefix.strip("/")
110
+
78
111
  def _p(x: str) -> str:
79
112
  return f"{p}/{x}".strip("/") if p else x
80
113
 
81
- self.routing = Pathlib(
114
+ self.routing = Router(
82
115
  Path(_p("signup"), self.signup, "AuthSignup"),
83
116
  Path(_p("login"), self.login, "AuthLogin"),
84
117
  Path(_p("logout"), self.logout, "AuthLogout"),
@@ -92,8 +125,12 @@ class AuthService:
92
125
  try:
93
126
  route = self.routing.get(req.path)
94
127
  return route.call_handler(req)
128
+
95
129
  except NexomError as e:
96
- return JsonResponse({"ok": False, "error": e.code}, status=400)
130
+ # error code -> proper HTTP status
131
+ status = _status_for_auth_error(e.code)
132
+ return JsonResponse({"ok": False, "error": e.code}, status=status)
133
+
97
134
  except Exception:
98
135
  return JsonResponse({"ok": False, "error": "InternalError"}, status=500)
99
136
 
@@ -101,38 +138,43 @@ class AuthService:
101
138
 
102
139
  def signup(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
103
140
  if request.method != "POST":
104
- return JsonResponse({"ok": False}, status=405)
141
+ return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
105
142
 
106
143
  data = request.json() or {}
107
- self.dbm.signup(
108
- user_id=str(data.get("user_id") or "").strip(),
109
- public_name=str(data.get("public_name") or "").strip(),
110
- password=str(data.get("password") or ""),
111
- )
144
+ user_id = str(data.get("user_id") or "").strip()
145
+ public_name = str(data.get("public_name") or "").strip()
146
+ password = str(data.get("password") or "")
147
+
148
+ self.dbm.signup(user_id=user_id, public_name=public_name, password=password)
112
149
  return JsonResponse({"ok": True}, status=201)
113
150
 
114
151
  def login(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
115
152
  if request.method != "POST":
116
- return JsonResponse({"ok": False}, status=405)
153
+ return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
117
154
 
118
155
  data = request.json() or {}
156
+ user_id = str(data.get("user_id") or "").strip()
157
+ password = str(data.get("password") or "")
158
+
119
159
  sess = self.dbm.login(
120
- str(data.get("user_id") or "").strip(),
121
- str(data.get("password") or ""),
160
+ user_id,
161
+ password,
122
162
  user_agent=request.headers.get("user-agent"),
123
163
  ttl_sec=self.ttl_sec,
124
164
  )
125
165
 
126
- return JsonResponse({
127
- "ok": True,
128
- "user_id": sess.user_id,
129
- "token": sess.token,
130
- "expires_at": sess.expires_at,
131
- })
166
+ return JsonResponse(
167
+ {
168
+ "ok": True,
169
+ "user_id": sess.user_id,
170
+ "token": sess.token,
171
+ "expires_at": sess.expires_at,
172
+ }
173
+ )
132
174
 
133
175
  def logout(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
134
176
  if request.method != "POST":
135
- return JsonResponse({"ok": False}, status=405)
177
+ return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
136
178
 
137
179
  token = str((request.json() or {}).get("token") or "")
138
180
  if token:
@@ -141,18 +183,22 @@ class AuthService:
141
183
 
142
184
  def verify(self, request: Request, args: dict[str, Optional[str]]) -> JsonResponse:
143
185
  if request.method != "POST":
144
- return JsonResponse({"ok": False}, status=405)
186
+ return JsonResponse({"ok": False, "error": "MethodNotAllowed"}, status=405)
145
187
 
146
188
  token = str((request.json() or {}).get("token") or "")
147
189
  sess = self.dbm.verify(token)
148
190
  if not sess:
149
- return JsonResponse({"active": False})
191
+ return JsonResponse({"active": False}, status=200)
192
+
193
+ return JsonResponse(
194
+ {
195
+ "active": True,
196
+ "user_id": sess.user_id,
197
+ "expires_at": sess.expires_at,
198
+ },
199
+ status=200,
200
+ )
150
201
 
151
- return JsonResponse({
152
- "active": True,
153
- "user_id": sess.user_id,
154
- "expires_at": sess.expires_at,
155
- })
156
202
 
157
203
  # --------------------
158
204
  # AuthClient (App側)
@@ -170,48 +216,97 @@ class AuthClient:
170
216
  self.timeout = timeout
171
217
 
172
218
  def _post(self, url: str, body: dict) -> dict:
173
- payload = json.dumps(body).encode("utf-8")
219
+ payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
174
220
  req = UrlRequest(
175
221
  url,
176
222
  data=payload,
177
- headers={"Content-Type": "application/json"},
223
+ headers={
224
+ "Content-Type": "application/json; charset=utf-8",
225
+ "Accept": "application/json",
226
+ },
178
227
  method="POST",
179
228
  )
229
+
180
230
  try:
181
231
  with urlopen(req, timeout=self.timeout) as r:
182
- data = json.loads(r.read().decode("utf-8"))
183
- except (HTTPError, URLError, json.JSONDecodeError) as e:
184
- print(str(e))
185
- raise AuthTokenInvalidError()
186
- return data
187
-
188
- def signup(self, *, user_id: str, public_name: str, password: str) -> bool:
189
- return bool(self._post(self.signup_url, {
190
- "user_id": user_id,
191
- "public_name": public_name,
192
- "password": password,
193
- }).get("ok"))
232
+ raw = r.read()
233
+ text = raw.decode("utf-8", errors="replace")
234
+ return json.loads(text) if text else {}
235
+
236
+ except HTTPError as e:
237
+ try:
238
+ raw = e.read()
239
+ text = raw.decode("utf-8", errors="replace")
240
+ return json.loads(text) if text else {"ok": False, "error": f"HTTP_{e.code}"}
241
+ except Exception:
242
+ return {"ok": False, "error": f"HTTP_{e.code}"}
243
+
244
+ except (URLError, TimeoutError):
245
+ raise AuthServiceUnavailableError()
246
+
247
+ except json.JSONDecodeError:
248
+ raise AuthServiceUnavailableError()
249
+
250
+ def signup(self, *, user_id: str, public_name: str, password: str) -> None:
251
+ d = self._post(
252
+ self.signup_url,
253
+ {"user_id": user_id, "public_name": public_name, "password": password},
254
+ )
255
+ if d.get("ok"):
256
+ return
257
+ self._raise_from_error_code(str(d.get("error") or ""))
194
258
 
195
259
  def login(self, *, user_id: str, password: str) -> tuple[str, str, int]:
196
260
  d = self._post(self.login_url, {"user_id": user_id, "password": password})
197
- return d["token"], d["user_id"], d["expires_at"]
261
+ if not d.get("ok"):
262
+ self._raise_from_error_code(str(d.get("error") or ""))
263
+
264
+ return str(d["token"]), str(d["user_id"]), int(d["expires_at"])
198
265
 
199
266
  def verify_token(self, *, token: str) -> tuple[bool, Optional[str], Optional[int]]:
200
267
  d = self._post(self.verify_url, {"token": token})
201
- if not d.get("active"):
202
- return False, None, None
203
- return True, d["user_id"], d["expires_at"]
204
268
 
205
- def logout(self, *, token: str) -> bool:
206
- return bool(self._post(self.logout_url, {"token": token}).get("ok"))
269
+ if d.get("active") is True:
270
+ return True, str(d["user_id"]), int(d["expires_at"])
271
+
272
+ # active False は正常系扱い
273
+ return False, None, None
274
+
275
+ def logout(self, *, token: str) -> None:
276
+ d = self._post(self.logout_url, {"token": token})
277
+ if d.get("ok"):
278
+ return
279
+ self._raise_from_error_code(str(d.get("error") or ""))
280
+
281
+ def _raise_from_error_code(self, code: str) -> None:
282
+ if code == "A01":
283
+ raise AuthMissingFieldError("unknown")
284
+ if code == "A02":
285
+ raise AuthUserIdAlreadyExistsError()
286
+ if code == "A03":
287
+ raise AuthInvalidCredentialsError()
288
+ if code == "A04":
289
+ raise AuthUserDisabledError()
290
+ if code == "A05":
291
+ raise AuthTokenMissingError()
292
+ if code == "A06":
293
+ raise AuthTokenInvalidError()
294
+ if code == "A07":
295
+ raise AuthTokenExpiredError()
296
+ if code == "A08":
297
+ raise AuthTokenRevokedError()
298
+ if code == "A09":
299
+ raise AuthServiceUnavailableError()
300
+
301
+ # 想定外レスポンス
302
+ raise AuthServiceUnavailableError()
303
+
207
304
 
208
305
  # --------------------
209
306
  # DB
210
307
  # --------------------
211
308
 
212
309
  class AuthDBM(DatabaseManager):
213
-
214
- # override
215
310
  def _init(self) -> None:
216
311
  self.execute_many(
217
312
  [
@@ -241,7 +336,7 @@ class AuthDBM(DatabaseManager):
241
336
  );
242
337
  """,
243
338
  (),
244
- )
339
+ ),
245
340
  ]
246
341
  )
247
342
 
@@ -254,18 +349,30 @@ class AuthDBM(DatabaseManager):
254
349
  raise AuthMissingFieldError("password")
255
350
 
256
351
  salt = _make_salt()
257
- self.execute(
258
- "INSERT INTO users VALUES(?,?,?,?,?,?,?)",
259
- _rand(),
260
- user_id,
261
- public_name,
262
- _hash_password(password, salt),
263
- salt,
264
- None,
265
- 1,
266
- )
352
+ uid = _rand()
353
+
354
+ try:
355
+ self.execute(
356
+ "INSERT INTO users VALUES(?,?,?,?,?,?,?)",
357
+ uid,
358
+ user_id,
359
+ public_name,
360
+ _hash_password(password, salt),
361
+ salt,
362
+ None,
363
+ 1,
364
+ )
365
+ except DBIntegrityError:
366
+ raise AuthUserIdAlreadyExistsError()
367
+ except Exception as e:
368
+ raise AuthServiceUnavailableError()
267
369
 
268
370
  def login(self, user_id: str, password: str, *, user_agent: str | None, ttl_sec: int) -> Session:
371
+ if not user_id:
372
+ raise AuthMissingFieldError("user_id")
373
+ if not password:
374
+ raise AuthMissingFieldError("password")
375
+
269
376
  rows = self.execute(
270
377
  "SELECT uid, user_id, password_hash, password_salt, is_active FROM users WHERE user_id=?",
271
378
  user_id,
@@ -276,15 +383,17 @@ class AuthDBM(DatabaseManager):
276
383
  uid, uid_text, pw_hash, salt, active = rows[0]
277
384
  if not active:
278
385
  raise AuthUserDisabledError()
279
- if not hmac.compare_digest(_hash_password(password, salt), pw_hash):
386
+
387
+ if not hmac.compare_digest(_hash_password(password, str(salt)), str(pw_hash)):
280
388
  raise AuthInvalidCredentialsError()
281
389
 
282
390
  token = _rand()
283
391
  exp = _now() + ttl_sec
392
+ sid = _rand()
284
393
 
285
394
  self.execute(
286
395
  "INSERT INTO sessions VALUES(?,?,?,?,?,?)",
287
- _rand(),
396
+ sid,
288
397
  uid,
289
398
  _token_hash(token),
290
399
  exp,
@@ -292,9 +401,12 @@ class AuthDBM(DatabaseManager):
292
401
  user_agent,
293
402
  )
294
403
 
295
- return Session("", uid, uid_text, token, exp, None, user_agent)
404
+ return Session(sid, uid, uid_text, token, exp, None, user_agent)
296
405
 
297
406
  def logout(self, token: str) -> None:
407
+ if not token:
408
+ raise AuthMissingFieldError("token")
409
+
298
410
  self.execute(
299
411
  "UPDATE sessions SET revoked_at=? WHERE token_hash=?",
300
412
  _now(),
@@ -308,7 +420,8 @@ class AuthDBM(DatabaseManager):
308
420
  rows = self.execute(
309
421
  """
310
422
  SELECT s.sid, s.uid, u.user_id, s.expires_at, s.revoked_at, s.user_agent
311
- FROM sessions s JOIN users u ON u.uid=s.uid
423
+ FROM sessions s
424
+ JOIN users u ON u.uid=s.uid
312
425
  WHERE s.token_hash=?
313
426
  """,
314
427
  _token_hash(token),
@@ -317,7 +430,7 @@ class AuthDBM(DatabaseManager):
317
430
  return None
318
431
 
319
432
  sid, uid, user_id, exp, rev, ua = rows[0]
320
- if rev or exp <= _now():
433
+ if rev or int(exp) <= _now():
321
434
  return None
322
435
 
323
- return Session(sid, uid, user_id, token, exp, None, ua)
436
+ return Session(str(sid), str(uid), str(user_id), 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 connect, Connection, Cursor, Error
5
-
6
- from ..core.error import DBMConnectionInvalidError, DBError
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
- raise DBMConnectionInvalidError(str(e))
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
- raise DBError(str(e))
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
- raise DBError(str(e))
115
+ _call_error_handler(e)
116
+
@@ -148,7 +148,7 @@ class Static(Path):
148
148
  # Pathlib
149
149
  # ====================
150
150
 
151
- class Pathlib(list[Path]):
151
+ class Router(list[Path]):
152
152
  """Collection of Path objects with middleware support."""
153
153
 
154
154
  def __init__(self, *paths: Path) -> None:
@@ -111,7 +111,7 @@ class JsonResponse(Response):
111
111
  cookie=cookie,
112
112
  content_type=content_type,
113
113
  charset=charset,
114
- include_charset=False, # ここは自前で付けたからFalse
114
+ include_charset=False,
115
115
  )
116
116
 
117
117
  class Redirect(Response):
@@ -119,11 +119,20 @@ class Redirect(Response):
119
119
  HTTP redirect response (302).
120
120
  """
121
121
 
122
- def __init__(self, location: str) -> None:
122
+ def __init__(
123
+ self,
124
+ location: str,
125
+ headers: Iterable[Header] | None = None,
126
+ cookie: str | None = None,
127
+ ) -> None:
128
+ extra = list(headers) if headers else []
129
+ res_headers = [("Location", location), *extra]
130
+
123
131
  super().__init__(
124
132
  body=b"",
125
133
  status=302,
126
- headers=[("Location", location)],
134
+ headers=res_headers,
135
+ cookie=cookie,
127
136
  )
128
137
 
129
138