Forráskód Böngészése

linode smtp restrict

conradlan 3 éve
szülő
commit
930ca36d21

+ 3 - 0
.gitignore

@@ -136,3 +136,6 @@ dmypy.json
 
 # Cython debug symbols
 cython_debug/
+alembic.ini
+.flake8
+./app/core/config.py

+ 30 - 0
.vscode/launch.json

@@ -0,0 +1,30 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Python: Current File",
+            "type": "python",
+            "request": "launch",
+            "program": "${file}",
+            "console": "integratedTerminal"
+        },
+        {
+            "name": "Python: FastAPI",
+            "type": "python",
+            "request": "launch",
+            "module": "uvicorn",
+            "args": [
+                "app.main:app",
+                "--host",
+                "0.0.0.0",
+                "--port",
+                "8999",
+                "--reload"
+            ],
+            "jinja": true
+        }
+    ]
+}

+ 6 - 6
alembic/versions/a509b6b8b5d9_creat_bank_benefitsharing.py

@@ -22,7 +22,7 @@ depends_on = None
 
 def upgrade():
     op.create_table(
-        'Bank',
+        'bank',
         sa.Column(
             'id', sa.String(36), primary_key=True,
             default=str(uuid.uuid4()), nullable=False),
@@ -35,10 +35,10 @@ def upgrade():
         sa.Column('account', sa.String(50), nullable=False),
         sa.Column(
             'creator_id', sa.String(36),
-            ForeignKey('Creator.id', ondelete='CASCADE'), nullable=False)
+            ForeignKey('creator.id', ondelete='CASCADE'), nullable=False)
     )
     op.create_table(
-        'BenefitSharing',
+        'benefitsharing',
         sa.Column(
             'id', sa.String(36), primary_key=True,
             default=str(uuid.uuid4()), nullable=False),
@@ -52,11 +52,11 @@ def upgrade():
         sa.Column('is_paid', sa.Boolean, nullable=False, default=False),
         sa.Column(
             'bank_id', sa.String(36),
-            ForeignKey('Bank.id', ondelete='CASCADE'), nullable=False)
+            ForeignKey('bank.id', ondelete='CASCADE'), nullable=False)
     )
 
 
 def downgrade():
-    op.drop_table('Bank')
-    op.drop_table('BenefitSharing')
+    op.drop_table('benefitsharing')
+    op.drop_table('bank')
     # pass

+ 25 - 21
alembic/versions/c18e926e95b7_fist_commit.py

@@ -23,46 +23,50 @@ depends_on = None
 
 def upgrade():
     op.create_table(
-        'Link',
+        '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(20), 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),
+            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))
+            default=datetime.datetime.now(), 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))
     )
     op.create_table(
-        'Creator',
+        'link',
         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('facebook', sa.String(200)),
+        sa.Column('instagram', sa.String(200)),
+        sa.Column('blog', sa.String(200)),
+        sa.Column('youtube', sa.String(200)),
         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))
+            'creator_id', sa.String(36),
+            ForeignKey('creator.id', ondelete='CASCADE'), nullable=False
+        )
     )
 
 
+
 def downgrade():
-    op.drop_table('Creator')
-    op.drop_table('Link')
+    op.drop_table('link')
+    op.drop_table('creator')
     # pass

+ 7 - 3
app/api/api_v1/api.py

@@ -1,9 +1,13 @@
 from fastapi import APIRouter
 
