conradlan il y a 3 ans
commit
e85509ddf6
46 fichiers modifiés avec 1823 ajouts et 0 suppressions
  1. 0 0
      README.md
  2. 0 0
      app/__init__.py
  3. 0 0
      app/api/__init__.py
  4. 0 0
      app/api/api_v1/__init__.py
  5. 8 0
      app/api/api_v1/api.py
  6. 0 0
      app/api/api_v1/endpoints/__init__.py
  7. 76 0
      app/api/api_v1/endpoints/login.py
  8. 99 0
      app/api/api_v1/endpoints/nft.py
  9. 152 0
      app/api/api_v1/endpoints/users.py
  10. 61 0
      app/api/deps.py
  11. 0 0
      app/core/__init__.py
  12. 5 0
      app/core/celery_app.py
  13. 90 0
      app/core/config.py
  14. 34 0
      app/core/security.py
  15. 5 0
      app/crud/__init__.py
  16. 66 0
      app/crud/base.py
  17. 34 0
      app/crud/crud_nft.py
  18. 64 0
      app/crud/crud_user.py
  19. 0 0
      app/db/__init__.py
  20. 5 0
      app/db/base.py
  21. 14 0
      app/db/base_class.py
  22. 25 0
      app/db/init_db.py
  23. 7 0
      app/db/session.py
  24. 15 0
      app/email-templates/src/new_account.mjml
  25. 19 0
      app/email-templates/src/reset_password.mjml
  26. 11 0
      app/email-templates/src/test_email.mjml
  27. 21 0
      app/main.py
  28. 2 0
      app/models/__init__.py
  29. 14 0
      app/models/nft.py
  30. 17 0
      app/models/user.py
  31. 4 0
      app/schemas/__init__.py
  32. 5 0
      app/schemas/msg.py
  33. 24 0
      app/schemas/nft.py
  34. 12 0
      app/schemas/token.py
  35. 25 0
      app/schemas/user.py
  36. 106 0
      app/utils.py
  37. 197 0
      ark.vuerd.json
  38. 1 0
      ark_table/README
  39. 77 0
      ark_table/env.py
  40. 24 0
      ark_table/script.py.mako
  41. 29 0
      ark_table/versions/1baf7a0c6aab_alert_table.py
  42. 28 0
      ark_table/versions/51dfa236bcf1_add_superuser.py
  43. 59 0
      ark_table/versions/e33bf2621069_alert_table.py
  44. 44 0
      requirement.txt
  45. 303 0
      requirements.txt
  46. 41 0
      test.vuerd.json

+ 0 - 0
README.md


+ 0 - 0
app/__init__.py


+ 0 - 0
app/api/__init__.py


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


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

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

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


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

@@ -0,0 +1,76 @@
+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, account=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.UserCreate)
+def test_token(current_user: models.users = Depends(deps.get_current_active_user)) -> Any:
+    """
+    Test access token
+    """
+    return current_user
+
+@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"}

