Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ omit=
*/docs/
*/venv/*
*/server.py
*/authentication
30 changes: 14 additions & 16 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import os
import datetime
import json
import types
import socket
import logging
import datetime
import os
import socket
import traceback
import types
from exceptions.exceptions import ServerException
from exceptions.send_alert import send_dingding_alert
from logging.config import dictConfig
from typing import Any, Dict, Tuple, Union

from flask_cors import CORS
from flask import Flask, request
from flask_cors import CORS
from werkzeug.exceptions import HTTPException

from logging.config import dictConfig
from typing import Tuple, Union, Dict, Any

from configures import settings
from resources import ApiResponse
from blueprints import all_blueprints
from models.database_models import db
from configures import settings
from models.base_model import BaseModel

from exceptions.exceptions import ServerException
from exceptions.send_alert import send_dingding_alert
from models.database_models import db
from resources import ApiResponse

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -49,9 +47,9 @@ def create_app() -> Flask:


def init_config(app) -> None:
app.config["SQLALCHEMY_DATABASE_URI"] = settings.SQLALCHEMY_DATABASE_URI
app.config["SQLALCHEMY_DATABASE_URI"] = settings.LOCAL_SQLALCHEMY_DATABASE_URI
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = settings.SQLALCHEMY_TRACK_MODIFICATIONS

app.config['SECRET_KEY'] = settings.SECRET_KEY
register_blueprints(app)
app.register_error_handler(Exception, handle_exception)

Expand Down
3 changes: 3 additions & 0 deletions authentication/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from authentication.token import auth

__all__ = ["auth"]
38 changes: 38 additions & 0 deletions authentication/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from flask import g
from flask_httpauth import HTTPBasicAuth
from flask_restful import Resource

from models.database_models.user_model import User

auth = HTTPBasicAuth()


# @auth.verify_password
# def verify_password(email, password):
# user = User.query.filter_by(email=email).first()
# if not user or not user.check_password(password):
# return False
# g.user = user
# return True


class Token(Resource):
@auth.login_required
def get(self):
token = g.user.generate_auth_token()
return {'token': token.decode('ascii')}


@auth.verify_password
def verify_password(username_or_token, password):
# first try to authenticate by token
user = User.verify_auth_token(username_or_token)
print("here", username_or_token)
print(user)
if not user:
# try to authenticate with username/password
user = User.query.filter_by(email=username_or_token).first()
if not user or not user.check_password(password):
return False
g.user = user
return True
7 changes: 5 additions & 2 deletions blueprints/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from flask import Blueprint
from flask_restful import Api

from authentication.token import Token
from resources.comment import Comment, Comments
from resources.post import Post, Posts
from resources.topic import Topic, Topics, RootTopic, RootTopics
from resources.topic import RootTopic, RootTopics, Topic, Topics
from resources.user import User, Users
from resources.comment import Comment, Comments

api_bp = Blueprint("api", __name__, url_prefix="/api")

Expand All @@ -24,3 +25,5 @@

api.add_resource(Comment, "/topics/<int:topic_id>/posts/<int:post_id>/comments/<int:comment_id>")
api.add_resource(Comments, "/topics/<int:topic_id>/posts/<int:post_id>/comments")

api.add_resource(Token, "/token")
5 changes: 4 additions & 1 deletion configures/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
NAMESPACE = "PRODUCTION"
SECRET_KEY = "fbca22c2a2ed11ea91b488e9fe4e9d33"
date_format = "%Y-%m-%d %H:%M:%S"
SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:ROOT@mysql/flask_restful?charset=UTF8MB4"
SQLALCHEMY_DATABASE_BASE = "mysql+pymysql://root:ROOT@mysql/"
Expand All @@ -8,5 +9,7 @@
TEST_SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:@localhost/TEST?charset=UTF8MB4"
TEST_DATABASE = "TEST"

LOCAL_SQLALCHEMY_DATABASE_URI = "mysql+pymysql://root:ROOT@localhost:3307/flask_restful?charset=UTF8MB4"
LOCAL_SQLALCHEMY_DATABASE_URI = (
"mysql+pymysql://root:ROOT@localhost:3307/flask_restful?charset=UTF8MB4"
)
LOCAL_SQLALCHEMY_DATABASE_BASE = "mysql+pymysql://root:ROOT@localhost:3307/"
16 changes: 8 additions & 8 deletions handlers/post_hander.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from sqlalchemy import and_
from exceptions.exceptions import ArgumentInvalid, ObjectsNotExist
from typing import Generator, Optional