-from app.api.api_v1.endpoints import items, login, users, utils
+from app.api.api_v1.endpoints import login, utils, creator
 
 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(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"])
+# api_router.include_router(items.router, prefix="/items", tags=["items"])
+api_router.include_router(creator.router, prefix="/creator", tags=["creator"])
+# api_router.include_router(link.router, prefix="/link", tags=["link"])
+# api_router.include_router(bank.router, prefix="/bank", tags=["bank"])
+# api_router.include_router(benefitsharing.router, prefix="/benefitsharing", tags=["benefitsharing"])

+ 140 - 0
app/api/api_v1/endpoints/creator.py

@@ -0,0 +1,140 @@
+from datetime import datetime
+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 sqlalchemy.sql import schema
+
+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.Creator])
+# def read_creators(
+#     db: Session = Depends(deps.get_db),
+#     skip: int = 0,
+#     limit: int = 100,
+#     current_creator: models.Creator = 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.Creator)
+# def create_creator(
+#     *,
+#     db: Session = Depends(deps.get_db),
+#     user_in: schemas.CreatorCreate,
+#     current_user: models.Creator = 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.Creator)
+def update_creator_me(
+    *,
+    db: Session = Depends(deps.get_db),
+    password: str = Body(None),
+    full_name: str = Body(None),
+    email: EmailStr = Body(None),
+    current_creator: models.Creator = Depends(deps.get_current_active_creator),
+) -> Any:
+    """
+    Update own user.
+    """
+    current_creator_data = jsonable_encoder(current_creator)
+    creator_in = schemas.CreatorUpdate(**current_creator_data)
+    if password is not None:
+        creator_in.password = password
+    if full_name is not None:
+        creator_in.full_name = full_name
+    if email is not None:
+        creator_in.email = email
+    creator = crud.creator.update(db, db_obj=current_creator, obj_in=creator_in)
+    return creator
+
+
+@router.get("/me", response_model=schemas.Creator)
+def read_creator_me(
+    db: Session = Depends(deps.get_db),
+    current_creator: models.Creator = Depends(deps.get_current_active_creator),
+) -> Any:
+    """
+    Get current user.
+    """
+    return current_creator
+
+
+@router.post("/open", response_model=schemas.Creator)
+def create_creator_open(
+    *,
+    db: Session = Depends(deps.get_db),
+    phone: str = Body(...),
+    password: str = Body(...),
+    email: EmailStr = Body(...),
+    account: str = Body(...),
+    nick_name: str = Body(None),
+    brief_introduction: str = Body(None),
+    work_exerience: str = Body(None),
+    case_type: 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",
+        )
+    creator = crud.creator.get_by_account(db, account=account)
+    if creator:
+        raise HTTPException(
+            status_code=400,
+            detail="The user with this username already exists in the system",
+        )
+    creator = crud.creator.get_by_email(db, email=email)
+    if creator:
+        raise HTTPException(
+            status_code=400,
+            detail="The user with this email already exists in the system",
+        )
+
+    try:
+        user_in = schemas.CreatorCreate(
+            account=account, phone=phone, email=email,
+            nick_name=nick_name, brief_introduction=brief_introduction,
+            work_exerience=work_exerience, case_type=case_type,
+            pwd=password
+        )
+    except ValueError:
+        raise HTTPException(
+            status_code=400,
+            detail='Please provide a valid mobile phone number',
+        )
+
+    creator = crud.creator.create(db, obj_in=user_in)
+
+    return creator

+ 16 - 16
app/api/api_v1/endpoints/login.py