+ 99 - 0
app/api/api_v1/endpoints/nft.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.NftBase])
+def read_items(
+    db: Session = Depends(deps.get_db),
+    skip: int = 0,
+    limit: int = 100,
+    current_user: models.users = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Retrieve items.
+    """
+    if crud.user.is_superuser(current_user):
+        nfts = crud.nft.get_multi(db, skip=skip, limit=limit)
+    else:
+        nfts = crud.nft.get_multi_by_owner(
+            db=db, owner_id=current_user.userid, skip=skip, limit=limit
+        )
+    return nfts
+
+
+@router.post("/", response_model=schemas.NftBase)
+def create_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    item_in: schemas.NftCreate,
+    current_user: models.users = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Create new item.
+    """
+    nft = crud.nft.create_with_owner(db=db, obj_in=item_in, owner_id=current_user.userid)
+    return nft
+
+
+@router.put("/{id}", response_model=schemas.NftCreate)
+def update_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    id: int,
+    item_in: schemas.NftUpdate,
+    current_user: models.users = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Update an item.
+    """
+    nft = crud.nft.get(db=db, id=id)
+    if not nft:
+        raise HTTPException(status_code=404, detail="Item not found")
+    if not crud.user.is_superuser(current_user) and (nft.userid != current_user.userid):
+        raise HTTPException(status_code=400, detail="Not enough permissions")
+    nft = crud.nft.update(db=db, db_obj=nft, obj_in=item_in)
+    return nft
+
+
+@router.get("/{id}", response_model=schemas.NftCreate)
+def read_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    id: int,
+    current_user: models.users = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Get item by ID.
+    """
+    nft = crud.nft.get(db=db, id=id)
+    if not nft:
+        raise HTTPException(status_code=404, detail="Item not found")
+    if not crud.user.is_superuser(current_user) and (nft.userid != current_user.userid):
+        raise HTTPException(status_code=400, detail="Not enough permissions")
+    return nft
+
+
+@router.delete("/{id}", response_model=schemas.NftCreate)
+def delete_item(
+    *,
+    db: Session = Depends(deps.get_db),
+    id: int,
+    current_user: models.users = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Delete an item.
+    """
+    nft = crud.nft.get(db=db, id=id)
+    if not nft:
+        raise HTTPException(status_code=404, detail="Item not found")
+    if not crud.user.is_superuser(current_user) and (nft.userid != current_user.userid):
+        raise HTTPException(status_code=400, detail="Not enough permissions")
+    nft = crud.nft.remove(db=db, id=id)
+    return nft

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

@@ -0,0 +1,152 @@
+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
+
+router = APIRouter()
+
+
+@router.get("/")
+def read_users(
+    db: Session = Depends(deps.get_db),
+    skip: int = 0,
+    limit: int = 100,
+    current_user: models.users = Depends(deps.get_current_active_superuser),
+) -> Any:
+    """
+    Retrieve users.
+    """
+    users = crud.user.get_multi(db, skip=skip, limit=limit)
+    return users
+
+
+@router.post("/")
+def create_user(
+    *,
+    db: Session = Depends(deps.get_db),
+    user_in: schemas.UserCreate,
+    current_user: models.users = 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)
+    return user
+
+
+@router.put("/me")
+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.users = 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.hashed_password = password
+    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")
+def read_user_me(
+    db: Session = Depends(deps.get_db),
+    current_user: models.users = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    Get current user.
+    """
+    return current_user
+
+
+@router.post("/open")
+def create_user_open(
+    *,
+    db: Session = Depends(deps.get_db),
+    password: str = Body(...),
+    email: EmailStr = Body(...),
+    account: str = Body(...),
+) -> 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 email already exists in the system",
+        )
+    user = crud.user.get_by_account(db, account=account)
+    if user:
+        raise HTTPException(
+            status_code=400,
+            detail="The user with this account already exists in the system",
+        )
+    user_in = schemas.UserCreate(hashed_password=password, email=email, account=account)
+    user = crud.user.create(db, obj_in=user_in)
+    return user
+
+
+@router.get("/{user_id}")
+def read_user_by_id(
+    user_id: int,
+    current_user: models.users = 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}")
+def update_user(
+    *,
+    db: Session = Depends(deps.get_db),
+    user_id: int,
+    user_in: schemas.UserUpdate,
+    current_user: models.users = 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

+ 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.users:
+    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.users = Depends(get_current_user),
+) -> models.users:
+    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.users = Depends(get_current_user),
+) -> models.users:
+    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"}

+ 90 - 0
app/core/config.py

@@ -0,0 +1,90 @@
+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 = "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"]'
+    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] = ""
+
+    @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/arkcard?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] = 465
+    SMTP_HOST: Optional[str] = 'smtp.gmail.com'
+    SMTP_USER: Optional[str] = ''
+    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:
+        if not v:
+            return values["PROJECT_NAME"]
+        return v
+
+    EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
+    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:
+        return bool(
+            values.get("SMTP_HOST")
+            and values.get("SMTP_PORT")
+            and values.get("EMAILS_FROM_EMAIL")
+        )
+
+    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
+
+
+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)

+ 5 - 0
app/crud/__init__.py

@@ -0,0 +1,5 @@
+# from .crud_item import item
+# from .crud_user import user
+from .crud_user import user
+from .crud_nft import nft
+# For a new basic set of CRUD operations you could just do

+ 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_nft.py

