This commit is contained in:
wuaho 2021-04-28 20:20:19 +08:00
parent 7375591882
commit 3b662873c7
33 changed files with 745 additions and 0 deletions

1
.gitignore vendored
View File

@ -129,3 +129,4 @@ dmypy.json
# Pyre type checker
.pyre/
.idea

0
api/__init__.py Normal file
View File

0
api/api_v1/__init__.py Normal file
View File

6
api/api_v1/api.py Normal file
View File

@ -0,0 +1,6 @@
from fastapi import APIRouter
from api.api_v1.endpoints import login
api_router = APIRouter()
api_router.include_router(login.router, tags=["登录接口"])

View File

View File

@ -0,0 +1,31 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from sqlalchemy.orm import Session
import crud, models, schemas
from api import deps
from core import security
from core.config import settings
from core.security import get_password_hash
from utils import (
verify_password_reset_token,
)
router = APIRouter()
@router.post("/create-space")
def create_space() -> Any:
pass
@router.post("/create-folder")
def create_folder() -> Any:
pass
@router.post("/create-folder")
def create_folder() -> Any:
pass

View File

@ -0,0 +1,86 @@
import json
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
import crud, models, schemas
from api import deps
from core import security
from core.config import settings
from core.security import get_password_hash
from utils import (
verify_password_reset_token,
)
router = APIRouter()
@router.post("/login")
def login(
data: schemas.UserLogin,
# data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(deps.get_db),
) -> Any:
"""
OAuth2兼容令牌登录获取将来令牌的访问令牌
"""
user = crud.user.authenticate(
db, name=data.username, password=data.password
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect name 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 {
'data': {
'name': user.name,
'email': user.email,
'token': security.create_access_token(
expires_delta=access_token_expires, user_id=user.id, email=user.email, is_active=user.is_active,
is_superuser=user.is_superuser, name=user.name
),
},
'code': 0,
'msg': 'success',
}
@router.post("/me", response_model=schemas.UserBase)
# @router.post("/me/")
def me(request: Request) -> Any:
"""
Test access token
"""
return request.state.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:
"""
重设密码
"""
user_id = verify_password_reset_token(token)
if not user_id:
raise HTTPException(status_code=400, detail="Invalid token")
user = crud.user.get(db, user_id)
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"}

View File

@ -0,0 +1,22 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException, Request
from sqlalchemy.orm import Session
import crud, models, schemas
from api import deps
from core import security
from core.config import settings
from core.security import get_password_hash
from utils import (
verify_password_reset_token,
)
router = APIRouter()
@router.post("/create-project")
def create_project() -> Any:
pass

60
api/deps.py Normal file
View File

@ -0,0 +1,60 @@
import json
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
import crud, models, schemas
from core import security
from core.config import settings
from db.session import SessionLocal
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/"
)
def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
def get_current_user(token:str
) -> schemas.UserBase:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
user = schemas.UserBase(**payload)
except (jwt.JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: models.User = Depends(get_current_user),
) -> models.User:
if not crud.user.is_active(current_user):
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(
current_user: models.User = Depends(get_current_user),
) -> models.User:
if not crud.user.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return current_user

0
core/__init__.py Normal file
View File

33
core/config.py Normal file
View File

@ -0,0 +1,33 @@
from typing import Any, Dict, List, Optional, Union
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, validator
class Settings(BaseSettings):
PROJECT_NAME: str = 'X数据分析后台'
API_V1_STR: str = '/api/v1'
ALLOW_ANONYMOUS: tuple = ('login','openapi.json')
BACKEND_CORS_ORIGINS: List[str] = ['*']
MYSQL_HOST: str = '127.0.0.1'
MYSQL_PORT: int = 3306
MYSQL_USER: str = 'root'
MYSQL_PASSWORD: str = 'root'
MYSQL_DB: str = 'xdata'
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}'
FIRST_EMAIL: str = '15392746632@qq.com'
FIRST_SUPERUSER_PASSWORD: str = '123456'
FIRST_NAME: str = 'root'
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
SECRET_KEY: str = 'ZaFX6EypK6PtuhGv11q4DLRvAb0csiLx4dbKUwLwCe8'
class Config:
case_sensitive = True
settings = Settings()

33
core/security.py Normal file
View File

@ -0,0 +1,33 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(
expires_delta: timedelta = None, **payload
) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
payload["exp"] = expire
encoded_jwt = jwt.encode(payload, 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)

3
crud/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .crud_user import user

66
crud/base.py Normal file
View File

@ -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 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
crud/crud_item.py Normal file
View File

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

55
crud/crud_user.py Normal file
View File