from sqlalchemy import and_

from configures.const import POST_MINIMUM_WORDS
from handlers import BaseHandler
from models.database_models import Comment
from models.database_models.user_model import User
from models.database_models.post_model import Post
from models.database_models.topic_model import Topic
from models.database_models.user_model import User
from models.response_models.post_model import ResponsePostModel

from configures.const import POST_MINIMUM_WORDS
from exceptions.exceptions import ObjectsNotExist, ArgumentInvalid


class PostHandler(BaseHandler):
_model = Post
Expand Down Expand Up @@ -41,7 +41,7 @@ def assert_post_id_is_not_none(self) -> None:

@staticmethod
def get_comment_count(instance: Comment) -> int:
condition = and_(Comment.deleted == False, Comment.post_id == instance.id)
condition = and_(Comment.deleted == False, Comment.post_id == instance.id) # noqa
total = Comment.query.filter(condition).count()

return total
Expand All @@ -60,10 +60,10 @@ def get_posts(self, **kwargs) -> Generator[ResponsePostModel, None, None]:
per_page = kwargs["per_page"]
offset = kwargs["offset"]

condition = and_(Post.deleted == False, Post.topic_id == self.topic_id)
condition = and_(Post.deleted == False, Post.topic_id == self.topic_id) # noqa
posts = self._model.query.filter(condition).offset(offset).limit(per_page) # 分页
for post in posts:
post.update(click_times=post.click_times + 1)
post.update(click_times=post.click_times or 0 + 1)
comments_count = self.get_comment_count(post)
yield ResponsePostModel(comments_count=comments_count, **post.as_dict())

Expand Down
14 changes: 5 additions & 9 deletions handlers/user_handler.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from exceptions.exceptions import ArgumentInvalid, ArgumentRequired, ObjectsDuplicated
from typing import Generator, Optional

from werkzeug.security import generate_password_hash

from handlers.utils import EmailChecker, PassWordChecker
from handlers import BaseHandler
from handlers.utils import EmailChecker, PassWordChecker
from models.database_models.user_model import User
from models.response_models.user_model import ResponseUserModel as ResponseUser

from exceptions.exceptions import ObjectsDuplicated, ArgumentInvalid, ArgumentRequired


class UserHandler(BaseHandler):
_model = User
Expand Down Expand Up @@ -46,12 +46,8 @@ def create(**kwargs) -> Optional[ResponseUser]:
if not PassWordChecker.is_allowed(password):
raise ArgumentInvalid(PassWordChecker.ERROR_MSG)

password_hash = generate_password_hash(password)

kwargs.pop("password")
kwargs.pop("email")

ins = User.create(password_hash=password_hash, email=email, **kwargs)
kwargs["password_hash"] = generate_password_hash(password)
ins = User.create(**kwargs)

return ResponseUser(**ins.as_dict())

Expand Down
6 changes: 4 additions & 2 deletions models/database_models/post_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ class Post(Base):
topic_id = Column(Integer, nullable=False, comment="文章所在的主题 ID")
user_id = Column(Integer, nullable=False, comment="发布文章用户的 ID")
content = Column(Text, nullable=False, comment="")
click_times = Column(Integer, default=0, comment="文章的点击数")
click_times = Column(Integer, default=0, server_default=text('0'), comment="文章的点击数")
tags = Column(JSON, nullable=True, default=[], comment="文章的 tag")

create_time = Column(DateTime, server_default=func.now(), comment="创建时间")
update_time = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
deleted = Column(Boolean, default=False, server_default=text('0'), nullable=False, comment="该项目是否被删除")
deleted = Column(
Boolean, default=False, server_default=text('0'), nullable=False, comment="该项目是否被删除"
)
33 changes: 32 additions & 1 deletion models/database_models/user_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from itsdangerous import BadSignature, SignatureExpired
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from sqlalchemy import Boolean, DateTime, Integer, String, func, text
from werkzeug.security import check_password_hash, generate_password_hash

from configures import settings
from models.database_models.base_model import Base, Column


Expand Down Expand Up @@ -44,4 +48,31 @@ class User(Base):

create_time = Column(DateTime, server_default=func.now(), comment="创建时间")
update_time = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
deleted = Column(Boolean, default=False, server_default=text('0'), nullable=False, comment="该项目是否被删除")
deleted = Column(
Boolean, default=False, server_default=text('0'), nullable=False, comment="该项目是否被删除"
)

