浏览代码

set up architecture && modify model and table

conradlan 3 年之前
父节点
当前提交
59eeb4e6fa

+ 138 - 0
.gitignore

@@ -0,0 +1,138 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/

+ 1 - 0
alembic/README

@@ -0,0 +1 @@
+Generic single-database configuration.

+ 77 - 0
alembic/env.py

@@ -0,0 +1,77 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+
+    Calls to context.execute() here emit the given string to the
+    script output.
+
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        dialect_opts={"paramstyle": "named"},
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+
+    """
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection, target_metadata=target_metadata
+        )
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 24 - 0
alembic/script.py.mako

@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}

+ 62 - 0
alembic/versions/a509b6b8b5d9_creat_bank_benefitsharing.py

@@ -0,0 +1,62 @@
+"""creat Bank&BenefitSharing
+
+Revision ID: a509b6b8b5d9
+Revises: c18e926e95b7
+Create Date: 2021-11-25 13:57:15.704870
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import uuid
+import datetime
+
+from sqlalchemy.sql.schema import ForeignKey
+
+
+# revision identifiers, used by Alembic.
+revision = 'a509b6b8b5d9'
+down_revision = 'c18e926e95b7'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table(
+        'Bank',
+        sa.Column(
+            'id', sa.String(36), primary_key=True,
+            default=str(uuid.uuid4()), nullable=False),
+        sa.Column(
+            'created_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column(
+            'updated_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column('account', sa.String(50), nullable=False),
+        sa.Column(
+            'creator_id', sa.String(36),
+            ForeignKey('Creator.id', ondelete='CASCADE'), nullable=False)
+    )
+    op.create_table(
+        'BenefitSharing',
+        sa.Column(
+            'id', sa.String(36), primary_key=True,
+            default=str(uuid.uuid4()), nullable=False),
+        sa.Column(
+            'created_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column(
+            'updated_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column('amount', sa.Float(10), nullable=False),
+        sa.Column('is_paid', sa.Boolean, nullable=False, default=False),
+        sa.Column(
+            'bank_id', sa.String(36),
+            ForeignKey('Bank.id', ondelete='CASCADE'), nullable=False)
+    )
+
+
+def downgrade():
+    op.drop_table('Bank')
+    op.drop_table('BenefitSharing')
+    # pass

+ 68 - 0
alembic/versions/c18e926e95b7_fist_commit.py

@@ -0,0 +1,68 @@
+"""fist commit
+
+Revision ID: c18e926e95b7
+Revises:
+Create Date: 2021-11-25 11:40:39.389660
+
+"""
+from alembic import op
+import sqlalchemy as sa
+import uuid
+import datetime
+from sqlalchemy.dialects.postgresql.base import BIT
+from sqlalchemy.sql.schema import Column, ForeignKey, ForeignKeyConstraint
+from sqlalchemy.sql.sqltypes import String
+
+
+# revision identifiers, used by Alembic.
+revision = 'c18e926e95b7'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table(
+        'Link',
+        sa.Column(
+            'id', sa.String(36), primary_key=True, default=str(uuid.uuid4())),
+        sa.Column(
+            'created_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column(
+            'updated_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column('facebook', sa.String(200)),
+        sa.Column('instagram', sa.String(200)),
+        sa.Column('blog', sa.String(200)),
+        sa.Column('youtube', sa.String(200))
+    )
+    op.create_table(
+        'Creator',
+        sa.Column(
+            'id', sa.String(36), primary_key=True, default=str(uuid.uuid4())),
+        sa.Column('account', sa.String(50), nullable=False, unique=True),
+        sa.Column('pwd', sa.String(60), nullable=False),
+        sa.Column('phone', sa.String(10), nullable=False, unique=True),
+        sa.Column('email', sa.String(100), nullable=False, unique=True),
+        sa.Column('is_active', sa.Boolean, default=True, nullable=False),
+        sa.Column(
+            'created_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column(
+            'updated_at', sa.types.DateTime(timezone=True),
+            default=datetime.datetime.now(), nullable=False),
+        sa.Column(
+            'link_id', sa.String(36),
+            ForeignKey('Link.id', ondelete='CASCADE'), nullable=False),
+        sa.Column('nick_name', sa.String(50)),
+        sa.Column('brief_introduction', sa.TEXT),
+        sa.Column('work_experience', sa.TEXT),
+        sa.Column('case_type', sa.String(300))
+    )
+
+
+def downgrade():
+    op.drop_table('Creator')
+    op.drop_table('Link')
+    # pass

+ 0 - 0
app/__init__.py


+ 0 - 0
app/api/__init__.py


+ 0 - 0
app/api/api_v1/__init__.py


+ 9 - 0
app/api/api_v1/api.py

@@ -0,0 +1,9 @@
+from fastapi import APIRouter
+
+from app.api.api_v1.endpoints import items, login, users, utils
+
+api_router = APIRouter()
+api_router.include_router(login.router, tags=["login"])
+api_router.include_router(users.router, prefix="/users", tags=["users"])
+api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
+api_router.include_router(items.router, prefix="/items", tags=["items"])

+ 0 - 0
app/api/api_v1/endpoints/__init__.py


+ 99 - 0
app/api/api_v1/endpoints/items.py

@@ -0,0 +1,99 @@
+from typing import Any, List
+
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+
+from app import crud, models, schemas
+from app.api import deps
+
+router = APIRouter()
+
+
+@router.get("/", response_model=List[schemas.Item])
+def read_items(
+    db: Session = Depends(deps.get_db),
+    skip: int = 0,
+    limit: int = 100,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Retrieve items.
+    """
+    if crud.user.is_superuser(current_user):
+        items = crud.item.get_multi(db, skip=skip, limit=limit)
+    else:
+        items = crud.item.get_multi_by_owner(
+            db=db, owner_id=current_user.id, skip=skip, limit=limit
+        )
+    return items
+
+
+@router.post("/", response_model=schemas.Item)
+def create_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    item_in: schemas.ItemCreate,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Create new item.
+    """
+    item = crud.item.create_with_owner(db=db, obj_in=item_in, owner_id=current_user.id)
+    return item
+
+
+@router.put("/{id}", response_model=schemas.Item)
+def update_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    id: int,
+    item_in: schemas.ItemUpdate,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Update an item.
+    """
+    item = crud.item.get(db=db, id=id)
+    if not item:
+        raise HTTPException(status_code=404, detail="Item not found")
+    if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
+        raise HTTPException(status_code=400, detail="Not enough permissions")
+    item = crud.item.update(db=db, db_obj=item, obj_in=item_in)
+    return item
+
+
+@router.get("/{id}", response_model=schemas.Item)
+def read_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    id: int,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Get item by ID.
+    """
+    item = crud.item.get(db=db, id=id)
+    if not item:
+        raise HTTPException(status_code=404, detail="Item not found")
+    if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
+        raise HTTPException(status_code=400, detail="Not enough permissions")
+    return item
+
+
+@router.delete("/{id}", response_model=schemas.Item)
+def delete_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    id: int,
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Delete an item.
+    """
+    item = crud.item.get(db=db, id=id)
+    if not item:
+        raise HTTPException(status_code=404, detail="Item not found")
+    if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
+        raise HTTPException(status_code=400, detail="Not enough permissions")
+    item = crud.item.remove(db=db, id=id)
+    return item

+ 96 - 0
app/api/api_v1/endpoints/login.py

@@ -0,0 +1,96 @@
+from datetime import timedelta
+from typing import Any
+
+from fastapi import APIRouter, Body, Depends, HTTPException
+from fastapi.security import OAuth2PasswordRequestForm
+from sqlalchemy.orm import Session
+
+from app import crud, models, schemas
+from app.api import deps
+from app.core import security
+from app.core.config import settings
+from app.core.security import get_password_hash
+from app.utils import (
+    generate_password_reset_token,
+    send_reset_password_email,
+    verify_password_reset_token,
+)
+
+router = APIRouter()
+
+
+@router.post("/login/access-token", response_model=schemas.Token)
+def login_access_token(
+    db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends()
+) -> Any:
+    """
+    OAuth2 compatible token login, get an access token for future requests
+    """
+    user = crud.user.authenticate(
+        db, email=form_data.username, password=form_data.password
+    )
+    if not user:
+        raise HTTPException(status_code=400, detail="Incorrect email or password")
+    elif not crud.user.is_active(user):
+        raise HTTPException(status_code=400, detail="Inactive user")
+    access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+    return {
+        "access_token": security.create_access_token(
+            user.id, expires_delta=access_token_expires
+        ),
+        "token_type": "bearer",
+    }
+
+
+@router.post("/login/test-token", response_model=schemas.User)
+def test_token(current_user: models.User = Depends(deps.get_current_user)) -> Any:
+    """
+    Test access token
+    """
+    return current_user
+
+
+@router.post("/password-recovery/{email}", response_model=schemas.Msg)
+def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any:
+    """
+    Password Recovery
+    """
+    user = crud.user.get_by_email(db, email=email)
+
+    if not user:
+        raise HTTPException(
+            status_code=404,
+            detail="The user with this username does not exist in the system.",
+        )
+    password_reset_token = generate_password_reset_token(email=email)
+    send_reset_password_email(
+        email_to=user.email, email=email, token=password_reset_token
+    )
+    return {"msg": "Password recovery email sent"}
+
+
+@router.post("/reset-password/", response_model=schemas.Msg)
+def reset_password(
+    token: str = Body(...),
+    new_password: str = Body(...),
+    db: Session = Depends(deps.get_db),
+) -> Any:
+    """
+    Reset password
+    """
+    email = verify_password_reset_token(token)
+    if not email:
+        raise HTTPException(status_code=400, detail="Invalid token")
+    user = crud.user.get_by_email(db, email=email)
+    if not user:
+        raise HTTPException(
+            status_code=404,
+            detail="The user with this username does not exist in the system.",
+        )
+    elif not crud.user.is_active(user):
+        raise HTTPException(status_code=400, detail="Inactive user")
+    hashed_password = get_password_hash(new_password)
+    user.hashed_password = hashed_password
+    db.add(user)
+    db.commit()
+    return {"msg": "Password updated successfully"}

+ 153 - 0
app/api/api_v1/endpoints/users.py

@@ -0,0 +1,153 @@
+from typing import Any, List
+
+from fastapi import APIRouter, Body, Depends, HTTPException
+from fastapi.encoders import jsonable_encoder
+from pydantic.networks import EmailStr
+from sqlalchemy.orm import Session
+
+from app import crud, models, schemas
+from app.api import deps
+from app.core.config import settings
+from app.utils import send_new_account_email
+
+router = APIRouter()
+
+
+@router.get("/", response_model=List[schemas.User])
+def read_users(
+    db: Session = Depends(deps.get_db),
+    skip: int = 0,
+    limit: int = 100,
+    current_user: models.User = Depends(deps.get_current_active_superuser),
+) -> Any:
+    """
+    Retrieve users.
+    """
+    users = crud.user.get_multi(db, skip=skip, limit=limit)
+    return users
+
+
+@router.post("/", response_model=schemas.User)
+def create_user(
+    *,
+    db: Session = Depends(deps.get_db),
+    user_in: schemas.UserCreate,
+    current_user: models.User = Depends(deps.get_current_active_superuser),
+) -> Any:
+    """
+    Create new user.
+    """
+    user = crud.user.get_by_email(db, email=user_in.email)
+    if user:
+        raise HTTPException(
+            status_code=400,
+            detail="The user with this username already exists in the system.",
+        )
+    user = crud.user.create(db, obj_in=user_in)
+    if settings.EMAILS_ENABLED and user_in.email:
+        send_new_account_email(
+            email_to=user_in.email, username=user_in.email, password=user_in.password
+        )
+    return user
+
+
+@router.put("/me", response_model=schemas.User)
+def update_user_me(
+    *,
+    db: Session = Depends(deps.get_db),
+    password: str = Body(None),
+    full_name: str = Body(None),
+    email: EmailStr = Body(None),
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Update own user.
+    """
+    current_user_data = jsonable_encoder(current_user)
+    user_in = schemas.UserUpdate(**current_user_data)
+    if password is not None:
+        user_in.password = password
+    if full_name is not None:
+        user_in.full_name = full_name
+    if email is not None:
+        user_in.email = email
+    user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
+    return user
+
+
+@router.get("/me", response_model=schemas.User)
+def read_user_me(
+    db: Session = Depends(deps.get_db),
+    current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Get current user.
+    """
+    return current_user
+
+
+@router.post("/open", response_model=schemas.User)
+def create_user_open(
+    *,
+    db: Session = Depends(deps.get_db),
+    password: str = Body(...),
+    email: EmailStr = Body(...),
+    full_name: str = Body(None),
+) -> Any:
+    """
+    Create new user without the need to be logged in.
+    """
+    if not settings.USERS_OPEN_REGISTRATION:
+        raise HTTPException(
+            status_code=403,
+            detail="Open user registration is forbidden on this server",
+        )
+    user = crud.user.get_by_email(db, email=email)
+    if user:
+        raise HTTPException(
+            status_code=400,
+            detail="The user with this username already exists in the system",
+        )
+    user_in = schemas.UserCreate(password=password, email=email, full_name=full_name)
+    user = crud.user.create(db, obj_in=user_in)
+    return user
+
+
+@router.get("/{user_id}", response_model=schemas.User)
+def read_user_by_id(
+    user_id: int,
+    current_user: models.User = Depends(deps.get_current_active_user),
+    db: Session = Depends(deps.get_db),
+) -> Any:
+    """
+    Get a specific user by id.
+    """
+    user = crud.user.get(db, id=user_id)
+    if user == current_user:
+        return user
+    if not crud.user.is_superuser(current_user):
+        raise HTTPException(
+            status_code=400, detail="The user doesn't have enough privileges"
+        )
+    return user
+
+
+@router.put("/{user_id}", response_model=schemas.User)
+def update_user(
+    *,
+    db: Session = Depends(deps.get_db),
+    user_id: int,
+    user_in: schemas.UserUpdate,
+    current_user: models.User = Depends(deps.get_current_active_superuser),
+) -> Any:
+    """
+    Update a user.
+    """
+    user = crud.user.get(db, id=user_id)
+    if not user:
+        raise HTTPException(
+            status_code=404,
+            detail="The user with this username does not exist in the system",
+        )
+    user = crud.user.update(db, db_obj=user, obj_in=user_in)
+    return user

+ 35 - 0
app/api/api_v1/endpoints/utils.py

@@ -0,0 +1,35 @@
+from typing import Any
+
+from fastapi import APIRouter, Depends
+from pydantic.networks import EmailStr
+
+from app import models, schemas
+from app.api import deps
+from app.core.celery_app import celery_app
+from app.utils import send_test_email
+
+router = APIRouter()
+
+
+@router.post("/test-celery/", response_model=schemas.Msg, status_code=201)
+def test_celery(
+    msg: schemas.Msg,
+    current_user: models.User = Depends(deps.get_current_active_superuser),
+) -> Any:
+    """
+    Test Celery worker.
+    """
+    celery_app.send_task("app.worker.test_celery", args=[msg.msg])
+    return {"msg": "Word received"}
+
+
+@router.post("/test-email/", response_model=schemas.Msg, status_code=201)
+def test_email(
+    email_to: EmailStr,
+    current_user: models.User = Depends(deps.get_current_active_superuser),
+) -> Any:
+    """
+    Test emails.
+    """
+    send_test_email(email_to=email_to)
+    return {"msg": "Test email sent"}

+ 61 - 0
app/api/deps.py

@@ -0,0 +1,61 @@
+from typing import Generator
+
+from fastapi import Depends, HTTPException, status
+from fastapi.security import OAuth2PasswordBearer
+from jose import jwt
+from pydantic import ValidationError
+from sqlalchemy.orm import Session
+
+from app import crud, models, schemas
+from app.core import security
+from app.core.config import settings
+from app.db.session import SessionLocal
+
+reusable_oauth2 = OAuth2PasswordBearer(
+    tokenUrl=f"{settings.API_V1_STR}/login/access-token"
+)
+
+
+def get_db() -> Generator:
+    try:
+        db = SessionLocal()
+        yield db
+    finally:
+        db.close()
+
+
+def get_current_user(
+    db: Session = Depends(get_db), token: str = Depends(reusable_oauth2)
+) -> models.User:
+    try:
+        payload = jwt.decode(
+            token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
+        )
+        token_data = schemas.TokenPayload(**payload)
+    except (jwt.JWTError, ValidationError):
+        raise HTTPException(
+            status_code=status.HTTP_403_FORBIDDEN,
+            detail="Could not validate credentials",
+        )
+    user = crud.user.get(db, id=token_data.sub)
+    if not user:
+        raise HTTPException(status_code=404, detail="User not found")
+    return user
+
+
+def get_current_active_user(
+    current_user: models.User = Depends(get_current_user),
+) -> models.User:
+    if not crud.user.is_active(current_user):
+        raise HTTPException(status_code=400, detail="Inactive user")
+    return current_user
+
+
+def get_current_active_superuser(
+    current_user: models.User = Depends(get_current_user),
+) -> models.User:
+    if not crud.user.is_superuser(current_user):
+        raise HTTPException(
+            status_code=400, detail="The user doesn't have enough privileges"
+        )
+    return current_user

+ 0 - 0
app/core/__init__.py


+ 5 - 0
app/core/celery_app.py

@@ -0,0 +1,5 @@
+from celery import Celery
+
+celery_app = Celery("worker", broker="amqp://guest@queue//")
+
+celery_app.conf.task_routes = {"app.worker.test_celery": "main-queue"}

+ 89 - 0
app/core/config.py

@@ -0,0 +1,89 @@
+import secrets
+from typing import Any, Dict, List, Optional, Union
+
+from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator
+
+
+class Settings(BaseSettings):
+    API_V1_STR: str = "/api/v1"
+    SECRET_KEY: str = secrets.token_urlsafe(32)
+    # 60 minutes * 24 hours * 8 days = 8 days
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
+    SERVER_NAME: str = "api.ptt.cx"
+    SERVER_HOST: AnyHttpUrl = "api.ptt.cx"
+    # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
+    # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
+    # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
+    BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
+
+    @validator("BACKEND_CORS_ORIGINS", pre=True)
+    def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
+        if isinstance(v, str) and not v.startswith("["):
+            return [i.strip() for i in v.split(",")]
+        elif isinstance(v, (list, str)):
+            return v
+        raise ValueError(v)
+
+    PROJECT_NAME: str = "creator"
+    SENTRY_DSN: Optional[HttpUrl] = None
+
+    @validator("SENTRY_DSN", pre=True)
+    def sentry_dsn_can_be_blank(cls, v: str) -> Optional[str]:
+        if len(v) == 0:
+            return None
+        return v
+    
+    SQLALCHEMY_DATABASE_URI = "mysql+pymysql://choozmo:pAssw0rd@db.ptt.cx:3306/AI_anchor?charset=utf8mb4"
+    # POSTGRES_SERVER: str 
+    # POSTGRES_USER: str
+    # POSTGRES_PASSWORD: str
+    # POSTGRES_DB: str
+    # SQLALCHEMY_DATABASE_URI: Optional[PostgresDsn] = None
+    # @validator("SQLALCHEMY_DATABASE_URI", pre=True)
+    # def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
+    #     if isinstance(v, str):
+    #         return v
+    #     return PostgresDsn.build(
+    #         scheme="postgresql",
+    #         user=values.get("POSTGRES_USER"),
+    #         password=values.get("POSTGRES_PASSWORD"),
+    #         host=values.get("POSTGRES_SERVER"),
+    #         path=f"/{values.get('POSTGRES_DB') or ''}",
+    #     )
+
+    SMTP_TLS: bool = True
+    SMTP_PORT: Optional[int] = None
+    SMTP_HOST: Optional[str] = None
+    SMTP_USER: Optional[str] = None
+    SMTP_PASSWORD: Optional[str] = None
+    EMAILS_FROM_EMAIL: Optional[EmailStr] = None
+    EMAILS_FROM_NAME: Optional[str] = None
+
+    @validator("EMAILS_FROM_NAME")
+    def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
+        if not v:
+            return values["PROJECT_NAME"]
+        return v
+
+    EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
+    EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
+    EMAILS_ENABLED: bool = False
+
+    @validator("EMAILS_ENABLED", pre=True)
+    def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool:
+        return bool(
+            values.get("SMTP_HOST")
+            and values.get("SMTP_PORT")
+            and values.get("EMAILS_FROM_EMAIL")
+        )
+
+    EMAIL_TEST_USER: EmailStr = "test@example.com"  # type: ignore
+    FIRST_SUPERUSER: EmailStr
+    FIRST_SUPERUSER_PASSWORD: str
+    USERS_OPEN_REGISTRATION: bool = False
+
+    class Config:
+        case_sensitive = True
+
+
+settings = Settings()

+ 34 - 0
app/core/security.py

@@ -0,0 +1,34 @@
+from datetime import datetime, timedelta
+from typing import Any, Union
+
+from jose import jwt
+from passlib.context import CryptContext
+
+from app.core.config import settings
+
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+ALGORITHM = "HS256"
+
+
+def create_access_token(
+    subject: Union[str, Any], expires_delta: timedelta = None
+) -> str:
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(
+            minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
+        )
+    to_encode = {"exp": expire, "sub": str(subject)}
+    encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
+    return encoded_jwt
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+    return pwd_context.verify(plain_password, hashed_password)
+
+
+def get_password_hash(password: str) -> str:
+    return pwd_context.hash(password)

+ 10 - 0
app/crud/__init__.py

@@ -0,0 +1,10 @@
+from .crud_item import item
+from .crud_user import user
+
+# For a new basic set of CRUD operations you could just do
+
+# from .base import CRUDBase
+# from app.models.item import Item
+# from app.schemas.item import ItemCreate, ItemUpdate
+
+# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)

+ 66 - 0
app/crud/base.py

@@ -0,0 +1,66 @@
+from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
+
+from fastapi.encoders import jsonable_encoder
+from pydantic import BaseModel
+from sqlalchemy.orm import Session
+
+from app.db.base_class import Base
+
+ModelType = TypeVar("ModelType", bound=Base)
+CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
+UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
+
+
+class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
+    def __init__(self, model: Type[ModelType]):
+        """
+        CRUD object with default methods to Create, Read, Update, Delete (CRUD).
+
+        **Parameters**
+
+        * `model`: A SQLAlchemy model class
+        * `schema`: A Pydantic model (schema) class
+        """
+        self.model = model
+
+    def get(self, db: Session, id: Any) -> Optional[ModelType]:
+        return db.query(self.model).filter(self.model.id == id).first()
+
+    def get_multi(
+        self, db: Session, *, skip: int = 0, limit: int = 100
+    ) -> List[ModelType]:
+        return db.query(self.model).offset(skip).limit(limit).all()
+
+    def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
+        obj_in_data = jsonable_encoder(obj_in)
+        db_obj = self.model(**obj_in_data)  # type: ignore
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def update(
+        self,
+        db: Session,
+        *,
+        db_obj: ModelType,
+        obj_in: Union[UpdateSchemaType, Dict[str, Any]]
+    ) -> ModelType:
+        obj_data = jsonable_encoder(db_obj)
+        if isinstance(obj_in, dict):
+            update_data = obj_in
+        else:
+            update_data = obj_in.dict(exclude_unset=True)
+        for field in obj_data:
+            if field in update_data:
+                setattr(db_obj, field, update_data[field])
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def remove(self, db: Session, *, id: int) -> ModelType:
+        obj = db.query(self.model).get(id)
+        db.delete(obj)
+        db.commit()
+        return obj

+ 34 - 0
app/crud/crud_item.py

@@ -0,0 +1,34 @@
+from typing import List
+
+from fastapi.encoders import jsonable_encoder
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase
+from app.models.item import Item
+from app.schemas.item import ItemCreate, ItemUpdate
+
+
+class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
+    def create_with_owner(
+        self, db: Session, *, obj_in: ItemCreate, owner_id: int
+    ) -> Item:
+        obj_in_data = jsonable_encoder(obj_in)
+        db_obj = self.model(**obj_in_data, owner_id=owner_id)
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def get_multi_by_owner(
+        self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100
+    ) -> List[Item]:
+        return (
+            db.query(self.model)
+            .filter(Item.owner_id == owner_id)
+            .offset(skip)
+            .limit(limit)
+            .all()
+        )
+
+
+item = CRUDItem(Item)

+ 55 - 0
app/crud/crud_user.py

@@ -0,0 +1,55 @@
+from typing import Any, Dict, Optional, Union
+
+from sqlalchemy.orm import Session
+
+from app.core.security import get_password_hash, verify_password
+from app.crud.base import CRUDBase
+from app.models.user import User
+from app.schemas.user import UserCreate, UserUpdate
+
+
+class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
+    def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
+        return db.query(User).filter(User.email == email).first()
+
+    def create(self, db: Session, *, obj_in: UserCreate) -> User:
+        db_obj = User(
+            email=obj_in.email,
+            hashed_password=get_password_hash(obj_in.password),
+            full_name=obj_in.full_name,
+            is_superuser=obj_in.is_superuser,
+        )
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def update(
+        self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
+    ) -> User:
+        if isinstance(obj_in, dict):
+            update_data = obj_in
+        else:
+            update_data = obj_in.dict(exclude_unset=True)
+        if update_data["password"]:
+            hashed_password = get_password_hash(update_data["password"])
+            del update_data["password"]
+            update_data["hashed_password"] = hashed_password
+        return super().update(db, db_obj=db_obj, obj_in=update_data)
+
+    def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
+        user = self.get_by_email(db, email=email)
+        if not user:
+            return None
+        if not verify_password(password, user.hashed_password):
+            return None
+        return user
+
+    def is_active(self, user: User) -> bool:
+        return user.is_active
+
+    def is_superuser(self, user: User) -> bool:
+        return user.is_superuser
+
+
+user = CRUDUser(User)

+ 0 - 0
app/db/__init__.py


+ 7 - 0
app/db/base.py

@@ -0,0 +1,7 @@
+# Import all the models, so that Base has them before being
+# imported by Alembic
+from app.db.base_class import Base  # noqa
+from app.models.bank import Bank  # noqa
+from app.models.creator import Creator  # noqa
+from app.models.link import Link  # noqa
+from app.models.benefitsharing import BenefitSharing  # noqa

+ 13 - 0
app/db/base_class.py

@@ -0,0 +1,13 @@
+from typing import Any
+
+from sqlalchemy.ext.declarative import as_declarative, declared_attr
+
+
+@as_declarative()
+class Base:
+    id: Any
+    __name__: str
+    # Generate __tablename__ automatically
+    @declared_attr
+    def __tablename__(cls) -> str:
+        return cls.__name__.lower()

+ 25 - 0
app/db/init_db.py

@@ -0,0 +1,25 @@
+from sqlalchemy.orm import Session
+
+from app import crud, schemas
+from app.core.config import settings
+from app.db import base  # noqa: F401
+
+# make sure all SQL Alchemy models are imported (app.db.base) before initializing DB
+# otherwise, SQL Alchemy might fail to initialize relationships properly
+# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
+
+
+def init_db(db: Session) -> None:
+    # Tables should be created with Alembic migrations
+    # But if you don't want to use migrations, create
+    # the tables un-commenting the next line
+    # Base.metadata.create_all(bind=engine)
+
+    user = crud.user.get_by_email(db, email=settings.FIRST_SUPERUSER)
+    if not user:
+        user_in = schemas.UserCreate(
+            email=settings.FIRST_SUPERUSER,
+            password=settings.FIRST_SUPERUSER_PASSWORD,
+            is_superuser=True,
+        )
+        user = crud.user.create(db, obj_in=user_in)  # noqa: F841

+ 7 - 0
app/db/session.py

@@ -0,0 +1,7 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+from app.core.config import settings
+
+engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

+ 21 - 0
app/main.py

@@ -0,0 +1,21 @@
+from fastapi import FastAPI
+from starlette.middleware.cors import CORSMiddleware
+
+from app.api.api_v1.api import api_router
+from app.core.config import settings
+
+app = FastAPI(
+    title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json"
+)
+
+# Set all CORS enabled origins
+if settings.BACKEND_CORS_ORIGINS:
+    app.add_middleware(
+        CORSMiddleware,
+        allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
+        allow_credentials=True,
+        allow_methods=["*"],
+        allow_headers=["*"],
+    )
+
+app.include_router(api_router, prefix=settings.API_V1_STR)

+ 4 - 0
app/models/__init__.py

@@ -0,0 +1,4 @@
+from .bank import Bank # noqa
+from .creator import Creator    # noqa
+from .link import Link  # noqa
+from .benefitsharing import BenefitSharing  # noqa

+ 17 - 0
app/models/bank.py

@@ -0,0 +1,17 @@
+import datetime
+import uuid
+from sqlalchemy import Column, String
+from sqlalchemy.orm import relation
+from sqlalchemy.sql.schema import ForeignKey
+from sqlalchemy.sql.sqltypes import DateTime
+from app.db.base_class import Base
+
+
+class Bank(Base):
+    id = Column(String(36), primary_key=True, nullable=False, default=uuid.uuid4())
+    account = Column(String(100), unique=True, nullable=False)
+    created_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    updated_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    creator_id = Column(String(36), ForeignKey("Creator.id"), nullable=False)
+
+    BenifitSharing = relation("BenifitSharing")

+ 15 - 0
app/models/benefitsharing.py

@@ -0,0 +1,15 @@
+import datetime
+import uuid
+from sqlalchemy import Column, String
+from sqlalchemy.sql.schema import ForeignKey
+from sqlalchemy.sql.sqltypes import Boolean, DateTime, Float
+from app.db.base_class import Base
+
+
+class BenefitSharing(Base):
+    id = Column(String(36), primary_key=True, nullable=False, default=uuid.uuid4())
+    created_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    updated_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    bank_id = Column(String(36), ForeignKey("Bank.id"), nullable=False)
+    amount = Column(Float(), nullable=False)
+    is_paid = Column(Boolean, default=False, nullable=False)

+ 25 - 0
app/models/creator.py

@@ -0,0 +1,25 @@
+import datetime
+import uuid
+from sqlalchemy import Boolean, Column, String
+from sqlalchemy.orm import relation
+from sqlalchemy.sql.schema import ForeignKey
+from sqlalchemy.sql.sqltypes import DateTime, Text
+from app.db.base_class import Base
+
+
+class Creator(Base):
+    id = Column(String(36), primary_key=True, nullable=False, default=uuid.uuid4())
+    account = Column(String(50), unique=True, nullable=False)
+    pwd = Column(String(60), nullable=False)
+    phone = Column(String(10), nullable=False)
+    email = Column(String(100), unique=True, nullable=False)
+    is_active = Column(Boolean(), default=True)
+    created_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    updated_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    nick_name = Column(String(50))
+    brief_introduction = Column(Text())
+    work_experience = Column(Text())
+    case_type = Column(String(50))
+    link_id = Column(String(36), ForeignKey("Link.id"), nullable=False)
+
+    Bank = relation("Bank")

+ 19 - 0
app/models/link.py

@@ -0,0 +1,19 @@
+import datetime
+import uuid
+from sqlalchemy import Column, String
+from sqlalchemy.orm import relation
+from sqlalchemy.sql.sqltypes import DateTime
+
+from app.db.base_class import Base
+
+
+class Link(Base):
+    id = Column(String(36), primary_key=True, nullable=False, default=uuid.uuid4())
+    created_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    updated_at = Column(DateTime, default=datetime.datetime.now(), nullable=False)
+    facebook = Column(String(100))
+    instagram = Column(String(100))
+    blog = Column(String(100))
+    youtube = Column(String(100))
+
+    Creator = relation("Creator")

+ 4 - 0
app/schemas/__init__.py

@@ -0,0 +1,4 @@
+from .item import Item, ItemCreate, ItemInDB, ItemUpdate
+from .msg import Msg
+from .token import Token, TokenPayload
+from .user import User, UserCreate, UserInDB, UserUpdate

+ 39 - 0
app/schemas/item.py

@@ -0,0 +1,39 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+# Shared properties
+class ItemBase(BaseModel):
+    title: Optional[str] = None
+    description: Optional[str] = None
+
+
+# Properties to receive on item creation
+class ItemCreate(ItemBase):
+    title: str
+
+
+# Properties to receive on item update
+class ItemUpdate(ItemBase):
+    pass
+
+
+# Properties shared by models stored in DB
+class ItemInDBBase(ItemBase):
+    id: int
+    title: str
+    owner_id: int
+
+    class Config:
+        orm_mode = True
+
+
+# Properties to return to client
+class Item(ItemInDBBase):
+    pass
+
+
+# Properties properties stored in DB
+class ItemInDB(ItemInDBBase):
+    pass

+ 5 - 0
app/schemas/msg.py

@@ -0,0 +1,5 @@
+from pydantic import BaseModel
+
+
+class Msg(BaseModel):
+    msg: str

+ 12 - 0
app/schemas/token.py

@@ -0,0 +1,12 @@
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class Token(BaseModel):
+    access_token: str
+    token_type: str
+
+
+class TokenPayload(BaseModel):
+    sub: Optional[int] = None

+ 39 - 0
app/schemas/user.py

@@ -0,0 +1,39 @@
+from typing import Optional
+
+from pydantic import BaseModel, EmailStr
+
+
+# Shared properties
+class UserBase(BaseModel):
+    email: Optional[EmailStr] = None
+    is_active: Optional[bool] = True
+    is_superuser: bool = False
+    full_name: Optional[str] = None
+
+
+# Properties to receive via API on creation
+class UserCreate(UserBase):
+    email: EmailStr
+    password: str
+
+
+# Properties to receive via API on update
+class UserUpdate(UserBase):
+    password: Optional[str] = None
+
+
+class UserInDBBase(UserBase):
+    id: Optional[int] = None
+
+    class Config:
+        orm_mode = True
+
+
+# Additional properties to return via API
+class User(UserInDBBase):
+    pass
+
+
+# Additional properties stored in DB
+class UserInDB(UserInDBBase):
+    hashed_password: str

+ 106 - 0
app/utils.py

@@ -0,0 +1,106 @@
+import logging
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+import emails
+from emails.template import JinjaTemplate
+from jose import jwt
+
+from app.core.config import settings
+
+
+def send_email(
+    email_to: str,
+    subject_template: str = "",
+    html_template: str = "",
+    environment: Dict[str, Any] = {},
+) -> None:
+    assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
+    message = emails.Message(
+        subject=JinjaTemplate(subject_template),
+        html=JinjaTemplate(html_template),
+        mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
+    )
+    smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT}
+    if settings.SMTP_TLS:
+        smtp_options["tls"] = True
+    if settings.SMTP_USER:
+        smtp_options["user"] = settings.SMTP_USER
+    if settings.SMTP_PASSWORD:
+        smtp_options["password"] = settings.SMTP_PASSWORD
+    response = message.send(to=email_to, render=environment, smtp=smtp_options)
+    logging.info(f"send email result: {response}")
+
+
+def send_test_email(email_to: str) -> None:
+    project_name = settings.PROJECT_NAME
+    subject = f"{project_name} - Test email"
+    with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
+        template_str = f.read()
+    send_email(
+        email_to=email_to,
+        subject_template=subject,
+        html_template=template_str,
+        environment={"project_name": settings.PROJECT_NAME, "email": email_to},
+    )
+
+
+def send_reset_password_email(email_to: str, email: str, token: str) -> None:
+    project_name = settings.PROJECT_NAME
+    subject = f"{project_name} - Password recovery for user {email}"
+    with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
+        template_str = f.read()
+    server_host = settings.SERVER_HOST
+    link = f"{server_host}/reset-password?token={token}"
+    send_email(
+        email_to=email_to,
+        subject_template=subject,
+        html_template=template_str,
+        environment={
+            "project_name": settings.PROJECT_NAME,
+            "username": email,
+            "email": email_to,
+            "valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
+            "link": link,
+        },
+    )
+
+
+def send_new_account_email(email_to: str, username: str, password: str) -> None:
+    project_name = settings.PROJECT_NAME
+    subject = f"{project_name} - New account for user {username}"
+    with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
+        template_str = f.read()
+    link = settings.SERVER_HOST
+    send_email(
+        email_to=email_to,
+        subject_template=subject,
+        html_template=template_str,
+        environment={
+            "project_name": settings.PROJECT_NAME,
+            "username": username,
+            "password": password,
+            "email": email_to,
+            "link": link,
+        },
+    )
+
+
+def generate_password_reset_token(email: str) -> str:
+    delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
+    now = datetime.utcnow()
+    expires = now + delta
+    exp = expires.timestamp()
+    encoded_jwt = jwt.encode(
+        {"exp": exp, "nbf": now, "sub": email}, settings.SECRET_KEY, algorithm="HS256",
+    )
+    return encoded_jwt
+
+
+def verify_password_reset_token(token: str) -> Optional[str]:
+    try:
+        decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
+        return decoded_token["email"]
+    except jwt.JWTError:
+        return None

文件差异内容过多而无法显示
+ 41 - 0
test.vuerd.json


部分文件因为文件数量过多而无法显示