@ -0,0 +1,55 @@
from typing import Any, Dict, Optional, Union
from sqlalchemy.orm import Session
from core.security import get_password_hash, verify_password
from crud.base import CRUDBase
from models.user import User
from schemas.user import UserCreate, UserUpdate
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_user(self, db: Session, *, name: str) -> Optional[User]:
return db.query(User).filter(User.name == name).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),
name=obj_in.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, *, name: str, password: str) -> Optional[User]:
user = self.get_by_user(db, name=name)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(self, user: User) -> bool:
return user.is_active
def is_superuser(self, user: User) -> bool:
return user.is_superuser
user = CRUDUser(User)

0
db/__init__.py Normal file
View File

0
db/base.py Normal file
View File

14
db/base_class.py Normal file
View File

@ -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
# 自动生成表名
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

16
db/init_db.py Normal file
View File

@ -0,0 +1,16 @@
from sqlalchemy.orm import Session
import crud, schemas
from core.config import settings
from db import base # noqa: F401
def init_db(db: Session) -> None:
user = crud.user.get_by_user(db, name=settings.FIRST_NAME)
if not user:
user_in = schemas.UserCreate(
name=settings.FIRST_NAME,
email=settings.FIRST_EMAIL,
password=settings.FIRST_SUPERUSER_PASSWORD,
is_superuser=True,
)
user = crud.user.create(db, obj_in=user_in) # noqa: F841

7
db/session.py Normal file
View File

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

22
initial_data.py Normal file
View File

@ -0,0 +1,22 @@
import logging
from db.init_db import init_db
from db.session import SessionLocal
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def init() -> None:
db = SessionLocal()
init_db(db)
def main() -> None:
logger.info("Creating initial data")
init()
logger.info("Initial data created")
if __name__ == "__main__":
main()

45
main.py Normal file
View File

@ -0,0 +1,45 @@
import time
import uvicorn
from fastapi import FastAPI, Request, Depends
from starlette.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from api.api_v1.api import api_router
from api.deps import get_current_user
from core.config import settings
app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json")
allow_anonymous = [f'{settings.API_V1_STR}/{page}' for page in settings.ALLOW_ANONYMOUS]
allow_anonymous.extend(['/docs'])
@app.middleware("http")
async def add_jwt_auth(request: Request, call_next):
fail = {'code': -1, 'msg': '身份验证失败'}
if request.scope.get('path') not in allow_anonymous:
token = request.headers.get("Authorization")
try:
user = get_current_user(token)
except:
return JSONResponse(fail)
#
# request.state.user = user
response = await call_next(request)
return response
if settings.BACKEND_CORS_ORIGINS:
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR)
if __name__ == '__main__':
uvicorn.run(app='main:app', host="0.0.0.0", port=8888, reload=True, debug=True)

1
models/__init__.py Normal file
View File

@ -0,0 +1 @@
from .user import User

15
models/dashboard.py Normal file
View File

@ -0,0 +1,15 @@
from sqlalchemy import Boolean, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from db.base_class import Base
class Dashboard(Base):
id = Column(Integer, primary_key=True, index=True)
folder_type = Column(String)
pid = Column(Integer, ForeignKey('dashboard.id'))
parent = relationship('Dashboard', remote_side=[id])
children = relationship('Dashboard', remote_side=[pid])
name = Column(String, nullable=False)
user_id = Column(Integer, ForeignKey('user.id'))
project_id = Column(Integer, ForeignKey('project.id'))

8
models/project.py Normal file
View File

@ -0,0 +1,8 @@
from sqlalchemy import Boolean, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from db.base_class import Base
class Dashboard(Base):
id = Column(Integer, primary_key=True, index=True)

13
models/user.py Normal file
View File

@ -0,0 +1,13 @@
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import relationship
from db.base_class import Base
class User(Base):
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
is_active = Column(Boolean(), default=True)
is_superuser = Column(Boolean(), default=False)
dashboard = relationship('Dashboard', back_populates='user')

3
schemas/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .user import User, UserCreate, UserInDB, UserUpdate,UserLogin,UserBase
from .token import Token, TokenPayload
from .msg import Msg

5
schemas/msg.py Normal file
View File

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

15
schemas/token.py Normal file
View File

@ -0,0 +1,15 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
token: str
code: int
name: str
email: str
msg: str
class TokenPayload(BaseModel):
user_id: int = None

44
schemas/user.py Normal file
View File

@ -0,0 +1,44 @@
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
name: Optional[str] = None
class UserLogin(BaseModel):
username: str = ...
password: str = ...
# 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

0
test/__init__.py Normal file
View File

107
utils.py Normal file
View File

@ -0,0 +1,107 @@
import json
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 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.get('user_id')
except jwt.JWTError:
return None