@@ -0,0 +1,34 @@
+from typing import Any, Dict, List, 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.nft import nft
+from app.schemas.nft import NftBase, NftCreate, NftUpdate
+
+
+class CRUDUser(CRUDBase[nft, NftBase, NftCreate]):
+    def get_multi_by_owner(
+        self, db: Session, *, skip: int = 0, limit: int = 100, owner_id: str
+    ) -> List[nft]:
+        return db.query(nft).filter(nft.userid==owner_id).offset(skip).limit(limit).all()
+
+    def create_with_owner(self, db: Session, *, obj_in: NftCreate, owner_id:str) -> nft:
+        db_obj = nft(
+            hash=obj_in.hash,
+            imgurl=obj_in.imgurl,
+            userid=owner_id,
+            title=obj_in.title,
+            context=obj_in.context,
+            is_active=obj_in.is_active,
+            category=obj_in.category
+        )
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+
+
+nft = CRUDUser(nft)

+ 64 - 0
app/crud/crud_user.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.user import users
+from app.schemas.user import UserBase, UserCreate, UserUpdate
+
+
+class CRUDUser(CRUDBase[users, UserBase, UserCreate]):
+    def get_by_email(self, db: Session, *, email: str) -> Optional[users]:
+        return db.query(users).filter(users.email == email).first()
+
+    def get_by_account(self, db: Session, *, account: str) -> Optional[users]:
+        return db.query(users).filter(users.account == account).first()
+
+    def create(self, db: Session, *, obj_in: UserCreate) -> users:
+        db_obj = users(
+            email=obj_in.email,
+            userid=obj_in.userid,
+            useraddress=obj_in.useraddress,
+            is_superuser=obj_in.is_superuser,
+            account=obj_in.account,
+            is_active=obj_in.is_active,
+            hashed_password=get_password_hash(obj_in.hashed_password)
+        )
+        db.add(db_obj)
+        db.commit()
+        db.refresh(db_obj)
+        return db_obj
+
+    def update(
+        self, db: Session, *, db_obj: users,
+        obj_in: Union[UserUpdate, Dict[str, Any]]
+    ) -> users:
+        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["password"] = hashed_password
+        return super().update(db, db_obj=db_obj, obj_in=update_data)
+
+    def authenticate(
+        self, db: Session, *, account: str, password: str
+    ) -> Optional[users]:
+        user = self.get_by_account(db, account=account)
+        if not user:
+            return None
+        if not verify_password(password, user.hashed_password):
+            return None
+        return user
+
+    def is_active(self, user: users) -> bool:
+        return user.is_active
+
+    def is_superuser(self, user: users) -> bool:
+        return user.is_superuser
+
+
+user = CRUDUser(users)

+ 0 - 0
app/db/__init__.py


+ 5 - 0
app/db/base.py

@@ -0,0 +1,5 @@
+# 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.user import users  # noqa
+from app.models.nft import nft  # noqa

+ 14 - 0
app/db/base_class.py

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

+ 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>

+ 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)

+ 2 - 0
app/models/__init__.py

@@ -0,0 +1,2 @@
+from .user import users
+from .nft import nft

+ 14 - 0
app/models/nft.py

@@ -0,0 +1,14 @@
+from sqlalchemy import Boolean, Column, String
+from sqlalchemy.sql.sqltypes import Integer
+from app.db.base_class import Base
+
+
+class nft(Base):
+    id = Column(Integer, primary_key=True, nullable=False)
+    hash = Column(String(200), unique=True)
+    imgurl = Column(String(200))
+    userid = Column(String(200))
+    title = Column(String(200))
+    context = Column(String(200))
+    is_actived = Column(Boolean(), default=True)
+    category = Column(String(100))

+ 17 - 0
app/models/user.py

@@ -0,0 +1,17 @@
+import datetime
+from sqlalchemy import Boolean, Column, String
+from sqlalchemy.sql.sqltypes import DateTime, Integer
+from app.db.base_class import Base
+
+
+class users(Base):
+    id = Column(Integer, primary_key=True, nullable=False)
+    userid = Column(String(200), unique=True)
+    useraddress = Column(String(200), unique=True)
+    email = Column(String(100), unique=True, nullable=False)
+    hashed_password = Column(String(100))
+    account = Column(String(100))
+    is_active = Column(Boolean(), default=True)
+    is_superuser = Column(Boolean(), default=False)
+    created_at = Column(DateTime, default=datetime.datetime.now, nullable=False)
+    updated_at = Column(DateTime, default=datetime.datetime.now, nullable=False)

