supernote 0.4.0__tar.gz → 0.5.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.
Files changed (50) hide show
  1. {supernote-0.4.0 → supernote-0.5.0}/PKG-INFO +1 -1
  2. {supernote-0.4.0 → supernote-0.5.0}/pyproject.toml +1 -1
  3. {supernote-0.4.0 → supernote-0.5.0}/supernote/cli/server.py +28 -37
  4. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/__init__.py +2 -2
  5. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/app.py +22 -9
  6. supernote-0.5.0/supernote/server/config.py +159 -0
  7. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/services/user.py +50 -64
  8. {supernote-0.4.0 → supernote-0.5.0}/supernote.egg-info/PKG-INFO +1 -1
  9. supernote-0.4.0/supernote/server/config.py +0 -16
  10. {supernote-0.4.0 → supernote-0.5.0}/LICENSE +0 -0
  11. {supernote-0.4.0 → supernote-0.5.0}/README.md +0 -0
  12. {supernote-0.4.0 → supernote-0.5.0}/setup.cfg +0 -0
  13. {supernote-0.4.0 → supernote-0.5.0}/supernote/__init__.py +0 -0
  14. {supernote-0.4.0 → supernote-0.5.0}/supernote/cli/__init__.py +0 -0
  15. {supernote-0.4.0 → supernote-0.5.0}/supernote/cli/client.py +0 -0
  16. {supernote-0.4.0 → supernote-0.5.0}/supernote/cli/main.py +0 -0
  17. {supernote-0.4.0 → supernote-0.5.0}/supernote/cli/notebook.py +0 -0
  18. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/__init__.py +0 -0
  19. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/api_model.py +0 -0
  20. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/auth.py +0 -0
  21. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/client.py +0 -0
  22. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/cloud_client.py +0 -0
  23. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/exceptions.py +0 -0
  24. {supernote-0.4.0 → supernote-0.5.0}/supernote/client/login_client.py +0 -0
  25. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/__init__.py +0 -0
  26. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/color.py +0 -0
  27. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/converter.py +0 -0
  28. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/decoder.py +0 -0
  29. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/exceptions.py +0 -0
  30. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/fileformat.py +0 -0
  31. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/manipulator.py +0 -0
  32. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/parser.py +0 -0
  33. {supernote-0.4.0 → supernote-0.5.0}/supernote/notebook/utils.py +0 -0
  34. {supernote-0.4.0 → supernote-0.5.0}/supernote/py.typed +0 -0
  35. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/models/__init__.py +0 -0
  36. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/models/auth.py +0 -0
  37. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/models/base.py +0 -0
  38. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/models/file.py +0 -0
  39. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/routes/auth.py +0 -0
  40. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/routes/decorators.py +0 -0
  41. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/routes/file.py +0 -0
  42. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/routes/system.py +0 -0
  43. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/services/__init__.py +0 -0
  44. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/services/file.py +0 -0
  45. {supernote-0.4.0 → supernote-0.5.0}/supernote/server/services/storage.py +0 -0
  46. {supernote-0.4.0 → supernote-0.5.0}/supernote.egg-info/SOURCES.txt +0 -0
  47. {supernote-0.4.0 → supernote-0.5.0}/supernote.egg-info/dependency_links.txt +0 -0
  48. {supernote-0.4.0 → supernote-0.5.0}/supernote.egg-info/entry_points.txt +0 -0
  49. {supernote-0.4.0 → supernote-0.5.0}/supernote.egg-info/requires.txt +0 -0
  50. {supernote-0.4.0 → supernote-0.5.0}/supernote.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supernote
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services
5
5
  Author-email: jya <jya@wizmy.net>, Allen Porter <allen.porter@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ requires = ["setuptools>=77.0"]
4
4
 
5
5
  [project]
6
6
  name = "supernote"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  license = "Apache-2.0"
9
9
  license-files = ["LICENSE"]
10
10
  description = "All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services"
@@ -2,35 +2,29 @@
2
2
 
3
3
  import argparse
4
4
  import getpass
5
- import os
6
-
7
- import yaml
8
5
 
9
6
  from supernote.server import app as server_app
