Compare commits

..

2 Commits

13 changed files with 92 additions and 52 deletions
+15
View File
@@ -0,0 +1,15 @@
from fastapi import APIRouter
from .deps import get_current_user
from .jwt import create_jwt_token, create_refresh_token, verify_access_token
# 统一创建并导出路由器,满足 main.py 的 include_router 需求
router = APIRouter(tags=["Authentication"])
# 显式声明这个包对外暴露的接口
__all__ = [
"router",
"get_current_user",
"create_jwt_token",
"create_refresh_token",
"verify_access_token",
]
+1 -1
View File
@@ -1,6 +1,6 @@
from fastapi import Request, HTTPException from fastapi import Request, HTTPException
import app.auth.jwt as jwt
import os import os
from . import jwt
JWT_SECRET = os.getenv("JWT_SECRET") JWT_SECRET = os.getenv("JWT_SECRET")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256") JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
+1 -1
View File
@@ -1,6 +1,6 @@
import app.auth.jwt as jwt
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import os import os
import jwt
from fastapi import HTTPException, status from fastapi import HTTPException, status
JWT_SECRET = os.getenv("JWT_SECRET") JWT_SECRET = os.getenv("JWT_SECRET")
+14
View File
@@ -1,8 +1,22 @@
import os import os
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv
# 加载 .env 文件
load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL") DATABASE_URL = os.getenv("DATABASE_URL")
# 自动将旧版的 postgres:// 替换为 SQLAlchemy 强要求的 postgresql://
if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
elif DATABASE_URL and DATABASE_URL.startswith("postgres+"):
DATABASE_URL = DATABASE_URL.replace("postgres+", "postgresql+", 1)
# 如果环境变量彻底缺失,做一个安全的报错提示
if not DATABASE_URL:
raise ValueError("DATABASE_URL environment variable is not set or empty.")
engine = create_engine(DATABASE_URL) engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine) SessionLocal = sessionmaker(bind=engine)
+2 -5
View File
@@ -24,8 +24,5 @@ schema = Schema(query=Query, mutation=Mutation)
graphql_router = GraphQLRouter(schema, context_getter=get_context) graphql_router = GraphQLRouter(schema, context_getter=get_context)
# 挂载路由 # 挂载路由
# 保留现有的 auth 路由供 Google OAuth 回调使用 app.include_router(auth_router) # 保留现有的 auth 路由供 Google OAuth 回调使用
app.include_router(auth_router) app.include_router(graphql_router, prefix="/graphql") # 挂载 GraphQL 终点,前端以后只需要请求 /graphql
# 挂载 GraphQL 终点,前端以后只需要请求 /graphql
app.include_router(graphql_router, prefix="/graphql")
+8 -6
View File
@@ -56,22 +56,24 @@ class AutoID:
不再需要手动定义任何 prefix,全自动根据 __tablename__ 编码生成 不再需要手动定义任何 prefix,全自动根据 __tablename__ 编码生成
支持子类自定义长度 支持子类自定义长度
""" """
# 默认配置 # 全局固定配置
_id_prefix_len = 3 # 前缀长度 ID_PREFIX_LEN = 3
_id_payload_len = 22 # 主体长度 ID_PAYLOAD_LEN = 22
@classmethod @classmethod
def id_length(cls) -> int: def id_length(cls) -> int:
"""返回当前类 ID 的总长度""" """返回当前类 ID 的总长度"""
return cls._id_prefix_len + cls._id_payload_len return cls.ID_PREFIX_LEN + cls.ID_PAYLOAD_LEN
@declared_attr @declared_attr
def id(cls): def id(cls):
# 运行时动态获取子类的 __tablename__ # 运行时动态获取子类的 __tablename__
tablename = cls.__tablename__ tablename = cls.__tablename__
# 获取子类配置的长度 # 获取子类配置的长度
p_len = cls._id_prefix_len p_len = AutoID.ID_PREFIX_LEN
s_len = cls._id_payload_len s_len = AutoID.ID_PAYLOAD_LEN
prefix = encode_tablename_to_prefix(tablename, length=p_len) prefix = encode_tablename_to_prefix(tablename, length=p_len)
# 核心工厂函数:拼接【编码后的表前缀】和【递增Payload】 # 核心工厂函数:拼接【编码后的表前缀】和【递增Payload】
+2 -9
View File
@@ -1,25 +1,18 @@
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime, String, ForeignKey, UniqueConstraint from sqlalchemy import Column, DateTime, String, ForeignKey, UniqueConstraint
from sqlalchemy.sql import func from sqlalchemy.sql import func
from .base import AutoID, Base from .base import AutoID, Base
# 只有在静态检查时才导入
# 运行阶段这段代码会被跳过,彻底避免循环导入
# 不确定是否用得上,但感觉这是个好习惯,尤其在模型之间有外键关系时
if TYPE_CHECKING:
from .user import User
class Follow(Base, AutoID): class Follow(Base, AutoID):
"""用户关注关系表""" """用户关注关系表"""
__tablename__ = "follows" __tablename__ = "follows"
follower_id = Column( follower_id = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="CASCADE"), ForeignKey("users.id", ondelete="CASCADE"),
index=True index=True
) )
followee_id = Column( followee_id = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="CASCADE"), ForeignKey("users.id", ondelete="CASCADE"),
index=True index=True
) )
+6 -12
View File
@@ -1,24 +1,18 @@
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime, String, ForeignKey, UniqueConstraint from sqlalchemy import Column, DateTime, String, ForeignKey, UniqueConstraint
from sqlalchemy.sql import func from sqlalchemy.sql import func
from .base import AutoID, Base from .base import AutoID, Base
if TYPE_CHECKING:
from .user import User
from .project import ProjectItem
from .workspace import Workspace
class ProjectLike(Base, AutoID): class ProjectLike(Base, AutoID):
"""项目点赞表""" """项目点赞表"""
__tablename__ = "project_likes" __tablename__ = "project_likes"
user_id = Column( user_id = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="CASCADE"), ForeignKey("users.id", ondelete="CASCADE"),
index=True index=True
) )
project_item_id = Column( project_item_id = Column(
String(ProjectItem.id_length()), String(AutoID.id_length()),
ForeignKey("project_items.id", ondelete="CASCADE"), ForeignKey("project_items.id", ondelete="CASCADE"),
index=True index=True
) )
@@ -34,12 +28,12 @@ class ProjectFavorite(Base, AutoID):
__tablename__ = "project_favorites" __tablename__ = "project_favorites"
user_id = Column( user_id = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="CASCADE"), ForeignKey("users.id", ondelete="CASCADE"),
index=True index=True
) )
project_item_id = Column( project_item_id = Column(
String(ProjectItem.id_length()), String(AutoID.id_length()),
ForeignKey("project_items.id", ondelete="CASCADE"), ForeignKey("project_items.id", ondelete="CASCADE"),
index=True index=True
) )
@@ -55,12 +49,12 @@ class ProjectShare(Base, AutoID):
__tablename__ = "project_shares" __tablename__ = "project_shares"
user_id = Column( user_id = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="CASCADE"), ForeignKey("users.id", ondelete="CASCADE"),
index=True index=True
) )
project_item_id = Column( project_item_id = Column(
String(ProjectItem.id_length()), String(AutoID.id_length()),
ForeignKey("project_items.id", ondelete="CASCADE"), ForeignKey("project_items.id", ondelete="CASCADE"),
index=True index=True
) )
+2 -7
View File
@@ -1,18 +1,13 @@
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime, String, ForeignKey, BigInteger from sqlalchemy import Column, DateTime, String, ForeignKey, BigInteger
from sqlalchemy.sql import func from sqlalchemy.sql import func
from .base import AutoID, Base from .base import AutoID, Base
if TYPE_CHECKING:
from .user import User
from .workspace import Workspace
class LibraryItem(Base, AutoID): class LibraryItem(Base, AutoID):
"""工作区共享媒体/文件资源库""" """工作区共享媒体/文件资源库"""
__tablename__ = "library_items" __tablename__ = "library_items"
workspace_id = Column( workspace_id = Column(
String(Workspace.id_length()), String(AutoID.id_length()),
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
index=True, index=True,
) )
@@ -34,7 +29,7 @@ class LibraryItem(Base, AutoID):
mime_type = Column(String, nullable=True) # 媒体类型,例如 'image/jpeg', 'video/mp4', 'application/pdf' mime_type = Column(String, nullable=True) # 媒体类型,例如 'image/jpeg', 'video/mp4', 'application/pdf'
uploaded_by = Column( uploaded_by = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="SET NULL"), ForeignKey("users.id", ondelete="SET NULL"),
nullable=True, nullable=True,
) )
+1 -5
View File
@@ -1,17 +1,13 @@
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime, String, ForeignKey, Integer from sqlalchemy import Column, DateTime, String, ForeignKey, Integer
from sqlalchemy.sql import func from sqlalchemy.sql import func
from .base import AutoID, Base from .base import AutoID, Base
if TYPE_CHECKING:
from .workspace import Workspace
class ProjectItem(Base, AutoID): class ProjectItem(Base, AutoID):
"""项目与文件夹的树状虚拟文件系统""" """项目与文件夹的树状虚拟文件系统"""
__tablename__ = "project_items" __tablename__ = "project_items"
workspace_id = Column( workspace_id = Column(
String(Workspace.id_length()), String(AutoID.id_length()),
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
index=True, index=True,
) )
+2 -6
View File
@@ -1,11 +1,7 @@
from typing import TYPE_CHECKING
from sqlalchemy import Column, DateTime, String, ForeignKey, BigInteger, UniqueConstraint, Integer from sqlalchemy import Column, DateTime, String, ForeignKey, BigInteger, UniqueConstraint, Integer
from sqlalchemy.sql import func from sqlalchemy.sql import func
from .base import AutoID, Base from .base import AutoID, Base
if TYPE_CHECKING:
from .user import User
class Workspace(Base, AutoID): class Workspace(Base, AutoID):
"""工作区表""" """工作区表"""
__tablename__ = "workspaces" __tablename__ = "workspaces"
@@ -34,13 +30,13 @@ class WorkspaceMember(Base, AutoID):
__tablename__ = "workspace_members" __tablename__ = "workspace_members"
workspace_id = Column( workspace_id = Column(
String(Workspace.id_length()), String(AutoID.id_length()),
ForeignKey("workspaces.id", ondelete="CASCADE"), ForeignKey("workspaces.id", ondelete="CASCADE"),
index=True, index=True,
) )
# 允许 user_id 为空,因为被邀请的人可能还没注册系统,我们先记录他的邮箱 # 允许 user_id 为空,因为被邀请的人可能还没注册系统,我们先记录他的邮箱
user_id = Column( user_id = Column(
String(User.id_length()), String(AutoID.id_length()),
ForeignKey("users.id", ondelete="CASCADE"), ForeignKey("users.id", ondelete="CASCADE"),
nullable=True, nullable=True,
index=True, index=True,
+2
View File
@@ -7,8 +7,10 @@ requires-python = ">=3.14"
dependencies = [ dependencies = [
"alembic>=1.18.4", "alembic>=1.18.4",
"boto3>=1.43.9", "boto3>=1.43.9",
"dotenv>=0.9.9",
"fastapi>=0.136.1", "fastapi>=0.136.1",
"httpx>=0.28.1", "httpx>=0.28.1",
"jwt>=1.4.0",
"oss2>=2.19.1", "oss2>=2.19.1",
"psycopg2-binary>=2.9.12", "psycopg2-binary>=2.9.12",
"python-jose>=3.5.0", "python-jose>=3.5.0",
Generated
+36
View File
@@ -145,8 +145,10 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },
{ name = "boto3" }, { name = "boto3" },
{ name = "dotenv" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" }, { name = "httpx" },
{ name = "jwt" },
{ name = "oss2" }, { name = "oss2" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "python-jose" }, { name = "python-jose" },
@@ -160,8 +162,10 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = ">=1.18.4" }, { name = "alembic", specifier = ">=1.18.4" },
{ name = "boto3", specifier = ">=1.43.9" }, { name = "boto3", specifier = ">=1.43.9" },
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "fastapi", specifier = ">=0.136.1" }, { name = "fastapi", specifier = ">=0.136.1" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "jwt", specifier = ">=1.4.0" },
{ name = "oss2", specifier = ">=2.19.1" }, { name = "oss2", specifier = ">=2.19.1" },
{ name = "psycopg2-binary", specifier = ">=2.9.12" }, { name = "psycopg2-binary", specifier = ">=2.9.12" },
{ name = "python-jose", specifier = ">=3.5.0" }, { name = "python-jose", specifier = ">=3.5.0" },
@@ -304,6 +308,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
] ]
[[package]]
name = "dotenv"
version = "0.9.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dotenv" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
]
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.19.2" version = "0.19.2"
@@ -419,6 +434,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" }, { url = "https://files.pythonhosted.org/packages/07/cb/5f001272b6faeb23c1c9e0acc04d48eaaf5c862c17709d20e3469c6e0139/jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f", size = 24489, upload-time = "2020-05-12T22:03:45.643Z" },
] ]
[[package]]
name = "jwt"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/20/21254c9e601e6c29445d1e8854c2a81bdb554e07a82fb1f9846137a6965c/jwt-1.4.0.tar.gz", hash = "sha256:f6f789128ac247142c79ee10f3dba6e366ec4e77c9920d18c1592e28aa0a7952", size = 24911, upload-time = "2025-06-23T13:28:38.289Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/80/34e3fae850adb0b7b8b9b1cf02b2d975fcb68e0e8eb7d56d6b4fc23f7433/jwt-1.4.0-py3-none-any.whl", hash = "sha256:7560a7f1de4f90de94ac645ee0303ac60c95b9e08e058fb69f6c330f71d71b11", size = 18248, upload-time = "2025-06-23T13:28:37.012Z" },
]
[[package]] [[package]]
name = "mako" name = "mako"
version = "1.3.12" version = "1.3.12"
@@ -608,6 +635,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
] ]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]] [[package]]
name = "python-jose" name = "python-jose"
version = "3.5.0" version = "3.5.0"