+ 4 - 0
app/schemas/__init__.py

@@ -0,0 +1,4 @@
+from .msg import Msg
+from .token import Token, TokenPayload
+from .user import *
+from .nft import *

+ 5 - 0
app/schemas/msg.py

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

+ 24 - 0
app/schemas/nft.py

@@ -0,0 +1,24 @@
+from typing import Optional
+from pydantic import BaseModel
+
+
+# Shared properties
+class NftBase(BaseModel):
+    hash: Optional[str] = None
+    imgurl: Optional[str] = None
+    userid: Optional[str] = True
+    title: Optional[str] = None
+    context: Optional[str] = None
+    is_active: Optional[bool] = True
+    category: Optional[str] = None
+
+    class Config:
+        orm_mode = True
+
+
+class NftCreate(NftBase):
+    pass
+
+
+class NftUpdate(NftBase):
+    pass

+ 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

+ 25 - 0
app/schemas/user.py

@@ -0,0 +1,25 @@
+from typing import Optional
+from pydantic import BaseModel, EmailStr
+
+
+# Shared properties
+class UserBase(BaseModel):
+    userid: Optional[str] = None
+    useraddress: Optional[str] = None
+    email: EmailStr
+    is_active: Optional[bool] = True
+    is_superuser: bool = False
+    account: Optional[str] = None
+
+
+# Properties to receive via API on creation
+class UserCreate(UserBase):
+    hashed_password: str
+
+
+class UserUpdate(UserCreate):
+    pass
+
+
+class User(UserCreate):
+    id: Optional[int] = None

+ 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

+ 197 - 0
ark.vuerd.json