7
+ from supernote.server.config import AuthConfig, ServerConfig
10
8
  from supernote.server.services.user import UserService
11
9
 
12
10
 
13
- def load_users(users_file):
14
- if not os.path.exists(users_file):
15
- return {"users": []}
16
- with open(users_file, "r") as f:
17
- return yaml.safe_load(f) or {"users": []}
18
-
19
-
20
- def save_users(data, users_file: str):
21
- with open(users_file, "w") as f:
22
- yaml.safe_dump(data, f, default_flow_style=False)
11
+ def get_auth_config(config_dir: str | None) -> AuthConfig:
12
+ # Load base config
13
+ server_config = ServerConfig.load(config_dir)
14
+ return server_config.auth
23
15
 
24
16
 
25
- def list_users(users_file: str):
26
- service = UserService(users_file)
17
+ def list_users(config_dir: str | None):
18
+ config = get_auth_config(config_dir)
19
+ service = UserService(config)
27
20
  for user in service.list_users():
28
- status = "active" if user.get("is_active", True) else "inactive"
29
- print(f"{user['username']} ({status})")
21
+ status = "active" if user.is_active else "inactive"
22
+ print(f"{user.username} ({status})")
30
23
 
31
24
 
32
- def add_user(users_file: str, username: str, password: str | None = None):
33
- service = UserService(users_file)
25
+ def add_user(config_dir: str | None, username: str, password: str | None = None):
26
+ config = get_auth_config(config_dir)
27
+ service = UserService(config)
34
28
  if password is None:
35
29
  password = getpass.getpass(f"Password for {username}: ")
36
30
  if service.add_user(username, password):
@@ -39,8 +33,9 @@ def add_user(users_file: str, username: str, password: str | None = None):
39
33
  print(f"User '{username}' already exists.")
40
34
 
41
35
 
42
- def deactivate_user(users_file: str, username: str):
43
- service = UserService(users_file)
36
+ def deactivate_user(config_dir: str | None, username: str):
37
+ config = get_auth_config(config_dir)
38
+ service = UserService(config)
44
39
  if service.deactivate_user(username):
45
40
  print(f"User '{username}' deactivated.")
46
41
  else:
@@ -56,46 +51,42 @@ def add_parser(subparsers):
56
51
 
57
52
  # User management
58
53
  parser_user = subparsers.add_parser("user", help="User management commands")
59
- parser_user.add_argument(
60
- "--users-file",
61
- type=str,
62
- default="./config/users.yaml",
63
- help="Path to users.yaml file",
64
- )
65
54
  user_subparsers = parser_user.add_subparsers(dest="user_command")
66
55
 
67
56
  # user list
68
- parser_user_list = user_subparsers.add_parser(
69
- "list", help="List all users in users.yaml"
70
- )
71
- parser_user_list.set_defaults(func=lambda args: list_users(args.users_file))
57
+ parser_user_list = user_subparsers.add_parser("list", help="List all users")
58
+ parser_user_list.set_defaults(func=lambda args: list_users(args.config_dir))
72
59
 
73
60
  # user add
74
- parser_user_add = user_subparsers.add_parser(
75
- "add", help="Add a new user to users.yaml"
76
- )
61
+ parser_user_add = user_subparsers.add_parser("add", help="Add a new user")
77
62
  parser_user_add.add_argument("username", type=str, help="Username to add")
78
63
  parser_user_add.add_argument(
79
64
  "--password", type=str, help="Password (if omitted, prompt interactively)"
80
65
  )
81
66
  parser_user_add.set_defaults(
82
- func=lambda args: add_user(args.users_file, args.username, args.password)
67
+ func=lambda args: add_user(args.config_dir, args.username, args.password)
83
68
  )
84
69
 
85
70
  # user deactivate
86
71
  parser_user_deactivate = user_subparsers.add_parser(
87
- "deactivate", help="Deactivate a user in users.yaml"
72
+ "deactivate", help="Deactivate a user"
88
73
  )
89
74
  parser_user_deactivate.add_argument(
90
75
  "username", type=str, help="Username to deactivate"
91
76
  )
