Nexom 1.0.4__py3-none-any.whl → 1.0.5__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.
@@ -5,64 +5,260 @@
5
5
  <meta charset="utf-8" />
6
6
  <meta name="viewport" content="width=device-width,initial-scale=1" />
7
7
  <title>Nexom Signup</title>
8
+
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+JP:wght@400;600;700&display=swap" rel="stylesheet">
12
+
8
13
  <style>
9
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
10
- .wrap { max-width: 720px; margin: 0 auto; }
11
- .card { border: 1px solid #eee; border-radius: 14px; padding: 16px; background: #fff; }
12
- label { display:block; font-size:12px; margin: 12px 0 6px; color:#333; }
13
- input { width:100%; padding:10px 12px; border:1px solid #ddd; border-radius:10px; font-size:14px; }
14
- button { margin-top: 14px; padding:10px 12px; border:1px solid #333; border-radius:10px; background:#333; color:#fff; cursor:pointer; }
15
- .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top: 10px; }
16
- a { color:#333; }
17
- .help { color:#666; font-size:12px; }
18
- .log { margin-top: 14px; padding: 12px; border-radius: 12px; background:#f7f7f7; border:1px solid #eee; white-space: pre-wrap;
19
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
20
- .ok { color:#0a7; }
21
- .ng { color:#c33; }
14
+ :root{
15
+ --bg:#0b0d12;
16
+ --card:#0f141c;
17
+ --line:rgba(255,255,255,.10);
18
+ --text:rgba(255,255,255,.92);
19
+ --muted:rgba(255,255,255,.62);
20
+ --danger:#ff5a6a;
21
+ --ok:#27d39b;
22
+ --btn:#ffffff;
23
+ --btnText:#0b0d12;
24
+ --shadow:0 20px 60px rgba(0,0,0,.45);
25
+ }
26
+ *{ box-sizing:border-box; }
27
+ html,body{ height:100%; }
28
+ body{
29
+ margin:0;
30
+ background: radial-gradient(1200px 600px at 20% 10%, rgba(106,140,255,.18), transparent 60%),
31
+ radial-gradient(900px 500px at 85% 30%, rgba(39,211,155,.12), transparent 55%),
32
+ var(--bg);
33
+ color:var(--text);
34
+ font-family: Inter, "Noto Sans JP", "Hiragino Sans", "ヒラギノ角ゴシック", "Yu Gothic", "游ゴシック", system-ui, -apple-system, "Segoe UI", sans-serif;
35
+ overflow:hidden; /* スクロール禁止 */
36
+ }
37
+ .wrap{
38
+ height:100%;
39
+ display:grid;
40
+ place-items:center;
41
+ padding:24px;
42
+ }
43
+ .card{
44
+ width:min(560px, 100%);
45
+ background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
46
+ border:1px solid var(--line);
47
+ border-radius:18px;
48
+ box-shadow: var(--shadow);
49
+ padding:18px;
50
+ backdrop-filter: blur(10px);
51
+ }
52
+ h1{
53
+ margin:0 0 10px;
54
+ font-size:18px;
55
+ letter-spacing:.2px;
56
+ }
57
+ .sub{
58
+ margin:0 0 14px;
59
+ color:var(--muted);
60
+ font-size:12px;
61
+ line-height:1.5;
62
+ }
63
+
64
+ .grid{
65
+ display:grid;
66
+ grid-template-columns: 1fr 1fr;
67
+ gap:10px;
68
+ }
69
+ .grid .full{ grid-column:1 / -1; }
70
+ @media (max-width: 480px){
71
+ .grid .hurf{ grid-column:1 / -1; }
72
+ }
73
+
74
+ label{
75
+ display:block;
76
+ font-size:11px;
77
+ color:var(--muted);
78
+ margin:0 0 6px;
79
+ }
80
+ input{
81
+ width:100%;
82
+ height:40px;
83
+ padding:0 12px;
84
+ border-radius:12px;
85
+ border:1px solid var(--line);
86
+ background: rgba(0,0,0,.25);
87
+ color:var(--text);
88
+ outline:none;
89
+ font-size:13px;
90
+ }
91
+ input:focus{
92
+ border-color: rgba(255,255,255,.18);
93
+ box-shadow: 0 0 0 3px rgba(106,140,255,.12);
94
+ }
95
+
96
+ .row{
97
+ display:flex;
98
+ gap:10px;
99
+ align-items:center;
100
+ justify-content:space-between;
101
+ margin-top:12px;
102
+ }
103
+
104
+ button{
105
+ height:40px;
106
+ padding:0 14px;
107
+ border-radius:12px;
108
+ border:1px solid rgba(255,255,255,.08);
109
+ background: var(--btn);
110
+ color: var(--btnText);
111
+ font-weight:700;
112
+ cursor:pointer;
113
+ white-space:nowrap;
114
+ }
115
+ button:disabled{
116
+ opacity:.55;
117
+ cursor:not-allowed;
118
+ }
119
+ .link{
120
+ color:var(--muted);
121
+ font-size:12px;
122
+ text-decoration:none;
123
+ border-bottom:1px dotted rgba(255,255,255,.25);
124
+ }
125
+ .link:hover{ color:var(--text); }
126
+
127
+ .msg{
128
+ display:none;
129
+ margin-top:10px;
130
+ padding:10px 12px;
131
+ border-radius:12px;
132
+ border:1px solid var(--line);
133
+ font-size:12px;
134
+ line-height:1.5;
135
+ white-space:pre-wrap;
136
+ background: rgba(0,0,0,.22);
137
+ }
138
+ .msg.is-error{ display:block; border-color: rgba(255,90,106,.35); }
139
+ .msg.is-ok{ display:block; border-color: rgba(39,211,155,.35); }
140
+
141
+ .okPanel{
142
+ display:none;
143
+ margin-top:10px;
144
+ padding:12px;
145
+ border-radius:14px;
146
+ border:1px solid rgba(39,211,155,.35);
147
+ background: rgba(39,211,155,.10);
148
+ }
149
+ .okPanel.show{ display:block; }
150
+ .okTitle{
151
+ font-weight:700;
152
+ font-size:13px;
153
+ margin:0 0 6px;
154
+ }
155
+ .okText{
156
+ margin:0 0 10px;
157
+ font-size:12px;
158
+ color:var(--muted);
159
+ line-height:1.5;
160
+ }
161
+ .okBtn{
162
+ width:100%;
163
+ height:40px;
164
+ border-radius:12px;
165
+ border:1px solid rgba(255,255,255,.08);
166
+ background: rgba(255,255,255,.92);
167
+ color:#0b0d12;
168
+ font-weight:800;
169
+ cursor:pointer;
170
+ }
22
171
  </style>
23
172
  </head>
173
+
24
174
  <body>
25
175
  <div class="wrap">
26
- <h1 style="margin:0 0 12px;font-size:20px;">Signup</h1>
27
-
28
176
  <section class="card">
29
- <form id="formSignup">
30
- <label for="user_id">user_id</label>
31
- <input id="user_id" name="user_id" autocomplete="username" required />
177
+ <h1>Signup</h1>
178
+ <p class="sub">アカウントを作成</p>
32
179
 
33
- <label for="public_name">public_name</label>
34
- <input id="public_name" name="public_name" autocomplete="nickname" required />
180
+ <form id="form">
181
+ <div class="grid">
182
+ <div class="full">
183
+ <label for="user_id">ユーザーID</label>
184
+ <input id="user_id" name="user_id" autocomplete="username" required />
185
+ </div>
35
186
 
36
- <label for="password">password</label>
37
- <input id="password" name="password" type="password" autocomplete="new-password" required />
187
+ <div class="full">
188
+ <label for="public_name">公開名</label>
189
+ <input id="public_name" name="public_name" autocomplete="nickname" required />
190
+ </div>
38
191
 
39
- <label for="password2">password (confirm)</label>
40
- <input id="password2" name="password2" type="password" autocomplete="new-password" required />
41
-
42
- <button type="submit">signup</button>
192
+ <div class="hurf">
193
+ <label for="password">パスワード</label>
194
+ <input id="password" name="password" type="password" autocomplete="new-password" required />
195
+ </div>
196
+ <div class="hurf">
197
+ <label for="password2">パスワード (確認)</label>
198
+ <input id="password2" name="password2" type="password" autocomplete="new-password" required />
199
+ </div>
200
+ </div>
43
201
 
44
202
  <div class="row">
45
- <span class="help">送信先はこのアプリの <code>/signup/</code>(アプリ → AuthClient → AuthService)</span>
46
- <a class="help" href="/login/">loginへ</a>
203
+ <a class="link" href="./login">アカウントがある場合</a>
204
+ <button id="btn" type="submit">作成</button>
47
205
  </div>
48
- </form>
49
206
 
50
- <div id="log" class="log">ready</div>
207
+ <div id="msg" class="msg"></div>
208
+
209
+ <div id="okPanel" class="okPanel">
210
+ <div class="okTitle">アカウントが作成されました</div>
211
+ <p class="okText">ログインしてください</p>
212
+ <button id="toLogin" class="okBtn" type="button">ログイン画面へ</button>
213
+ </div>
214
+ </form>
51
215
  </section>
52
216
  </div>
53
217
 
54
218
  <script>
55
- const logEl = document.getElementById("log");
219
+ const AUTH_BASE = ""
220
+ const PAGE_PATH = "/{{ page_path }}"
56
221
 
57
- function pretty(v) {
58
- try { return JSON.stringify(v, null, 2); } catch { return String(v); }
222
+ const error_code_for_message = {
223
+ // ===== Auth errors =====
224
+ "A01": "フォームの入力内容に不足があります。",
225
+ "A02": "このユーザーIDはすでに使用されています。",
226
+ "A03": "ユーザーIDまたはパスワードが正しくありません。",
227
+ "A04": "このユーザーは無効化されています。",
228
+ "A05": "認証情報が見つかりません。再度ログインしてください。",
229
+ "A06": "認証情報が不正です。再度ログインしてください。",
230
+ "A07": "セッションの有効期限が切れています。再度ログインしてください。",
231
+ "A08": "このセッションは無効化されています。再度ログインしてください。",
232
+ "A09": "認証サービスに接続できません。しばらく時間をおいて再度お試しください。",
233
+
234
+ // fallback
235
+ "DEFAULT": "エラーが発生しました。もう一度お試しください。"
236
+ };
237
+ function getErrorMessage(code) {
238
+ if (typeof code !== "string") {
239
+ return error_code_for_message.DEFAULT;
240
+ }
241
+
242
+ return error_code_for_message[code] ?? error_code_for_message.DEFAULT;
243
+ }
244
+
245
+ const form = document.getElementById("form");
246
+ const btn = document.getElementById("btn");
247
+ const msg = document.getElementById("msg");
248
+ const okPanel = document.getElementById("okPanel");
249
+ const toLogin = document.getElementById("toLogin");
250
+
251
+ function clearMsg(){
252
+ msg.className = "msg";
253
+ msg.textContent = "";
59
254
  }
60
- function writeLog(ok, title, data) {
61
- const cls = ok ? "ok" : "ng";
62
- logEl.innerHTML = `<span class="${cls}">${title}</span>\n${pretty(data)}`;
255
+ function setError(text){
256
+ msg.className = "msg is-error";
257
+ msg.textContent = getErrorMessage(text);
63
258
  }
64
259
 
65
- async function postJSON(url, bodyObj) {
260
+ async function postJSON(path, bodyObj){
261
+ const url = (AUTH_BASE || "") + path;
66
262
  const res = await fetch(url, {
67
263
  method: "POST",
68
264
  headers: {
@@ -75,12 +271,13 @@
75
271
  const text = await res.text();
76
272
  let data = {};
77
273
  try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
78
-
79
274
  return { ok: res.ok, status: res.status, data };
80
275
  }
81
276
 
82
- document.getElementById("formSignup").addEventListener("submit", async (e) => {
277
+ form.addEventListener("submit", async (e) => {
83
278
  e.preventDefault();
279
+ clearMsg();
280
+ okPanel.classList.remove("show");
84
281
 
85
282
  const user_id = document.getElementById("user_id").value.trim();
86
283
  const public_name = document.getElementById("public_name").value.trim();
@@ -88,18 +285,36 @@
88
285
  const password2 = document.getElementById("password2").value;
89
286
 
90
287
  if (!user_id || !public_name || !password) {
91
- writeLog(false, "signup: missing fields", { user_id, public_name });
288
+ setError("T: missing fields");
92
289
  return;
93
290
  }
94
291
  if (password !== password2) {
95
- writeLog(false, "signup: password mismatch", {});
292
+ setError("T: password mismatch");
96
293
  return;
97
294
  }
98
295
 
99
- const r = await postJSON("/signup/", { user_id, public_name, password });
100
- writeLog(r.ok, `signup: ${r.status}`, r.data);
296
+ btn.disabled = true;
297
+ try{
298
+ const r = await postJSON(PAGE_PATH, { user_id, public_name, password });
299
+
300
+ if (!r.ok || !r.data || r.data.ok !== true) {
301
+
302
+ const code = (r.data && r.data.error) ? String(r.data.error) : "UnknownError";
303
+ setError(code);
304
+ return;
305
+ }
306
+
307
+ okPanel.classList.add("show");
308
+ }catch(e){
309
+ console.error(e)
310
+ setError("NetworkError");
311
+ }finally{
312
+ btn.disabled = false;
313
+ }
314
+ });
101
315
 
102
- // if (r.ok) location.href = "/login/";
316
+ toLogin.addEventListener("click", () => {
317
+ location.href = "./login";
103
318
  });
104
319
  </script>
105
320
  </body>
nexom/buildTools/build.py CHANGED
@@ -125,7 +125,7 @@ def create_app(
125
125
 
126
126
  # static
127
127
  static_pkg = "nexom.assets.app.static"
128
- for fn in ("dog.jpeg", "style.css"):
128
+ for fn in ("dog.jpeg", "github.png", "style.css"):
129
129
  _copy_from_package(static_pkg, fn, static_dir / fn)
130
130
 
131
131
  # app files
nexom/core/error.py CHANGED
@@ -160,68 +160,161 @@ class ObjectHTMLTypeError(NexomError):
160
160
  # =========================
161
161
  # DatabaseManager
162
162
  # =========================
163
- class DBMConnectionInvalidError(NexomError):
164
- """Raised when an udbm connection is invalid."""
163
+ class DBError(NexomError):
164
+ """
165
+ Base class for database-related errors.
166
+
167
+ This error represents a generic database failure that does not fall into
168
+ a more specific category such as connection, integrity, operational, or
169
+ programming errors.
170
+
171
+ It is typically used as a catch-all or wrapper error when the underlying
172
+ database exception cannot be safely or clearly classified.
173
+ """
174
+
175
+ class DBMConnectionInvalidError(DBError):
176
+ """
177
+ Raised when the database manager connection is invalid or not initialized.
165
178
 
166
- def __init__(self, message="Not started") -> None:
179
+ This error indicates that the database manager (DBM) is in an unusable state,
180
+ such as:
181
+ - the database connection has not been established yet
182
+ - the connection was already closed
183
+ - the DBM was accessed before proper initialization
184
+
185
+ This is typically a lifecycle or configuration error.
186
+ """
187
+ def __init__(self, message: str = "Not started") -> None:
167
188
  super().__init__(
168
189
  "DBM01",
169
190
  f"DBM connection is invalid. -> {message}",
170
191
  )
171
192
 
172
- class DBError(NexomError):
173
- """Raised when an udbm connection is invalid."""
174
193
 
175
- def __init__(self, message) -> None:
194
+ class DBOperationalError(DBError):
195
+ """
196
+ Raised when a database operational error occurs.
197
+
198
+ This error represents failures related to the database runtime environment,
199
+ such as:
200
+ - inability to open or connect to the database file
201
+ - database being locked
202
+ - I/O errors during a query
203
+ - transaction failures caused by the database state
204
+
205
+ Typically maps to sqlite3.OperationalError.
206
+ """
207
+ def __init__(self, message: str) -> None:
176
208
  super().__init__(
177
209
  "DBM02",
178
- f"DBM connection is invalid. -> {message}",
210
+ f"Database operational error. -> {message}",
179
211
  )
180
212
 
213
+
214
+ class DBIntegrityError(DBError):
215
+ """
216
+ Raised when a database integrity constraint is violated.
217
+
218
+ This error indicates that a database constraint has been broken, such as:
219
+ - UNIQUE constraint violations
220
+ - FOREIGN KEY constraint failures
221
+ - NOT NULL constraint violations
222
+ - CHECK constraint failures
223
+
224
+ Typically maps to sqlite3.IntegrityError.
225
+ """
226
+ def __init__(self, message: str) -> None:
227
+ super().__init__(
228
+ "DBM03",
229
+ f"Database integrity constraint violated. -> {message}",
230
+ )
231
+
232
+
233
+ class DBProgrammingError(DBError):
234
+ """
235
+ Raised when a database programming or SQL syntax error occurs.
236
+
237
+ This error indicates a bug in application code or query construction, such as:
238
+ - malformed SQL statements
239
+ - referencing non-existent tables or columns
240
+ - incorrect parameter binding
241
+ - misuse of the database API
242
+
243
+ Typically maps to sqlite3.ProgrammingError.
244
+ """
245
+ def __init__(self, message: str) -> None:
246
+ super().__init__(
247
+ "DBM04",
248
+ f"Database programming error. -> {message}",
249
+ )
250
+
251
+
181
252
  # =========================
182
253
  # Auth
183
254
  # =========================
184
255
 
185
256
  class AuthMissingFieldError(NexomError):
186
- """Raised when required auth fields are missing."""
257
+ """Required auth fields are missing."""
187
258
  def __init__(self, key: str) -> None:
188
- super().__init__(
189
- "A01",
190
- f"Missing field. '{key}'"
191
- )
259
+ super().__init__("A01", f"Missing field. '{key}'")
260
+
261
+
262
+ class AuthUserIdAlreadyExistsError(NexomError):
263
+ """user_id already exists (signup conflict)."""
264
+ def __init__(self) -> None:
265
+ super().__init__("A02", "This user_id is already in use.")
192
266
 
193
267
 
194
268
  class AuthInvalidCredentialsError(NexomError):
195
- """Raised when user_id or password is invalid."""
269
+ """user_id or password is invalid (login)."""
196
270
  def __init__(self) -> None:
197
- super().__init__(
198
- "A02",
199
- "Invalid credentials."
200
- )
271
+ super().__init__("A03", "Invalid credentials.")
201
272
 
202
273
 
203
274
  class AuthUserDisabledError(NexomError):
204
- """Raised when the user is inactive/disabled."""
275
+ """User is inactive/disabled."""
205
276
  def __init__(self) -> None:
206
- super().__init__(
207
- "A03",
208
- "This user is disabled."
209
- )
277
+ super().__init__("A04", "This user is disabled.")
278
+
279
+
280
+ class AuthTokenMissingError(NexomError):
281
+ """Token is missing."""
282
+ def __init__(self) -> None:
283
+ super().__init__("A05", "Token is missing.")
210
284
 
211
285
 
212
286
  class AuthTokenInvalidError(NexomError):
213
- """Raised when token is missing/invalid/expired/revoked."""
287
+ """Token is invalid (malformed / not found)."""
214
288
  def __init__(self) -> None:
215
- super().__init__(
216
- "A04",
217
- "This token is invalid."
218
- )
289
+ super().__init__("A06", "This token is invalid.")
290
+
291
+
292
+ class AuthTokenExpiredError(NexomError):
293
+ """Token is expired."""
294
+ def __init__(self) -> None:
295
+ super().__init__("A07", "This token has expired.")
296
+
297
+
298
+ class AuthTokenRevokedError(NexomError):
299
+ """Token is revoked (logout etc)."""
300
+ def __init__(self) -> None:
301
+ super().__init__("A08", "This token is revoked.")
219
302
 
220
303
 
221
304
  class AuthServiceUnavailableError(NexomError):
222
- """Raised when AuthService is unreachable or failed to respond."""
305
+ """AuthService is unreachable / timed out / invalid response."""
223
306
  def __init__(self) -> None:
224
- super().__init__(
225
- "A05",
226
- "Authentication service is currently unavailable."
227
- )
307
+ super().__init__("A09", "Authentication service is currently unavailable.")
308
+
309
+ def _status_for_auth_error(code: str) -> int:
310
+ return {
311
+ "A01": 400, # missing field
312
+ "A02": 409, # user_id already exists
313
+ "A03": 401, # invalid credentials
314
+ "A04": 403, # user disabled
315
+ "A05": 401, # token missing
316
+ "A06": 401, # token invalid
317
+ "A07": 401, # token expired
318
+ "A08": 401, # token revoked
319
+ "A09": 503, # auth service unavailable
320
+ }.get(code, 400)