@@ -26,28 +26,28 @@ def login_access_token(
     """
     OAuth2 compatible token login, get an access token for future requests
     """
-    user = crud.user.authenticate(
-        db, email=form_data.username, password=form_data.password
+    creator = crud.creator.authenticate(
+        db, account=form_data.username, password=form_data.password
     )
-    if not user:
+    if not creator:
         raise HTTPException(status_code=400, detail="Incorrect email or password")
-    elif not crud.user.is_active(user):
+    elif not crud.creator.is_active(creator):
         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
+            creator.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:
+@router.post("/login/test-token", response_model=schemas.Creator)
+def test_token(current_creator: models.Creator = Depends(deps.get_current_active_creator)) -> Any:
     """
     Test access token
     """
-    return current_user
+    return current_creator
 
 
 @router.post("/password-recovery/{email}", response_model=schemas.Msg)
@@ -55,16 +55,16 @@ def recover_password(email: str, db: Session = Depends(deps.get_db)) -> Any:
     """
     Password Recovery
     """
-    user = crud.user.get_by_email(db, email=email)
+    creator = crud.creator.get_by_email(db, email=email)
 
-    if not user:
+    if not creator:
         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
+        email_to=creator.email, email=email, token=password_reset_token
     )
     return {"msg": "Password recovery email sent"}
 
@@ -81,16 +81,16 @@ def 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:
+    creator = crud.creator.get_by_email(db, email=email)
+    if not creator:
         raise HTTPException(
             status_code=404,
             detail="The user with this username does not exist in the system.",
         )
-    elif not crud.user.is_active(user):
+    elif not crud.creator.is_active(creator):
         raise HTTPException(status_code=400, detail="Inactive user")
     hashed_password = get_password_hash(new_password)
-    user.hashed_password = hashed_password
-    db.add(user)
+    creator.pwd = hashed_password
+    db.add(creator)
     db.commit()
     return {"msg": "Password updated successfully"}

+ 20 - 20
app/api/api_v1/endpoints/utils.py

@@ -11,25 +11,25 @@ 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-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"}
+# @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"}

+ 18 - 18
app/api/deps.py

@@ -24,9 +24,9 @@ def get_db() -> Generator:
         db.close()
 
 
-def get_current_user(
+def get_current_creator(
     db: Session = Depends(get_db), token: str = Depends(reusable_oauth2)
-) -> models.User:
+) -> models.Creator:
     try:
         payload = jwt.decode(
             token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
@@ -37,25 +37,25 @@ def get_current_user(
             status_code=status.HTTP_403_FORBIDDEN,
             detail="Could not validate credentials",
         )
-    user = crud.user.get(db, id=token_data.sub)
-    if not user:
+    creator = crud.creator.get(db, id=token_data.sub)
+    if not creator:
         raise HTTPException(status_code=404, detail="User not found")
-    return user
+    return creator
 
 
-def get_current_active_user(
-    current_user: models.User = Depends(get_current_user),
-) -> models.User:
-    if not crud.user.is_active(current_user):
+def get_current_active_creator(
+    current_creator: models.Creator = Depends(get_current_creator),
+) -> models.Creator:
+    if not crud.creator.is_active(current_creator):
         raise HTTPException(status_code=400, detail="Inactive user")
-    return current_user
+    return current_creator
 
 
-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
+# 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

+ 16 - 15
app/core/config.py

@@ -10,7 +10,7 @@ class Settings(BaseSettings):
     # 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"
+    SERVER_HOST: AnyHttpUrl = "http://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"]'
@@ -25,14 +25,14 @@ class Settings(BaseSettings):
         raise ValueError(v)
 
     PROJECT_NAME: str = "creator"
-    SENTRY_DSN: Optional[HttpUrl] = None
+    SENTRY_DSN: Optional[HttpUrl] = ""
 
     @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
@@ -52,12 +52,12 @@ class Settings(BaseSettings):
     #     )
 
     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
+    SMTP_PORT: Optional[int] = 465
+    SMTP_HOST: Optional[str] = 'smtp.gmail.com'
+    SMTP_USER: Optional[str] = 'verify@choozmo.com'
+    SMTP_PASSWORD: Optional[str] = 'ckmspyijofyavuwg'
+    EMAILS_FROM_EMAIL: Optional[EmailStr] = 'verify@choozmo.com'
+    EMAILS_FROM_NAME: Optional[str] = 'Choozmo Team'
 
     @validator("EMAILS_FROM_NAME")
     def get_project_name(cls, v: Optional[str], values: Dict[str, Any]) -> str:
@@ -66,8 +66,9 @@ class Settings(BaseSettings):
         return v
 
     EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
-    EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
-    EMAILS_ENABLED: bool = False
+    EMAIL_TEMPLATES_DIR: str = "/home/conrad/creator/app/email-templates/build"
+    # EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
+    EMAILS_ENABLED: bool = True
 
     @validator("EMAILS_ENABLED", pre=True)
     def get_emails_enabled(cls, v: bool, values: Dict[str, Any]) -> bool:
@@ -77,10 +78,10 @@ class Settings(BaseSettings):
             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
+    EMAIL_TEST_USER: EmailStr = "conradlan@choozmo.com"  # type: ignore
+    FIRST_SUPERUSER: EmailStr = "conradlan@choozmo.com"
+    FIRST_SUPERUSER_PASSWORD: str = "test123"
+    USERS_OPEN_REGISTRATION: bool = True
 
     class Config:
         case_sensitive = True

+ 13 - 7
app/crud/__init__.py

@@ -1,10 +1,16 @@
-from .crud_item import item
-from .crud_user import user
-
+# from .crud_item import item
+# from .crud_user import user
+from .crud_creator import creator
 # 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
+from .base import CRUDBase
+from app.models.link import Link
+from app.schemas.link import LinkCreate, LinkUpdate
+from app.models.bank import Bank
+from app.schemas.bank import BankCreate, BankUpdate
+from app.models.benefitsharing import BenefitSharing
+from app.schemas.benefitsharing import BenefitSharingCreate, BenefitSharingUpdate
 
-# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
+link = CRUDBase[Link, LinkCreate, LinkUpdate](Link)
+bank = CRUDBase[Bank, BankCreate, BankUpdate](Bank)
+benefitsharing = CRUDBase[BenefitSharing, BenefitSharingCreate, BenefitSharingUpdate](BenefitSharing)

+ 64 - 0
app/crud/crud_creator.py

@@ -0,0 +1,64 @@
+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.creator import Creator
+from app.schemas.creator import CreatorCreate, CreatorUpdate
+
+
+class CRUDUser(CRUDBase[Creator, CreatorCreate, CreatorUpdate]):
+    def get_by_email(self, db: Session, *, email: str) -> Optional[Creator]:
+        return db.query(Creator).filter(Creator.email == email).first()
+
+    def get_by_account(self, db: Session, *, account: str) -> Optional[Creator]:
+        return db.query(Creator).filter(Creator.account == account).first()
+
+    def create(self, db: Session, *, obj_in: CreatorCreate) -> Creator:
+        db_obj = Creator(
+            email=obj_in.email,
+            account=obj_in.account,
+            phone=obj_in.phone,
+            is_active=obj_in.is_active,
+            nick_name=obj_in.nick_name,
+            brief_introduction=obj_in.brief_introduction,
+            work_experience=obj_in.work_experience,
+            case_type=obj_in.case_type,
+            pwd=get_password_hash(obj_in.pwd)
+        )
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def update(
+        self, db: Session, *, db_obj: Creator,
+        obj_in: Union[CreatorUpdate, Dict[str, Any]]
+    ) -> Creator:
+        if isinstance(obj_in, dict):
+            update_data = obj_in
+        else:
+            update_data = obj_in.dict(exclude_unset=True)
+        if update_data["pwd"]:
+            hashed_password = get_password_hash(update_data["pwd"])
+            del update_data["pwd"]
+            update_data["pwd"] = hashed_password
+        return super().update(db, db_obj=db_obj, obj_in=update_data)
+
+    def authenticate(self, db: Session, *, account: str, password: str) -> Optional[Creator]:
+        creator = self.get_by_account(db, account=account)
+        if not creator:
+            return None
+        if not verify_password(password, creator.pwd):
+            return None
+        return creator
+
+    def is_active(self, creator: Creator) -> bool:
+        return creator.is_active
+
+    # def is_superuser(self, user: User) -> bool:
+    #     return user.is_superuser
+
+
+creator = CRUDUser(Creator)

+ 0 - 34
app/crud/crud_item.py

@@ -1,34 +0,0 @@
-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)