92
77
  parser_user_deactivate.set_defaults(
93
- func=lambda args: deactivate_user(args.users_file, args.username)
78
+ func=lambda args: deactivate_user(args.config_dir, args.username)
94
79
  )
95
80
 
96
81
 
97
82
  def main():
98
83
  parser = argparse.ArgumentParser(description="Supernote Server CLI")
84
+ parser.add_argument(
85
+ "--config-dir",
86
+ type=str,
87
+ default=None,
88
+ help="Path to configuration directory (default: config/)",
89
+ )
99
90
  subparsers = parser.add_subparsers(dest="command")
100
91
  add_parser(subparsers)
101
92
  args = parser.parse_args()
@@ -3,10 +3,10 @@
3
3
  """
4
4
 
5
5
  from .app import create_app, run
6
- from .config import Config
6
+ from .config import ServerConfig
7
7
 
8
8
  __all__ = [
9
9
  "create_app",
10
10
  "run",
11
- "Config",
11
+ "ServerConfig",
12
12
  ]
@@ -6,9 +6,9 @@ from typing import Any, Awaitable, Callable
6
6
 
7
7
  from aiohttp import web
8
8
 
9
- from supernote.server.services.user import JWT_ALGORITHM, JWT_SECRET
9
+ from supernote.server.services.user import JWT_ALGORITHM
10
10
 
11
- from . import config
11
+ from .config import ServerConfig
12
12
  from .models.base import create_error_response
13
13
  from .routes import auth, file, system
14
14
  from .services.file import FileService
@@ -56,8 +56,11 @@ async def trace_middleware(
56
56
  "body": body_str,
57
57
  }
58
58
 
59
+ # Get config from app
60
+ server_config: ServerConfig = request.app["config"]
61
+
59
62
  try:
60
- with open(config.TRACE_LOG_FILE, "a") as f:
63
+ with open(server_config.trace_log_file, "a") as f:
61
64
  f.write(json.dumps(log_entry) + "\n")
62
65
  f.flush()
63
66
  except Exception as e:
@@ -92,8 +95,12 @@ async def jwt_auth_middleware(
92
95
 
93
96
  import jwt
94
97
 
98
+ server_config: ServerConfig = request.app["config"]
99
+
95
100
  try:
96
- payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
101
+ payload = jwt.decode(
102
+ token, server_config.auth.secret_key, algorithms=[JWT_ALGORITHM]
103
+ )
97
104
  except jwt.InvalidTokenError:
98
105
  return web.json_response(
99
106
  create_error_response("Invalid token").to_dict(), status=401
@@ -102,15 +109,19 @@ async def jwt_auth_middleware(
102
109
  return await handler(request)
103
110
 
104
111
 
105
- def create_app() -> web.Application:
112
+ def create_app(config: ServerConfig | None = None) -> web.Application:
113
+ if config is None:
114
+ config = ServerConfig.load()
115
+
106
116
  app = web.Application(middlewares=[trace_middleware, jwt_auth_middleware])
117
+ app["config"] = config
107
118
 
108
119
  # Initialize services
109
- storage_root = Path(config.STORAGE_DIR)
120
+ storage_root = Path(config.storage_dir)
110
121
  temp_root = storage_root / "temp"
111
122
  storage_service = StorageService(storage_root, temp_root)
112
123
  app["storage_service"] = storage_service
113
- app["user_service"] = UserService(config.USER_CONFIG_FILE)
124
+ app["user_service"] = UserService(config.auth)
114
125
  app["file_service"] = FileService(storage_service)
115
126
 
116
127
  # Register routes
@@ -125,5 +136,7 @@ def create_app() -> web.Application:
125
136
 
126
137
  def run(args: Any) -> None:
127
138
  logging.basicConfig(level=logging.DEBUG)
128
- app = create_app()
129
- web.run_app(app, host=config.HOST, port=config.PORT)
139
+ config_dir = getattr(args, "config_dir", None)
140
+ config = ServerConfig.load(config_dir)
141
+ app = create_app(config)
142
+ web.run_app(app, host=config.host, port=config.port)
@@ -0,0 +1,159 @@
1
+ import logging
2
+ import os
3
+ import secrets
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from mashumaro.mixins.dict import DataClassDictMixin
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class AuthConfig(DataClassDictMixin):
16
+ secret_key: str = ""
17
+ expiration_hours: int = 24
18
+ users_file: str = "users.yaml"
19
+
20
+
21
+ @dataclass
22
+ class UserEntry(DataClassDictMixin):
23
+ username: str
24
+ password_md5: str
25
+ is_active: bool = True
26
+ devices: list[str] = field(default_factory=list)
27
+ profile: dict[str, Any] = field(default_factory=dict)
28
+ mobile: str | None = None
29
+ email: str | None = None
30
+ avatar: str | None = None
31
+ signature: str | None = None
32
+
33
+
34
+ @dataclass
35
+ class UsersConfig(DataClassDictMixin):
36
+ users: list[UserEntry] = field(default_factory=list)
37
+
38
+ @classmethod
39
+ def load(cls, path: str | Path) -> "UsersConfig":
40
+ path = Path(path)
41
+ if not path.exists():
42
+ return cls()
43
+ try:
44
+ with open(path, "r") as f:
45
+ data = yaml.safe_load(f) or {}
46
+ return cls.from_dict(data)
47
+ except Exception as e:
48
+ logger.warning(f"Failed to load users file {path}: {e}")
49
+ return cls()
50
+
51
+ def save(self, path: str | Path) -> None:
52
+ path = Path(path)
53
+ if not path.parent.exists():
54
+ path.parent.mkdir(parents=True, exist_ok=True)
55
+ with open(path, "w") as f:
56
+ yaml.safe_dump(self.to_dict(), f, default_flow_style=False)
57
+
58
+
59
+ @dataclass
60
+ class ServerConfig(DataClassDictMixin):
61
+ host: str = "0.0.0.0"
62
+ port: int = 8080
63
+ trace_log_file: str = "data/server_trace.log"
64
+ storage_dir: str = "storage"
65
+ auth: AuthConfig = field(default_factory=AuthConfig)
66
+
67
+ @classmethod
68
+ def load(cls, config_dir: str | Path | None = None) -> "ServerConfig":
69
+ """Load configuration from directory."""
70
+ # 1. Determine config directory
71
+ if config_dir is None:
72
+ config_dir = os.getenv("SUPERNOTE_CONFIG_DIR", "config")
73
+
74
+ config_dir_path = Path(config_dir)
75
+ config_file = config_dir_path / "config.yaml"
76
+
77
+ # 2. Load from YAML if exists
78
+ file_data: dict[str, Any] = {}
79
+ if file_exists := config_file.exists():
80
+ try:
81
+ with open(config_file, "r") as f:
82
+ file_data = yaml.safe_load(f) or {}
83
+ except Exception as e:
84
+ logger.warning(f"Failed to load config file {config_file}: {e}")
85
+
86
+ # 3. Create config object using mashumaro
87
+ try:
88
+ config = cls.from_dict(file_data)
89
+ except Exception as e:
90
+ logger.warning(
91
+ f"Failed to parse config file {config_file}, using defaults: {e}"
92
+ )
93
+ config = cls()
94
+
95
+ # Resolve users_file relative to config_dir if not absolute
96
+ users_path = Path(config.auth.users_file)
97
+ if not users_path.is_absolute():
98
+ config.auth.users_file = str(config_dir_path / users_path)
99
+
100
+ # 4. Generate secret if missing AND not provided by env var
101
+ env_secret = os.getenv("SUPERNOTE_JWT_SECRET")
102
+ secret_generated = False
103
+
104
+ if not config.auth.secret_key and not env_secret:
105
+ logger.warning("No JWT secret key configured. Generating a new random key.")
106
+ config.auth.secret_key = secrets.token_hex(32)
107
+ secret_generated = True
108
+
109
+ # 5. Save config if it was missing or if we generated a secret
110
+ if not file_exists or secret_generated:
111
+ try:
112
+ # Ensure directory exists
113
+ if not config_file.parent.exists():
114
+ config_file.parent.mkdir(parents=True, exist_ok=True)
115
+
116
+ # Prepare config for saving (restore relative paths if possible)
117
+ config_to_save = cls.from_dict(config.to_dict())
118
+ try:
119
+ users_path_abs = Path(config.auth.users_file)
120
+ if users_path_abs.is_absolute() and users_path_abs.is_relative_to(
121
+ config_dir_path
122
+ ):
123
+ config_to_save.auth.users_file = str(
124
+ users_path_abs.relative_to(config_dir_path)
125
+ )
126
+ except ValueError:
127
+ pass
128
+
129
+ with open(config_file, "w") as f:
130
+ yaml.safe_dump(
131
+ config_to_save.to_dict(), f, default_flow_style=False
132
+ )
133
+
134
+ if secret_generated:
135
+ logger.info(
136
+ f"Saved new configuration with generated secret to {config_file}"
137
+ )
138
+ else:
139
+ logger.info(f"Created default configuration file at {config_file}")
140
+ except Exception as e:
141
+ logger.warning(f"Failed to save configuration to {config_file}: {e}")
142
+
143
+ # 6. Apply env var override (runtime only, not saved)
144
+ if env_secret:
145
+ config.auth.secret_key = env_secret
146
+
147
+ if os.getenv("SUPERNOTE_HOST"):
148
+ config.host = os.getenv("SUPERNOTE_HOST", config.host)
149
+
150
+ if os.getenv("SUPERNOTE_PORT"):
151
+ try:
152
+ config.port = int(os.getenv("SUPERNOTE_PORT", str(config.port)))
153
+ except ValueError:
154
+ pass
155
+
156
+ if os.getenv("SUPERNOTE_STORAGE_DIR"):
157
+ config.storage_dir = os.getenv("SUPERNOTE_STORAGE_DIR", config.storage_dir)
158
+
159
+ return config
@@ -1,73 +1,59 @@
1
1
  import hashlib
2
2
  import logging
3
- import os
4
3
  import secrets
5
4
  import time
6
5
  from typing import Optional
7
6
 
8
7
  import jwt
9
- import yaml
10
8
 
9
+ from ..config import AuthConfig, UserEntry, UsersConfig
11
10
  from ..models.auth import LoginResult, UserVO
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
- # TODO: This should be generated on first startup and stored somewhere secure
16
- JWT_SECRET = os.environ.get("SUPERNOTE_JWT_SECRET", "supernote-secret-key")
17
14
  JWT_ALGORITHM = "HS256"
18
- JWT_EXPIRATION_HOURS = int(os.environ.get("SUPERNOTE_JWT_EXPIRATION_HOURS", "24"))
19
-
20
-
21
- def _load_users(users_file: str) -> list[dict]:
22
- if not os.path.exists(users_file):
23
- return []
24
- with open(users_file, "r") as f:
25
- data = yaml.safe_load(f) or {"users": []}
26
- return data.get("users") or []
27
-
28
-
29
- def _save_users(users_file: str, users: list[dict]) -> None:
30
- with open(users_file, "w") as f:
31
- yaml.safe_dump({"users": users}, f, default_flow_style=False)
32
15
 
33
16
 
34
17
  class UserService:
35
- def __init__(self, users_file: str):
36
- self._users_file = users_file
37
- self._users = _load_users(users_file)
18
+ def __init__(self, config: AuthConfig):
19
+ self._config = config
20
+ self._users_config = UsersConfig.load(config.users_file)
38
21
  self._random_codes: dict[
39
22
  str, tuple[str, str]
40
23
  ] = {} # account -> (code, timestamp)
41
24
 
42
- def list_users(self) -> list[dict]:
25
+ @property
26
+ def _users(self) -> list[UserEntry]:
27
+ return self._users_config.users
28
+
29
+ def list_users(self) -> list[UserEntry]:
43
30
  return list(self._users)
44
31
 
45
32
  def add_user(self, username: str, password: str) -> bool:
46
- if any(u["username"] == username for u in self._users):
33
+ if any(u.username == username for u in self._users):
47
34
  return False
48
35
  password_md5 = hashlib.md5(password.encode()).hexdigest()
49
- self._users.append(
50
- {
51
- "username": username,
52
- "password_md5": password_md5,
53
- "is_active": True,
54
- "devices": [], # List of bound equipment numbers
55
- "profile": {}, # User profile data
56
- }
36
+ new_user = UserEntry(
37
+ username=username,
38
+ password_md5=password_md5,
39
+ is_active=True,
40
+ devices=[],
41
+ profile={},
57
42
  )
58
- _save_users(self._users_file, self._users)
43
+ self._users.append(new_user)
44
+ self._users_config.save(self._config.users_file)
59
45
  return True
60
46
 
61
47
  def deactivate_user(self, username: str) -> bool:
62
48
  for user in self._users:
63
- if user["username"] == username:
64
- user["is_active"] = False
65
- _save_users(self._users_file, self._users)
49
+ if user.username == username:
50
+ user.is_active = False
51
+ self._users_config.save(self._config.users_file)
66
52
  return True
67
53
  return False
68
54
 
69
55
  def check_user_exists(self, account: str) -> bool:
70
- return any(u["username"] == account for u in self._users)
56
+ return any(u.username == account for u in self._users)
71
57
 
72
58
  def generate_random_code(self, account: str) -> tuple[str, str]:
73
59
  """Generate a random code for login challenge."""
@@ -77,30 +63,31 @@ class UserService:
77
63
  self._random_codes[account] = (random_code, timestamp)
78
64
  return random_code, timestamp
79
65
 
80
- def _get_user(self, account: str) -> dict | None:
66
+ def _get_user(self, account: str) -> UserEntry | None:
81
67
  for user in self._users:
82
- if user["username"] == account:
68
+ if user.username == account:
83
69
  return user
84
70
  return None
85
71
 
86
72
  def verify_password(self, account: str, password: str) -> bool:
87
73
  user = self._get_user(account)
88
- if not user or not user.get("is_active", True):
74
+ if not user or not user.is_active:
89
75
  logger.info("User not found or inactive: %s", account)
90
76
  return False
91
- if (password_md5 := user.get("password_md5")) is None:
77
+ if not user.password_md5:
92
78
  logger.info("MD5 password hash not found for user: %s", account)
93
79
  return False
94
80
  # Compute md5(password) and compare
95
81
  password_bytes = password.encode()
96
82
  hash_hex = hashlib.md5(password_bytes).hexdigest()
97
- return bool(hash_hex == password_md5)
83
+ return bool(hash_hex == user.password_md5)
98
84
 
99
85
  def verify_login_hash(self, account: str, client_hash: str, timestamp: str) -> bool:
100
86
  user = self._get_user(account)
101
- if not user or not user.get("is_active", True):
87
+ if not user or not user.is_active:
102
88
  logger.info("User not found or inactive: %s", account)
103
89
  return False
90
+
104
91
  code_tuple = self._random_codes.get(account)
105
92
  if not code_tuple or code_tuple[1] != timestamp:
106
93
  logger.warning(
@@ -108,12 +95,15 @@ class UserService:
108
95
  )
109
96
  return False
110
97
  random_code = code_tuple[0]
111
- if (password_md5 := user.get("password_md5")) is None:
98
+
99
+ if not user.password_md5:
112
100
  logger.info("MD5 password hash not found for user: %s", account)
113
101
  return False
114
- # Compute expected hash: sha256(password_md5 + random_code + timestamp)
115
- concat = password_md5 + random_code
102
+
103
+ # Compute expected hash: sha256(password_md5 + random_code)
104
+ concat = user.password_md5 + random_code
116
105
  expected_hash = hashlib.sha256(concat.encode()).hexdigest()
106
+
117
107
  if expected_hash == client_hash:
118
108
  return True
119
109
  logger.info("Login hash mismatch for user: %s", account)
@@ -138,7 +128,7 @@ class UserService:
138
128
  LoginResult if login is successful, None otherwise.
139
129
  """