@@ -0,0 +1,197 @@
+{
+  "canvas": {
+    "version": "2.2.10",
+    "width": 2000,
+    "height": 2000,
+    "scrollTop": -444,
+    "scrollLeft": -174,
+    "zoomLevel": 0.7,
+    "show": {
+      "tableComment": true,
+      "columnComment": true,
+      "columnDataType": true,
+      "columnDefault": true,
+      "columnAutoIncrement": false,
+      "columnPrimaryKey": true,
+      "columnUnique": false,
+      "columnNotNull": true,
+      "relationship": true
+    },
+    "database": "MySQL",
+    "databaseName": "ark",
+    "canvasType": "ERD",
+    "language": "GraphQL",
+    "tableCase": "pascalCase",
+    "columnCase": "camelCase",
+    "highlightTheme": "VS2015",
+    "bracketType": "none",
+    "setting": {
+      "relationshipDataTypeSync": true,
+      "relationshipOptimization": false,
+      "columnOrder": [
+        "columnName",
+        "columnDataType",
+        "columnNotNull",
+        "columnUnique",
+        "columnAutoIncrement",
+        "columnDefault",
+        "columnComment"
+      ]
+    },
+    "pluginSerializationMap": {}
+  },
+  "table": {
+    "tables": [
+      {
+        "name": "User",
+        "comment": "",
+        "columns": [
+          {
+            "name": "",
+            "comment": "",
+            "dataType": "",
+            "default": "",
+            "option": {
+              "autoIncrement": false,
+              "primaryKey": false,
+              "unique": false,
+              "notNull": false
+            },
+            "ui": {
+              "active": false,
+              "pk": false,
+              "fk": false,
+              "pfk": false,
+              "widthName": 60,
+              "widthComment": 60,
+              "widthDataType": 60,
+              "widthDefault": 60
+            },
+            "id": "ba33c802-efd0-44f6-8df6-76b2c11c4aad"
+          },
+          {
+            "name": "",
+            "comment": "",
+            "dataType": "",
+            "default": "",
+            "option": {
+              "autoIncrement": false,
+              "primaryKey": true,
+              "unique": false,
+              "notNull": true
+            },
+            "ui": {
+              "active": false,
+              "pk": true,
+              "fk": false,
+              "pfk": false,
+              "widthName": 60,
+              "widthComment": 60,
+              "widthDataType": 60,
+              "widthDefault": 60
+            },
+            "id": "eeecd26e-53cb-487d-9d72-8a325a8f987a"
+          }
+        ],
+        "ui": {
+          "active": false,
+          "left": 293.2142,
+          "top": 276.4289,
+          "zIndex": 102,
+          "widthName": 60,
+          "widthComment": 60
+        },
+        "visible": true,
+        "id": "d64990cb-fc7a-4a0d-962f-bfd85f9541da"
+      },
+      {
+        "name": "Nft",
+        "comment": "",
+        "columns": [
+          {
+            "name": "",
+            "comment": "",
+            "dataType": "",
+            "default": "",
+            "option": {
+              "autoIncrement": false,
+              "primaryKey": false,
+              "unique": false,
+              "notNull": true
+            },
+            "ui": {
+              "active": false,
+              "pk": false,
+              "fk": true,
+              "pfk": false,
+              "widthName": 60,
+              "widthComment": 60,
+              "widthDataType": 60,
+              "widthDefault": 60
+            },
+            "id": "23a86d39-075c-44d3-b92c-4897df34e960"
+          }
+        ],
+        "ui": {
+          "active": false,
+          "left": 289.8335,
+          "top": 685.1667,
+          "zIndex": 88,
+          "widthName": 60,
+          "widthComment": 60
+        },
+        "visible": true,
+        "id": "244bd25d-605c-447a-9033-b7de8e258ed1"
+      },
+      {
+        "name": "Log",
+        "comment": "",
+        "columns": [],
+        "ui": {
+          "active": false,
+          "left": 834.071,
+          "top": 0,
+          "zIndex": 103,
+          "widthName": 60,
+          "widthComment": 60
+        },
+        "visible": true,
+        "id": "a6ae301a-0524-4316-bb7f-d3d397abb23f"
+      }
+    ],
+    "indexes": []
+  },
+  "memo": {
+    "memos": []
+  },
+  "relationship": {
+    "relationships": [
+      {
+        "identification": false,
+        "relationshipType": "OneN",
+        "startRelationshipType": "Dash",
+        "start": {
+          "tableId": "d64990cb-fc7a-4a0d-962f-bfd85f9541da",
+          "columnIds": [
+            "eeecd26e-53cb-487d-9d72-8a325a8f987a"
+          ],
+          "x": 466.7142,
+          "y": 386.4289,
+          "direction": "bottom"
+        },
+        "end": {
+          "tableId": "244bd25d-605c-447a-9033-b7de8e258ed1",
+          "columnIds": [
+            "23a86d39-075c-44d3-b92c-4897df34e960"
+          ],
+          "x": 463.3335,
+          "y": 685.1667,
+          "direction": "top"
+        },
+        "constraintName": "fk_user_to_nft",
+        "visible": true,
+        "id": "f5caeb49-3984-4cc2-86d1-1edd7f3aba0f"
+      }
+    ]
+  }
+}

+ 1 - 0
ark_table/README

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

+ 77 - 0
ark_table/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
ark_table/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"}

+ 29 - 0
ark_table/versions/1baf7a0c6aab_alert_table.py

@@ -0,0 +1,29 @@
+"""alert table 
+
+Revision ID: 1baf7a0c6aab
+Revises: e33bf2621069
+Create Date: 2021-12-13 16:28:02.806559
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '1baf7a0c6aab'
+down_revision = 'e33bf2621069'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column('nft',
+        sa.Column('category', sa.String(10))
+    )
+    op.add_column('nft',
+        sa.Column('is_actived', sa.Boolean(), default=False, nullable=False)
+    )
+
+def downgrade():
+    op.drop_column('nft', 'category')
+    op.drop_column('nft', 'is_actived')

+ 28 - 0
ark_table/versions/51dfa236bcf1_add_superuser.py

@@ -0,0 +1,28 @@
+"""add superuser
+
+Revision ID: 51dfa236bcf1
+Revises: 1baf7a0c6aab
+Create Date: 2021-12-13 23:00:51.552018
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '51dfa236bcf1'
+down_revision = '1baf7a0c6aab'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column(
+        'users', sa.Column('is_superuser', sa.Boolean(), default=False, nullable=False)
+    )
+
+
+def downgrade():
+    op.drop_column(
+        'users', 'is_superuser'
+    )