def __repr__(self):
return f"User<{self.name}>"

def hash_password(self, password):
self.password_hash = generate_password_hash(password)

def check_password(self, password):
return check_password_hash(self.password_hash, password)

def generate_auth_token(self, expiration=600):
s = Serializer(settings.SECRET_KEY, expires_in=expiration)
return s.dumps({'id': self.id})

@staticmethod
def verify_auth_token(token):
s = Serializer(settings.SECRET_KEY)
try:
data = s.loads(token)
except SignatureExpired:
return None # valid token, but expired
except BadSignature:
return None # invalid token
user = User.query.get(data['id'])
return user
13 changes: 13 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
alembic==1.4.2
aniso8601==8.0.0
appdirs==1.4.4
appnope==0.1.0
astroid==2.3.3
attrs==19.3.0
backcall==0.1.0
certifi==2020.4.5.1
cffi==1.14.0
cfgv==3.1.0
Expand All @@ -18,6 +20,7 @@ docopt==0.6.2
filelock==3.0.12
Flask==1.1.2
Flask-Cors==3.0.8
Flask-HTTPAuth==4.0.0
Flask-Migrate==2.5.3
Flask-RESTful==0.3.8
Flask-Script==2.0.6
Expand All @@ -28,8 +31,11 @@ gunicorn==20.0.4
identify==1.4.15
idna==2.9
importlab==0.5.1
ipython==7.14.0
ipython-genutils==0.2.0
isort==4.3.21
itsdangerous==1.1.0
jedi==0.17.0
Jinja2==2.11.2
lazy-object-proxy==1.4.3
Mako==1.1.2
Expand All @@ -42,10 +48,16 @@ networkx==2.4
ninja==1.9.0.post1
nodeenv==1.3.5
packaging==20.3
parso==0.7.0
pexpect==4.8.0
pickleshare==0.7.5
pluggy==0.13.1
pre-commit==2.4.0
prompt-toolkit==3.0.5
ptyprocess==0.6.0
py==1.8.1
pycparser==2.20
Pygments==2.6.1
pylint==2.4.4
PyMySQL==0.9.3
pyparsing==2.4.7
Expand All @@ -58,6 +70,7 @@ requests==2.23.0
six==1.14.0
SQLAlchemy==1.3.16
toml==0.10.0
traitlets==4.3.3
typed-ast==1.4.1
typing-extensions==3.7.4.2
urllib3==1.25.9
Expand Down
11 changes: 8 additions & 3 deletions resources/comment.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
from flask_restful import Resource
from resources import schema, ApiResponse

from authentication import auth
from handlers.comment_handler import CommentHandler
from models.query_models.comment_model import CommentQueryModel
from models.response_models.comment_model import ResponseCommentModel

from handlers.comment_handler import CommentHandler
from resources import ApiResponse, schema


class Comment(Resource):
@auth.login_required
@schema(query_model=CommentQueryModel, response_model=ResponseCommentModel)
def get(self, topic_id, post_id, comment_id) -> ApiResponse:
comment = CommentHandler(topic_id, post_id, comment_id).get_comment()
return ApiResponse().ok(comment)

@auth.login_required
@schema(query_model=CommentQueryModel, response_model=ResponseCommentModel)
def put(self, topic_id, post_id, comment_id) -> ApiResponse:
kwargs = self.parsed_args
comment = CommentHandler(topic_id, post_id, comment_id).update_comment(**kwargs)
return ApiResponse().ok(comment)

@auth.login_required
@schema(query_model=CommentQueryModel, response_model=ResponseCommentModel)
def delete(self, topic_id, post_id, comment_id) -> ApiResponse:
CommentHandler(topic_id, post_id, comment_id).delete_comment()
return ApiResponse().ok()


class Comments(Resource):
@auth.login_required
@schema(query_model=CommentQueryModel, response_model=ResponseCommentModel)
def get(self, topic_id, post_id) -> ApiResponse:
kwargs = self.parsed_args
comments = CommentHandler(topic_id=topic_id, post_id=post_id).get_comments(**kwargs)

return ApiResponse().ok(comments)

@auth.login_required
@schema(query_model=CommentQueryModel, response_model=ResponseCommentModel)
def post(self, topic_id, post_id) -> ApiResponse:
kwargs = self.parsed_args
Expand Down
Loading