implement signin and signup

main
htylight 2023-07-04 11:50:02 +08:00
parent a40e4d412b
commit b46b22678e
13 changed files with 152 additions and 41 deletions

View File

@ -10,6 +10,8 @@ asyncpg = "*"
sqlalchemy = {extras = ["asyncio"], version = "*"}
alembic = "*"
ulid-py = "*"
passlib = {version = "*", extras = ["bcrypt"]}
python-multipart = "*"
[dev-packages]

54
Pipfile.lock generated
View File

@ -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": [

View File

@ -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'),

View File

@ -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()

View File

@ -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})'

View File

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

View File

@ -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')

View File

View File

@ -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'}

View File

@ -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'}

View File

@ -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 <together_app@outlook.com>'
msg['To'] = f'<{to}>'
@ -33,7 +33,7 @@ async def send_email(to: str):
<head></head>
<body>
<p>您的验证码是: <b>{code}</b></p>
<br>
<p>验证码60秒内有效</p>
<footer>邮件来自: <a>Together App</a></footer>
</body>
</html>
@ -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

View File

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