+ 59 - 0
ark_table/versions/e33bf2621069_alert_table.py

@@ -0,0 +1,59 @@
+"""alert table 
+
+Revision ID: e33bf2621069
+Revises: 
+Create Date: 2021-12-13 16:02:15.900470
+
+"""
+from datetime import datetime
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'e33bf2621069'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column(
+        'users', sa.Column('is_active', sa.Boolean(), default=False, nullable=False)
+    )
+    op.add_column(
+        'users', sa.Column('created_at', sa.types.DateTime(timezone=True), default=datetime.now(), nullable=False)
+    )
+    op.add_column(
+        'users', sa.Column('updated_at', sa.types.DateTime(timezone=True), default=datetime.now(), nullable=False)
+    )
+    op.add_column(
+        'users', sa.Column('hashed_password', sa.String(60), nullable=False)
+    )
+    op.add_column(
+        'users', sa.Column('email', sa.String(100), nullable=False)
+    )
+    op.add_column(
+        'users', sa.Column('account', sa.String(50), nullable=False)
+    )
+
+
+def downgrade():
+    op.drop_column('users',
+    'is_active'
+    )
+    op.drop_column('users',
+    'created_at'
+    )
+    op.drop_column('users',
+    'updated_at'
+    )
+    op.drop_column('users',
+    'hashed_password'
+    )
+    op.drop_column('users',
+    'email'
+    )
+    op.drop_column('users',
+    'account'
+    )

+ 44 - 0
requirement.txt

@@ -0,0 +1,44 @@
+alembic==1.7.5
+anyio==3.4.0
+asgiref==3.4.1
+cachetools==4.2.4
+certifi==2020.6.20
+chardet==4.0.0
+charset-normalizer==2.0.9
+click==8.0.3
+cssselect==1.1.0
+cssutils==2.3.0
+dnspython==2.1.0
+ecdsa==0.17.0
+email-validator==1.1.3
+emails==0.6
+fastapi==0.70.1
+greenlet==1.1.2
+h11==0.12.0
+httptools==0.3.0
+idna==3.3
+lxml==4.6.5
+Mako==1.1.6
+MarkupSafe==2.0.1
+passlib==1.7.4
+premailer==3.10.0
+pyasn1==0.4.8
+pydantic==1.8.2
+PyMySQL==1.0.2
+python-dateutil==2.8.2
+python-dotenv==0.19.2
+python-jose==3.3.0
+python-multipart==0.0.5
+PyYAML==6.0
+requests==2.26.0
+rsa==4.8
+six==1.16.0
+sniffio==1.2.0
+SQLAlchemy==1.4.28
+starlette==0.16.0
+typing_extensions==4.0.1
+urllib3==1.26.7
+uvicorn==0.16.0
+uvloop==0.16.0
+watchgod==0.7
+websockets==10.1

+ 303 - 0
requirements.txt