140
130
  user = self._get_user(account)
141
- if not user or not user.get("is_active", True):
131
+ if not user or not user.is_active:
142
132
  # TODO: Raise exceptions so we can return a useful error message
143
133
  # to the web APIs.
144
134
  logger.warning("Login failed: user not found or inactive: %s", account)
@@ -155,7 +145,7 @@ class UserService:
155
145
  return None
156
146
 
157
147
  # Check binding status
158
- bound_devices = user.get("devices", [])
148
+ bound_devices = user.devices
159
149
  is_bind = "Y" if bound_devices else "N"
160
150
  is_bind_equipment = "N"
161
151
  if equipment_no and equipment_no in bound_devices:
@@ -165,9 +155,9 @@ class UserService:
165
155
  "sub": account,
166
156
  "equipment_no": equipment_no or "",
167
157
  "iat": int(time.time()),
168
- "exp": int(time.time()) + (JWT_EXPIRATION_HOURS * 3600),
158
+ "exp": int(time.time()) + (self._config.expiration_hours * 3600),
169
159
  }
170
- token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
160
+ token = jwt.encode(payload, self._config.secret_key, algorithm=JWT_ALGORITHM)
171
161
 
172
162
  return LoginResult(
173
163
  token=token,
@@ -182,17 +172,17 @@ class UserService:
182
172
  return None
183
173
 
184
174
  # Default profile values
185
- username = user["username"]
186
- profile = user.get("profile", {})
175
+ username = user.username
176
+ profile = user.profile
187
177
 
188
178
  return UserVO(
189
179
  user_name=profile.get("user_name", username),
190
- email=profile.get("email", username),
191
- phone=profile.get("phone", ""),
180
+ email=user.email or profile.get("email", username),
181
+ phone=user.mobile or profile.get("phone", ""),
192
182
  country_code=profile.get("country_code", "1"),
193
183
  total_capacity=profile.get("total_capacity", "25485312"),
194
184
  file_server=profile.get("file_server", "0"),
195
- avatars_url=profile.get("avatars_url", ""),
185
+ avatars_url=user.avatar or profile.get("avatars_url", ""),
196
186
  birthday=profile.get("birthday", ""),
197
187
  sex=profile.get("sex", ""),
198
188
  )
@@ -205,11 +195,9 @@ class UserService:
205
195
  logger.warning("User not found for binding: %s", account)
206
196
  return False
207
197
 
208
- devices = user.get("devices", [])
209
- if equipment_no not in devices:
210
- devices.append(equipment_no)
211
- user["devices"] = devices
212
- _save_users(self._users_file, self._users)
198
+ if equipment_no not in user.devices:
199
+ user.devices.append(equipment_no)
200
+ self._users_config.save(self._config.users_file)
213
201
 
214
202
  return True
215
203
 
@@ -218,13 +206,11 @@ class UserService:
218
206
  logger.info("Unlinking equipment %s", equipment_no)
219
207
  found = False
220
208
  for user in self._users:
221
- devices = user.get("devices", [])
222
- if equipment_no in devices:
223
- devices.remove(equipment_no)
224
- user["devices"] = devices
209
+ if equipment_no in user.devices:
210
+ user.devices.remove(equipment_no)
225
211
  found = True
226
212
 
227
213
  if found:
228
- _save_users(self._users_file, self._users)
214
+ self._users_config.save(self._config.users_file)
229
215
 
230
216
  return True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: supernote
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: All-in-one toolkit for Supernote devices: parse notebooks, self-host services, access services
5
5
  Author-email: jya <jya@wizmy.net>, Allen Porter <allen.porter@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1,16 +0,0 @@
1
- import os
2
-
3
- # Default port for Supernote Private Cloud
4
- PORT = int(os.getenv("SUPERNOTE_PORT", "8080"))
5
- HOST = os.getenv("SUPERNOTE_HOST", "0.0.0.0")
6
- TRACE_LOG_FILE = os.getenv("SUPERNOTE_TRACE_LOG", "data/server_trace.log")
7
- STORAGE_DIR = os.getenv("SUPERNOTE_STORAGE_DIR", "storage")
8
- USER_CONFIG_FILE = os.getenv("SUPERNOTE_USER_CONFIG_FILE", "config/users.yaml")
9
-
10
-
11
- class Config:
12
- PORT = PORT
13
- HOST = HOST
14
- TRACE_LOG_FILE = TRACE_LOG_FILE
15
- STORAGE_DIR = STORAGE_DIR
16
- USER_CONFIG_FILE = USER_CONFIG_FILE
File without changes
File without changes
File without changes
File without changes