+ 0 - 55
app/crud/crud_user.py

@@ -1,55 +0,0 @@
-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)

+ 1 - 0
app/db/base_class.py

@@ -8,6 +8,7 @@ class Base:
     id: Any
     __name__: str
     # Generate __tablename__ automatically
+
     @declared_attr
     def __tablename__(cls) -> str:
         return cls.__name__.lower()

+ 15 - 0
app/email-templates/src/new_account.mjml

@@ -0,0 +1,15 @@
+<mjml>
+  <mj-body background-color="#fff">
+    <mj-section>
+      <mj-column>
+        <mj-divider border-color="#555"></mj-divider>
+        <mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - New Account</mj-text>
+        <mj-text font-size="16px" color="#555">You have a new account:</mj-text>
+        <mj-text font-size="16px" color="#555">Username: {{ username }}</mj-text>
+        <mj-text font-size="16px" color="#555">Password: {{ password }}</mj-text>
+        <mj-button padding="50px 0px" href="{{ link }}">Go to Dashboard</mj-button>
+        <mj-divider border-color="#555" border-width="2px" />
+      </mj-column>
+    </mj-section>
+  </mj-body>
+</mjml>

+ 19 - 0
app/email-templates/src/reset_password.mjml

@@ -0,0 +1,19 @@
+<mjml>
+  <mj-body background-color="#fff">
+    <mj-section>
+      <mj-column>
+        <mj-divider border-color="#555"></mj-divider>
+        <mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }} - Password Recovery</mj-text>
+        <mj-text font-size="16px" color="#555">We received a request to recover the password for user {{ username }}
+          with email {{ email }}</mj-text>
+        <mj-text font-size="16px" color="#555">Reset your password by clicking the button below:</mj-text>
+        <mj-button padding="50px 0px" href="{{ link }}">Reset Password</mj-button>
+        <mj-text font-size="16px" color="#555">Or open the following link:</mj-text>
+        <mj-text font-size="16px" color="#555"><a href="{{ link }}">{{ link }}</a></mj-text>
+        <mj-divider border-color="#555" border-width="2px" />
+        <mj-text font-size="14px" color="#555">The reset password link / button will expire in {{ valid_hours }} hours.</mj-text>
+        <mj-text font-size="14px" color="#555">If you didn't request a password recovery you can disregard this email.</mj-text>
+      </mj-column>
+    </mj-section>
+  </mj-body>
+</mjml>

+ 11 - 0
app/email-templates/src/test_email.mjml

@@ -0,0 +1,11 @@
+<mjml>
+  <mj-body background-color="#fff">
+    <mj-section>
+      <mj-column>
+        <mj-divider border-color="#555"></mj-divider>
+        <mj-text font-size="20px" color="#555" font-family="helvetica">{{ project_name }}</mj-text>
+        <mj-text font-size="16px" color="#555">Test email for: {{ email }}</mj-text>
+      </mj-column>
+    </mj-section>
+  </mj-body>
+</mjml>

+ 3 - 3
app/models/bank.py

@@ -1,7 +1,7 @@
 import datetime
 import uuid
 from sqlalchemy import Column, String
-from sqlalchemy.orm import relation
+from sqlalchemy.orm import backref, relation, relationship
 from sqlalchemy.sql.schema import ForeignKey
 from sqlalchemy.sql.sqltypes import DateTime
 from app.db.base_class import Base
@@ -12,6 +12,6 @@ class Bank(Base):
     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)
+    creator_id = Column(String(36), ForeignKey("creator.id"), nullable=False)
 
-    BenifitSharing = relation("BenifitSharing")
+    benefitsharing = relation("BenefitSharing")

+ 2 - 1
app/models/benefitsharing.py

@@ -1,6 +1,7 @@
 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 Boolean, DateTime, Float
 from app.db.base_class import Base
@@ -10,6 +11,6 @@ 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)
+    bank_id = Column(String(36), ForeignKey("bank.id"), nullable=False)
     amount = Column(Float(), nullable=False)
     is_paid = Column(Boolean, default=False, nullable=False)

+ 3 - 3
app/models/creator.py

@@ -11,7 +11,7 @@ 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)
+    phone = Column(String(20), 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)
@@ -20,6 +20,6 @@ class Creator(Base):
     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")
+    link = relation("Link")
+    bank = relation("Bank")

+ 2 - 3
app/models/link.py

@@ -1,7 +1,7 @@
 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
@@ -15,5 +15,4 @@ class Link(Base):
     instagram = Column(String(100))
     blog = Column(String(100))
     youtube = Column(String(100))
-
-    Creator = relation("Creator")
+    creator_id = Column(String(36), ForeignKey("creator.id"), nullable=False)

+ 4 - 2
app/schemas/__init__.py

@@ -1,4 +1,6 @@
-from .item import Item, ItemCreate, ItemInDB, ItemUpdate
 from .msg import Msg
 from .token import Token, TokenPayload
-from .user import User, UserCreate, UserInDB, UserUpdate
+from .bank import Bank, BankCreate, BankInDB, BankUpdate
+from .creator import Creator, CreatorCreate, CreatorInDB, CreatorUpdate
+from .link import Link, LinkCreate, LinkInDB, LinkUpdate
+from .benefitsharing import BenefitSharing, BenefitSharingCreate, BenefitSharingInDB, BenefitSharingUpdate

+ 50 - 0
app/schemas/bank.py