@@ -0,0 +1,303 @@
+absl-py==0.15.0
+alabaster==0.7.12
+anaconda-client==1.7.2
+anaconda-navigator==2.0.3
+anaconda-project==0.9.1
+anyio==3.3.4
+appdirs==1.4.4
+argh==0.26.2
+argon2-cffi==20.1.0
+asgiref==3.4.1
+asn1crypto==1.4.0
+astroid==2.5
+astropy==4.2.1
+astunparse==1.6.3
+async-generator==1.10
+atomicwrites==1.4.0
+attrs==20.3.0
+autopep8==1.5.6
+Babel==2.9.0
+backcall==0.2.0
+backports.functools-lru-cache==1.6.4
+backports.shutil-get-terminal-size==1.0.0
+backports.tempfile==1.0
+backports.weakref==1.0.post1
+beautifulsoup4==4.9.3
+bitarray==2.1.0
+bkcharts==0.2
+black==19.10b0
+bleach==3.3.0
+bokeh==2.3.2
+boto==2.49.0
+Bottleneck==1.3.2
+brotlipy==0.7.0
+cachetools==4.2.4
+certifi==2021.10.8
+cffi==1.14.5
+chardet==4.0.0
+charset-normalizer==2.0.7
+clang==5.0
+click==8.0.3
+cloudpickle==1.6.0
+clyent==1.2.2
+colorama==0.4.4
+conda==4.10.3
+conda-build==3.21.4
+conda-content-trust==0+unknown
+conda-package-handling==1.7.3
+conda-repo-cli==1.0.4
+conda-token==0.3.0
+conda-verify==3.4.2
+contextlib2==0.6.0.post1
+cryptography==3.4.7
+cssselect==1.1.0
+cssutils==2.3.0
+cycler==0.10.0
+Cython==0.29.23
+cytoolz==0.11.0
+dask==2021.4.0
+decorator==5.0.6
+defusedxml==0.7.1
+diff-match-patch==20200713
+distributed==2021.4.1
+docutils==0.17.1
+ecdsa==0.17.0
+emails==0.6
+entrypoints==0.3
+et-xmlfile==1.0.1
+fastapi==0.70.0
+fastcache==1.1.0
+filelock==3.0.12
+flake8==3.9.0
+Flask==1.1.2
+flatbuffers==1.12
+fsspec==0.9.0
+future==0.18.2
+gast==0.4.0
+gevent==21.1.2
+glob2==0.7
+gmpy2==2.0.8
+google-auth==2.3.0
+google-auth-oauthlib==0.4.6
+google-pasta==0.2.0
+greenlet==1.0.0
+grpcio==1.41.0
+h5py==3.1.0
+HeapDict==1.0.1
+html5lib==1.1
+httptools==0.2.0
+idna==3.3
+imageio==2.9.0
+imagesize==1.2.0
+importlib-metadata==3.10.0
+iniconfig==1.1.1
+intervaltree==3.1.0
+ipykernel==5.3.4
+ipython==7.22.0
+ipython-genutils==0.2.0
+ipywidgets==7.6.3
+isort==5.8.0
+itsdangerous==1.1.0
+jdcal==1.4.1
+jedi==0.17.2
+jeepney==0.6.0
+Jinja2==3.0.2
+joblib==1.0.1
+json5==0.9.5
+jsonschema==3.2.0
+jupyter==1.0.0
+jupyter-client==6.1.12
+jupyter-console==6.4.0
+jupyter-core==4.7.1
+jupyter-packaging==0.7.12
+jupyter-server==1.4.1
+jupyterlab==3.0.14
+jupyterlab-pygments==0.1.2
+jupyterlab-server==2.4.0
+jupyterlab-widgets==1.0.0
+keras==2.6.0
+Keras-Preprocessing==1.1.2
+keyring==22.3.0
+kiwisolver==1.3.1
+lazy-object-proxy==1.6.0
+libarchive-c==2.9
+line-pay==0.2.0
+llvmlite==0.36.0
+locket==0.2.1
+lxml==4.6.3
+Markdown==3.3.4
+MarkupSafe==2.0.1
+matplotlib==3.3.4
+mccabe==0.6.1
+mistune==0.8.4
+mkl-fft==1.3.0
+mkl-random==1.2.1
+mkl-service==2.3.0
+mock==4.0.3
+more-itertools==8.7.0
+mpmath==1.2.1
+msgpack==1.0.2
+multipledispatch==0.6.0
+mypy-extensions==0.4.3
+mysql==0.0.3
+mysql-connector-python==8.0.27
+mysqlclient==2.0.3
+navigator-updater==0.2.1
+nbclassic==0.2.6
+nbclient==0.5.3
+nbconvert==6.0.7
+nbformat==5.1.3
+nest-asyncio==1.5.1
+networkx==2.5
+nltk==3.6.1
+nose==1.3.7
+notebook==6.3.0
+numba==0.53.1
+numexpr==2.7.3
+numpy==1.19.5
+numpydoc==1.1.0
+oauthlib==3.1.1
+olefile==0.46
+openpyxl==3.0.7
+opt-einsum==3.3.0
+packaging==20.9
+pandas==1.2.4
+pandocfilters==1.4.3
+parso==0.7.0
+partd==1.2.0
+passlib==1.7.4
+path==15.1.2
+pathlib2==2.3.5
+pathspec==0.7.0
+patsy==0.5.1
+pep8==1.7.1
+pexpect==4.8.0
+pickleshare==0.7.5
+Pillow==8.2.0
+pip==21.0.1
+pkginfo==1.7.0
+pluggy==0.13.1
+ply==3.11
+premailer==3.10.0
+prometheus-client==0.10.1
+prompt-toolkit==3.0.17
+protobuf==3.19.0
+psutil==5.8.0
+ptyprocess==0.7.0
+py==1.10.0
+pyasn1==0.4.8
+pyasn1-modules==0.2.8
+pycodestyle==2.6.0
+pycosat==0.6.3
+pycparser==2.20
+pycurl==7.43.0.6
+pydocstyle==6.0.0
+pyerfa==1.7.3
+pyflakes==2.2.0
+Pygments==2.8.1
+pylint==2.7.4
+pyls-black==0.4.6
+pyls-spyder==0.3.2
+pyodbc==4.0.0-unsupported
+pyOpenSSL==20.0.1
+pyparsing==2.4.7
+pyrsistent==0.17.3
+PySocks==1.7.1
+pytest==6.2.3
+python-dateutil==2.8.1
+python-dotenv==0.19.1
+python-jose==3.3.0
+python-jsonrpc-server==0.4.0
+python-language-server==0.36.2
+pytz==2021.1
+PyWavelets==1.1.1
+pyxdg==0.27
+PyYAML==6.0
+pyzmq==20.0.0
+QDarkStyle==2.8.1
+QtAwesome==1.0.2
+qtconsole==5.0.3
+QtPy==1.9.0
+regex==2021.4.4
+requests==2.26.0
+requests-oauthlib==1.3.0
+rope==0.18.0
+rsa==4.7.2
+Rtree==0.9.7
+ruamel-yaml-conda==0.15.100
+scikit-image==0.18.1
+scikit-learn==0.24.1
+scipy==1.6.2
+seaborn==0.11.1
+SecretStorage==3.3.1
+Send2Trash==1.5.0
+setuptools==52.0.0.post20210125
+simplegeneric==0.8.1
+singledispatch==0.0.0
+sip==4.19.13
+six==1.15.0
+sniffio==1.2.0
+snowballstemmer==2.1.0
+sortedcollections==2.1.0
+sortedcontainers==2.3.0
+soupsieve==2.2.1
+Sphinx==4.0.1
+sphinxcontrib-applehelp==1.0.2
+sphinxcontrib-devhelp==1.0.2
+sphinxcontrib-htmlhelp==1.0.3
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-qthelp==1.0.3
+sphinxcontrib-serializinghtml==1.1.4
+sphinxcontrib-websupport==1.2.4
+spyder==4.2.5
+spyder-kernels==1.10.2
+SQLAlchemy==1.4.15
+starlette==0.16.0
+statsmodels==0.12.2
+sympy==1.8
+tables==3.6.1
+tblib==1.7.0
+tensorboard==2.7.0
+tensorboard-data-server==0.6.1
+tensorboard-plugin-wit==1.8.0
+tensorflow==2.6.0
+tensorflow-estimator==2.6.0
+termcolor==1.1.0
+terminado==0.9.4
+testpath==0.4.4
+textdistance==4.2.1
+threadpoolctl==2.1.0
+three-merge==0.1.1
+tifffile==2020.10.1
+toml==0.10.2
+toolz==0.11.1
+tornado==6.1
+tqdm==4.59.0
+traitlets==5.0.5
+typed-ast==1.4.2
+typing-extensions==3.10.0.2
+ujson==4.0.2
+unicodecsv==0.14.1
+unicorn==1.0.3
+urllib3==1.26.7
+uvicorn==0.15.0
+uvloop==0.16.0
+watchdog==1.0.2
+watchgod==0.7
+wcwidth==0.2.5
+webencodings==0.5.1
+websockets==10.0
+Werkzeug==1.0.1
+wheel==0.36.2
+widgetsnbextension==3.5.1
+wrapt==1.12.1
+wurlitzer==2.1.0
+xlrd==2.0.1
+XlsxWriter==1.3.8
+xlwt==1.3.0
+xmltodict==0.12.0
+yapf==0.31.0
+zict==2.0.0
+zipp==3.4.1
+zope.event==4.5.0
+zope.interface==5.3.0

Fichier diff supprimé car celui-ci est trop grand
+ 41 - 0
test.vuerd.json


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff