diff --git a/Pipfile b/Pipfile index d8cdf06..780c814 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,8 @@ asyncpg = "*" sqlalchemy = {extras = ["asyncio"], version = "*"} alembic = "*" ulid-py = "*" +passlib = {version = "*", extras = ["bcrypt"]} +python-multipart = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 97ea679..d6af6a1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "c2f505048c4397339837b528a77d4bb8428b29a0ecda73233aedd4c8e268cc46" + "sha256": "4c9a3f9a5f04ef73cd8c93835c24167a5bec926e43f67f0ddb8d13c80baeb271" }, "pipfile-spec": 6, "requires": { @@ -76,6 +76,32 @@ "index": "pypi", "version": "==0.27.0" }, + "bcrypt": { + "hashes": [ + "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535", + "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0", + "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410", + "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd", + "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665", + "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab", + "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71", + "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215", + "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b", + "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda", + "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9", + "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a", + "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344", + "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f", + "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d", + "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c", + "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c", + "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2", + "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d", + "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e", + "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3" + ], + "version": "==4.0.1" + }, "certifi": { "hashes": [ "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7", @@ -114,11 +140,11 @@ "all" ], "hashes": [ - "sha256:b87fffddf9c761c5618f638b492cfb73bba1d208ab170b89122cce52d1fb044a", - "sha256:eab70f072d6c424d16f02ba635dc0945fecab549210e7961c838f4b467f0b4a7" + "sha256:976df7bab51ac7beda9f68c4513b8c4490b5c1135c72aafd0a5ee4023ec5282e", + "sha256:ac78f717cd80d657bd183f94d33b9bda84aa376a46a9dab513586b8eef1dc6fc" ], "index": "pypi", - "version": "==0.99.0" + "version": "==0.99.1" }, "greenlet": { "hashes": [ @@ -486,6 +512,17 @@ ], "version": "==3.9.1" }, + "passlib": { + "extras": [ + "bcrypt" + ], + "hashes": [ + "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", + "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04" + ], + "index": "pypi", + "version": "==1.7.4" + }, "pydantic": { "hashes": [ "sha256:20a3b30fd255eeeb63caa9483502ba96b7795ce5bf895c6a179b3d909d9f53a6", @@ -540,6 +577,7 @@ "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132", "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" ], + "index": "pypi", "version": "==0.0.6" }, "pyyaml": { @@ -699,13 +737,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:16224afa8cc2b3679dd9e9a1efe719dd2e20a03f0cc2e4cc4c97870ae9622532", - "sha256:3c2c2cd887648efa0ea8f8ba4260a1213058e8e4a25a6a6f4e084740b2c858e2", - "sha256:5d8c9dac95c27d20df12fb1d97b9793ab8b2af8a3a525e68c80e21060c161771", - "sha256:935ccf31549830cda708b42289d44b6f74084d616a00be651601a4f968e77c82" + "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", + "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" ], "markers": "python_version >= '3.7'", - "version": "==4.7.0" + "version": "==4.7.1" }, "ujson": { "hashes": [ diff --git a/migrations/versions/2ac7bad1f528_create_user_account_table.py b/migrations/versions/be5208ed3fdf_create_user_account_table.py similarity index 85% rename from migrations/versions/2ac7bad1f528_create_user_account_table.py rename to migrations/versions/be5208ed3fdf_create_user_account_table.py index 851eb71..041c09f 100644 --- a/migrations/versions/2ac7bad1f528_create_user_account_table.py +++ b/migrations/versions/be5208ed3fdf_create_user_account_table.py @@ -1,8 +1,8 @@ """create user_account table -Revision ID: 2ac7bad1f528 +Revision ID: be5208ed3fdf Revises: -Create Date: 2023-07-03 01:00:13.176776 +Create Date: 2023-07-04 01:45:56.243660 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '2ac7bad1f528' +revision = 'be5208ed3fdf' down_revision = None branch_labels = None depends_on = None @@ -22,7 +22,7 @@ def upgrade() -> None: sa.Column('id', sa.String(length=26), nullable=False), sa.Column('username', sa.String(length=20), nullable=False), sa.Column('email', sa.String(), nullable=False), - sa.Column('password', sa.String(), nullable=False), + sa.Column('password', sa.String(length=60), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint('id'), diff --git a/src/crud/user_account.py b/src/crud/user_account.py index 7c62195..aef8631 100644 --- a/src/crud/user_account.py +++ b/src/crud/user_account.py @@ -1,5 +1,24 @@ -from ..database.db import engine +from typing import Literal, Tuple -async def insert_user(): +from sqlalchemy import select, insert - pass +from ..database.db import async_session +from ..database.models import UserAccount + + +async def select_user_by(condition: Literal['email', 'username'], value: str) -> Tuple[bool, UserAccount]: + async with async_session() as session: + if condition == 'email': + res = await session.scalars(select(UserAccount).where(UserAccount.email == value)) + else: + res = await session.scalars(select(UserAccount).where(UserAccount.username == value)) + + user = res.first() + + return (True, user) if user else (False, None) + + +async def insert_user(username: str, password: str, email: str): + async with async_session() as session: + await session.execute(insert(UserAccount).values(username=username, email=email, password=password)) + await session.commit() diff --git a/src/database/models.py b/src/database/models.py index 45fdb13..6a6adeb 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -2,7 +2,7 @@ from datetime import datetime from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import mapped_column, Mapped -from sqlalchemy import String, func +from sqlalchemy import String import ulid @@ -18,9 +18,9 @@ class UserAccount(BaseModel): id: Mapped[str] = mapped_column(String(26), primary_key=True, insert_default=ulid.new().str) username: Mapped[str] = mapped_column(String(20), unique=True) email: Mapped[str] = mapped_column(String, unique=True) - password: Mapped[str] = mapped_column(String) - updated_at: Mapped[datetime] = mapped_column(insert_default=func.now()) - created_at: Mapped[datetime] = mapped_column(server_onupdate=func.now()) + password: Mapped[str] = mapped_column(String(60)) + updated_at: Mapped[datetime] = mapped_column(default=datetime.now) + created_at: Mapped[datetime] = mapped_column(default=datetime.now, onupdate=datetime.now) def __repr__(self): return f'UserAccount(username={self.username}, email={self.email})' diff --git a/src/utils/redis_api.py b/src/database/redis_api.py similarity index 100% rename from src/utils/redis_api.py rename to src/database/redis_api.py diff --git a/src/handlers/signup_handlers.py b/src/handlers/signup_handlers.py deleted file mode 100644 index 38d4865..0000000 --- a/src/handlers/signup_handlers.py +++ /dev/null @@ -1,8 +0,0 @@ -from ..utils.redis_api import redis_server - - -async def verify_code(email: str, code: str) -> bool: - key = f'code:{email}' - value = redis_server.get(key) - return code == value - diff --git a/src/main.py b/src/main.py index 3d588bd..920a313 100644 --- a/src/main.py +++ b/src/main.py @@ -1,11 +1,13 @@ from fastapi import FastAPI +from .router.signin import router as signin_router from .utils.email_code import smtp from .router.signup import router as signup_router app = FastAPI() app.include_router(signup_router) +app.include_router(signin_router) @app.on_event('shutdown') diff --git a/src/router/profile.py b/src/router/profile.py new file mode 100644 index 0000000..e69de29 diff --git a/src/router/signin.py b/src/router/signin.py new file mode 100644 index 0000000..44a634c --- /dev/null +++ b/src/router/signin.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter +from fastapi import Depends +from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer + +from ..crud.user_account import select_user_by +from ..utils.password import verify_password + +router = APIRouter(prefix='/signin', tags=['signin'], redirect_slashes=False, ) + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl='token') + + +@router.post('/token/username') +async def signin_by_username(form_data: OAuth2PasswordRequestForm = Depends()): + username = form_data.username + password = form_data.password + is_existence, user = await select_user_by('username', username) + + if not is_existence: + return {'code': 10201, 'msg': 'Username or Password Is Incorrect'} + + is_correct = verify_password(password, user.password) + + if is_correct: + return {'code': 10200, 'msg': 'Signin Successfully'} + else: + return {'code': 10201, 'msg': 'Username or Password Is Incorrect'} diff --git a/src/router/signup.py b/src/router/signup.py index 5f0250e..2ca8c6d 100644 --- a/src/router/signup.py +++ b/src/router/signup.py @@ -1,10 +1,13 @@ +from typing import Literal + from fastapi import APIRouter, BackgroundTasks from pydantic import BaseModel -from ..utils.email_code import send_email -from ..handlers import signup_handlers +from ..utils.email_code import send_email, verify_code +from ..crud import user_account +from ..utils.password import * -router = APIRouter(tags=['signup'], prefix='/signup') +router = APIRouter(tags=['signup'], prefix='/signup', redirect_slashes=False) class SignUpAccount(BaseModel): @@ -14,18 +17,30 @@ class SignUpAccount(BaseModel): code: str +@router.get('/has_existed') +async def has_account_existed(condition: Literal['username', 'email'], value: str): + (res, _) = await user_account.select_user_by(condition, value) + if res: + return {'code': 10101, 'msg': f'{condition.capitalize()} Has Existed'} + else: + return {'code': 10100, 'msg': f'{condition.capitalize()} Has Not Been Used'} + + @router.get('/code/{email}') async def get_code(email: str, background_tasks: BackgroundTasks): background_tasks.add_task(send_email, email) - return {'code': 10000, 'msg': 'success'} + return {'code': 10100, 'msg': 'Get verification Code Success'} @router.post('') async def sign_up(signup_account: SignUpAccount): - verification_res = await signup_handlers.verify_code(signup_account.email, signup_account.code) + verification_res = verify_code(signup_account.email, signup_account.code) if not verification_res: - return {'code': 10001, 'msg': 'Code is incorrect'} + return {'code': 10102, 'msg': 'Code Is Incorrect or Code Is Out of Date'} + hashed_password = get_hashed_password(signup_account.password) + await user_account.insert_user(signup_account.username, hashed_password, signup_account.email) + return {'code': 10100, 'msg': 'Sign up successfully'} diff --git a/src/utils/email_code.py b/src/utils/email_code.py index f170a6f..d41d47e 100644 --- a/src/utils/email_code.py +++ b/src/utils/email_code.py @@ -2,7 +2,7 @@ import random from email.message import EmailMessage from smtplib import SMTP, SMTPServerDisconnected -from .redis_api import redis_server +from src.database.redis_api import redis_server smtp = SMTP(host='smtp.office365.com') smtp.ehlo() @@ -10,7 +10,7 @@ smtp.starttls() smtp.login('together_app@outlook.com', 'togetherno.1') -async def connect_email_server(): +def connect_email_server(): try: smtp.noop() except SMTPServerDisconnected: @@ -20,10 +20,10 @@ async def connect_email_server(): smtp.login('together_app@outlook.com', 'togetherno.1') -async def send_email(to: str): - code = await generate_code(to) +def send_email(to: str): + code = generate_code(to) msg = EmailMessage() - await connect_email_server() + connect_email_server() msg['Subject'] = 'Together app signup verification code' msg['From'] = 'TogetherApp ' msg['To'] = f'<{to}>' @@ -33,7 +33,7 @@ async def send_email(to: str):

您的验证码是: {code}

-
+

验证码60秒内有效

@@ -44,9 +44,15 @@ async def send_email(to: str): smtp.quit() -async def generate_code(email: str) -> str: +def generate_code(email: str) -> str: seed = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] chosen_elements = random.choices(seed, k=6) code = ''.join(chosen_elements) redis_server.set(f'code:{email}', code, ex=60) return code + + +def verify_code(email: str, code: str) -> bool: + key = f'code:{email}' + value = redis_server.get(key) + return code == value diff --git a/src/utils/password.py b/src/utils/password.py new file mode 100644 index 0000000..3f3e4c9 --- /dev/null +++ b/src/utils/password.py @@ -0,0 +1,12 @@ +from passlib.context import CryptContext + +pwd_ctx = CryptContext(schemes=['bcrypt'], deprecated='auto') + + +def get_hashed_password(password: str) -> str: + return pwd_ctx.hash(password) + + +def verify_password(plain_password: str, hashed_password): + return pwd_ctx.verify(plain_password, hashed_password) +