@@ -0,0 +1,50 @@
+from typing import Optional
+from datetime import datetime
+import uuid
+from pydantic import BaseModel, validator
+
+from app.schemas.creator import Creator
+
+
+# Shared properties
+class BankBase(BaseModel):
+    account: str
+    created_at: datetime
+    update_at: datetime
+    creator_id: str
+
+    # @validator('bank')
+    # def check_bank(cls, v):
+    #     if v is None:
+    #         return v
+
+    #     if "https://www.facebook.com" not in v:
+    #         raise ValueError('Please provide a valid facebook link')
+    #     return v
+
+
+# Properties to receive via API on creation
+class BankCreate(BankBase):
+    pass
+
+
+# Properties to receive via API on update
+class BankUpdate(BankBase):
+    pass
+
+
+class BankInDBBase(BankBase):
+    id: Optional[str] = None
+
+    class Config:
+        orm_mode = True
+
+
+# Additional properties to return via API
+class Bank(BankInDBBase):
+    pass
+
+
+# Additional properties stored in DB
+class BankInDB(BankInDBBase):
+    pass

+ 52 - 0
app/schemas/benefitsharing.py

@@ -0,0 +1,52 @@
+from typing import Optional
+from datetime import datetime
+import uuid
+from pydantic import BaseModel, validator
+from app.schemas.bank import Bank
+
+from app.schemas.creator import Creator
+
+
+# Shared properties
+class BenefitSharingBase(BaseModel):
+    amount: float
+    created_at: datetime
+    update_at: datetime
+    bank_id: str
+    is_paid: bool
+
+    # @validator('bank')
+    # def check_bank(cls, v):
+    #     if v is None:
+    #         return v
+
+    #     if "https://www.facebook.com" not in v:
+    #         raise ValueError('Please provide a valid facebook link')
+    #     return v
+
+
+# Properties to receive via API on creation
+class BenefitSharingCreate(BenefitSharingBase):
+    pass
+
+
+# Properties to receive via API on update
+class BenefitSharingUpdate(BenefitSharingBase):
+    pass
+
+
+class BenefitSharingInDBBase(BenefitSharingBase):
+    id: Optional[str] = None
+
+    class Config:
+        orm_mode = True
+
+
+# Additional properties to return via API
+class BenefitSharing(BenefitSharingInDBBase):
+    pass
+
+
+# Additional properties stored in DB
+class BenefitSharingInDB(BenefitSharingInDBBase):
+    pass

+ 76 - 0
app/schemas/creator.py

@@ -0,0 +1,76 @@
+from datetime import datetime
+from typing import Optional
+import uuid
+from .link import Link
+from pydantic import BaseModel, EmailStr, validator
+from phonenumbers import (
+    NumberParseException,
+    PhoneNumberFormat,
+    PhoneNumberType,
+    format_number,
+    is_valid_number,
+    number_type,
+    parse as parse_phone_number,
+)
+
+MOBILE_NUMBER_TYPES = PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE
+
+
+# Shared properties
+class CreatorBase(BaseModel):
+    account: str
+    phone: str
+    email: EmailStr
+    is_active: Optional[bool] = True
+    # is_superuser: bool = False
+    nick_name: Optional[str] = None
+    brief_introduction: Optional[str] = None
+    work_experience: Optional[str] = None
+    case_type: Optional[str] = None
+
+    @validator('phone')
+    def check_phone(cls, v):
+        if v is None:
+            return v
+
+        try:
+            n = parse_phone_number(v, "TW")
+        except NumberParseException as e:
+            raise ValueError('Please provide a valid mobile phone number') from e
+
+        if not is_valid_number(n) or number_type(n) not in MOBILE_NUMBER_TYPES:
+            raise ValueError('Please provide a valid mobile phone number')
+
+        return format_number(
+            n, PhoneNumberFormat.NATIONAL if n.country_code == 886
+            else PhoneNumberFormat.INTERNATIONAL)
+
+    class Config:
+        orm_mode = True
+
+
+# Properties to receive via API on creation
+class CreatorCreate(CreatorBase):
+    pwd: str
+
+
+# Properties to receive via API on update
+class CreatorUpdate(CreatorBase):
+    pwd: Optional[str] = None
+
+
+class CreatorInDBBase(CreatorBase):
+    id: Optional[str] = None
+
+    class Config:
+        orm_mode = True
+
+
+# Additional properties to return via API
+class Creator(CreatorInDBBase):
+    pass
+
+
+# Additional properties stored in DB
+class CreatorInDB(CreatorInDBBase):
+    hashed_password: str

