Jelajahi Sumber

chore(secrets): use OS keyring for DB password and ignore .env

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
tanlie 6 hari lalu
induk
melakukan
4d24e407c5
7 mengubah file dengan 133 tambahan dan 0 penghapusan
  1. 4 0
      app/app.py
  2. 42 0
      app/config/setting.py
  3. 6 0
      app/extensions.py
  4. 16 0
      app/libs/enums.py
  5. 13 0
      app/models/user.py
  6. 46 0
      app/validators/user.py
  7. 6 0
      requirements.txt

+ 4 - 0
app/app.py

@@ -3,6 +3,7 @@
 from flask import Flask
 
 from app.config.setting import config
+from app.extensions import db
 
 
 def register_blueprints(app):
@@ -22,6 +23,9 @@ def create_app(env: str = "default"):
     app = Flask(__name__)
     app.config.from_object(config[env])
 
+    # initialize extensions
+    db.init_app(app)
+
     # optional: initialize logging and other extensions here
     try:
         from app.utils.logger import setup_logging

+ 42 - 0
app/config/setting.py

@@ -1,12 +1,45 @@
 """Application configuration settings."""
 
 import os
+from urllib.parse import quote_plus
+
 from dotenv import load_dotenv
 
 # Load .env from project root (if present)
 load_dotenv()
 
 
+def build_database_uri():
+    """Build a SQLAlchemy MySQL DSN from environment variables.
+
+    Password resolution order:
+    1. MYSQL_PASSWORD environment variable
+    2. Secret store via keyring (service 'ginger_mysql' and username)
+    3. Empty password (not recommended)
+    """
+    user = os.getenv("MYSQL_USER", "root")
+    raw_password = os.getenv("MYSQL_PASSWORD")
+
+    # Try secret store if password not provided in env
+    if not raw_password:
+        try:
+            import keyring
+
+            raw_password = keyring.get_password("ginger_mysql", user)
+        except Exception:
+            raw_password = None
+
+    password = quote_plus(raw_password or "")
+    host = os.getenv("MYSQL_HOST", "127.0.0.1")
+    port = os.getenv("MYSQL_PORT", "3306")
+    db_name = os.getenv("MYSQL_DB", "flask")
+    charset = os.getenv("MYSQL_CHARSET", "utf8mb4")
+    return (
+        f"mysql+pymysql://{user}:{password}@{host}:{port}/{db_name}"
+        f"?charset={charset}"
+    )
+
+
 class BaseConfig:
     """Base configuration shared by all environments."""
 
@@ -14,6 +47,15 @@ class BaseConfig:
     DEBUG = False
     TESTING = False
 
+    MYSQL_USER = os.getenv("MYSQL_USER", "root")
+    MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
+    MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
+    MYSQL_PORT = os.getenv("MYSQL_PORT", "3306")
+    MYSQL_DB = os.getenv("MYSQL_DB", "flask")
+    MYSQL_CHARSET = os.getenv("MYSQL_CHARSET", "utf8mb4")
+    SQLALCHEMY_DATABASE_URI = build_database_uri()
+    SQLALCHEMY_TRACK_MODIFICATIONS = False
+
 
 class DevelopmentConfig(BaseConfig):
     """Development configurations."""

+ 6 - 0
app/extensions.py

@@ -0,0 +1,6 @@
+"""Shared Flask extensions."""
+
+from flask_sqlalchemy import SQLAlchemy
+
+
+db = SQLAlchemy()

+ 16 - 0
app/libs/enums.py

@@ -0,0 +1,16 @@
+"""
+enums -
+auther: tanlie
+date: 2026/8/23
+"""
+from enum import Enum
+
+
+class ClientTypeEnum(Enum):
+    USER_EMAIL = 100
+    USER_MOBILE = 101
+
+    # 微信小程序
+    USER_MINA = 200
+    # 微信公众号
+    USER_WX = 201

+ 13 - 0
app/models/user.py

@@ -0,0 +1,13 @@
+from app.extensions import db
+
+
+class User(db.Model):
+    """Simple user model for testing database connectivity."""
+
+    __tablename__ = "users"
+
+    id = db.Column(db.Integer, primary_key=True)
+    username = db.Column(db.String(80), unique=True, nullable=False)
+
+    def to_dict(self):
+        return {"id": self.id, "username": self.username}

+ 46 - 0
app/validators/user.py

@@ -0,0 +1,46 @@
+"""
+user -
+auther: tanlie
+date: 2026/8/23
+"""
+from wtforms import StringField, IntegerField, ValidationError
+from wtforms.validators import DataRequired, Email, Regexp, length
+
+from app.libs.enums import ClientTypeEnum
+from app.validators.base import BaseForm as Form
+
+
+class ClientForm(Form):
+    account = StringField(validators=[DataRequired(message='不允许为空'), length(
+        min=5, max=32
+    )])
+    secret = StringField()
+    type = IntegerField(validators=[DataRequired()])
+
+    def validate_type(self, value):
+        try:
+            client = ClientTypeEnum(value.data)
+        except ValueError as e:
+            raise e
+        self.type.data = client
+
+
+class UserEmailForm(ClientForm):
+    account = StringField(validators=[
+        Email(message='invalidate email')
+    ])
+    secret = StringField(validators=[
+        DataRequired(),
+        # password can only include letters , numbers and "_"
+        Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')
+    ])
+    nickname = StringField(validators=[DataRequired(),
+                                       length(min=2, max=22)])
+
+    def validate_account(self, value):
+        if len(value.data) < 5 or len(value.data) > 32:
+            raise ValidationError(
+                'Account must be between 5 and 32 characters long'
+            )
+            # if User.query.filter_by(email=value.data).first():
+            raise ValidationError()

+ 6 - 0
requirements.txt

@@ -2,7 +2,9 @@ blinker==1.9.0
 click==8.4.2
 colorama==0.4.6
 Flask==3.1.3
+Flask-SQLAlchemy==3.1.1
 Flask-WTF==1.3.0
+greenlet==3.5.5
 iniconfig==2.3.0
 itsdangerous==2.2.0
 Jinja2==3.1.6
@@ -10,7 +12,11 @@ MarkupSafe==3.0.3
 packaging==26.3
 pluggy==1.6.0
 Pygments==2.21.0
+PyMySQL==1.2.0
+keyring==23.13.1
 pytest==9.1.1
 python-dotenv==1.2.3
+SQLAlchemy==2.0.52
+typing_extensions==4.16.0
 Werkzeug==3.1.8
 WTForms==3.2.2