+ 0 - 39
app/schemas/item.py

@@ -1,39 +0,0 @@
-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

+ 69 - 0
app/schemas/link.py

@@ -0,0 +1,69 @@
+from datetime import datetime
+from typing import Optional
+import uuid
+
+from pydantic import BaseModel, validator
+
+
+# Shared properties
+class LinkBase(BaseModel):
+    facebook: Optional[str] = None
+    instagram: Optional[str] = None
+    blog: Optional[str] = None
+    youtube: Optional[str] = None
+    creator_id: str
+
+    @validator('facebook')
+    def check_facebook(cls, v):
+        if v is None:
+            return v
+
+        if "https://www.facebook.com" not in v:
+            raise ValueError('Please provide a valid facebook link')
+        return v
+
+    @validator('instagram')
+    def check_instagram(cls, v):
+        if v is None:
+            return v
+
+        if "https://www.instagram.com" not in v:
+            raise ValueError('Please provide a valid instagram link')
+        return v
+
+    @validator('youtube')
+    def check_youtube(cls, v):
+        if v is None:
+            return v
+
+        if "https://www.youtube.com" not in v:
+            raise ValueError('Please provide a valid youtube link')
+        return v
+
+
+# Properties to receive via API on creation
+class LinkCreate(LinkBase):
+    pass
+
+
+# Properties to receive via API on update
+class LinkUpdate(LinkBase):
+    update_at: datetime
+    pass
+
+
+class LinkInDBBase(LinkBase):
+    id: Optional[str] = None
+
+    class Config:
+        orm_mode = True
+
+
+# Additional properties to return via API
+class Link(LinkInDBBase):
+    pass
+
+
+# Additional properties stored in DB
+class LinkInDB(LinkInDBBase):
+    pass

+ 0 - 39
app/schemas/user.py

@@ -1,39 +0,0 @@
-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

+ 12 - 12
test.vuerd.json

@@ -3,9 +3,9 @@
     "version": "2.2.10",
     "width": 2000,
     "height": 2000,
-    "scrollTop": -588.8000000000008,
-    "scrollLeft": -436,
-    "zoomLevel": 0.9,
+    "scrollTop": -710.8,
+    "scrollLeft": -221.60625000000027,
+    "zoomLevel": 1,
     "show": {
       "tableComment": true,
       "columnComment": true,
@@ -352,7 +352,7 @@
           "active": false,
           "left": 971.6899,
           "top": 487.0662,
-          "zIndex": 2,
+          "zIndex": 5,
           "widthName": 60,
           "widthComment": 60,
           "color": "#D52B2B"
@@ -528,9 +528,9 @@
         ],
         "ui": {
           "active": false,
-          "left": 1564.0784,
-          "top": 556.3231,
-          "zIndex": 4,
+          "left": 1578.0784,
+          "top": 562.3231,
+          "zIndex": 6,
           "widthName": 60,
           "widthComment": 60
         },
@@ -769,7 +769,7 @@
           "active": false,
           "left": 384.5478,
           "top": 580.3948,
-          "zIndex": 5,
+          "zIndex": 3,
           "widthName": 60,
           "widthComment": 60
         },
@@ -923,7 +923,7 @@
           "active": false,
           "left": 387.3308,
           "top": 913.0695,
-          "zIndex": 7,
+          "zIndex": 15,
           "widthName": 87.28076171875,
           "widthComment": 60
         },
@@ -1054,7 +1054,7 @@
           "active": false,
           "left": 996.6859,
           "top": 216.9885,
-          "zIndex": 3,
+          "zIndex": 2,
           "widthName": 60,
           "widthComment": 60
         },
@@ -1104,8 +1104,8 @@
           "columnIds": [
             "0413e51b-270c-47e9-a743-6c268bcd70b0"
           ],
-          "x": 1564.0784,
-          "y": 662.5731,
+          "x": 1578.0784,
+          "y": 668.5731,
           "direction": "left"
         },
         "end": {