change username, email and password, token login

main
htylight 2023-07-17 01:02:59 +08:00
parent bd0eeec213
commit 28e72d7467
12 changed files with 222 additions and 68 deletions

44
Pipfile.lock generated
View File

@ -199,27 +199,31 @@
},
"cryptography": {
"hashes": [
"sha256:059e348f9a3c1950937e1b5d7ba1f8e968508ab181e75fc32b879452f08356db",
"sha256:1a5472d40c8f8e91ff7a3d8ac6dfa363d8e3138b961529c996f3e2df0c7a411a",
"sha256:1a8e6c2de6fbbcc5e14fd27fb24414507cb3333198ea9ab1258d916f00bc3039",
"sha256:1fee5aacc7367487b4e22484d3c7e547992ed726d14864ee33c0176ae43b0d7c",
"sha256:5d092fdfedaec4cbbffbf98cddc915ba145313a6fdaab83c6e67f4e6c218e6f3",
"sha256:5f0ff6e18d13a3de56f609dd1fd11470918f770c6bd5d00d632076c727d35485",
"sha256:7bfc55a5eae8b86a287747053140ba221afc65eb06207bedf6e019b8934b477c",
"sha256:7fa01527046ca5facdf973eef2535a27fec4cb651e4daec4d043ef63f6ecd4ca",
"sha256:8dde71c4169ec5ccc1087bb7521d54251c016f126f922ab2dfe6649170a3b8c5",
"sha256:8f4ab7021127a9b4323537300a2acfb450124b2def3756f64dc3a3d2160ee4b5",
"sha256:948224d76c4b6457349d47c0c98657557f429b4e93057cf5a2f71d603e2fc3a3",
"sha256:9a6c7a3c87d595608a39980ebaa04d5a37f94024c9f24eb7d10262b92f739ddb",
"sha256:b46e37db3cc267b4dea1f56da7346c9727e1209aa98487179ee8ebed09d21e43",
"sha256:b4ceb5324b998ce2003bc17d519080b4ec8d5b7b70794cbd2836101406a9be31",
"sha256:cb33ccf15e89f7ed89b235cff9d49e2e62c6c981a6061c9c8bb47ed7951190bc",
"sha256:d198820aba55660b4d74f7b5fd1f17db3aa5eb3e6893b0a41b75e84e4f9e0e4b",
"sha256:d34579085401d3f49762d2f7d6634d6b6c2ae1242202e860f4d26b046e3a1006",
"sha256:eb8163f5e549a22888c18b0d53d6bb62a20510060a22fd5a995ec8a05268df8a",
"sha256:f73bff05db2a3e5974a6fd248af2566134d8981fd7ab012e5dd4ddb1d9a70699"
"sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711",
"sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7",
"sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd",
"sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e",
"sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58",
"sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0",
"sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d",
"sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83",
"sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831",
"sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766",
"sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b",
"sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c",
"sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182",
"sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f",
"sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa",
"sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4",
"sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a",
"sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2",
"sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76",
"sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5",
"sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee",
"sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f",
"sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14"
],
"version": "==41.0.1"
"version": "==41.0.2"
},
"dnspython": {
"hashes": [

View File

@ -46,7 +46,7 @@ def upgrade() -> None:
sa.Column('location', sa.String(), nullable=True),
sa.Column('status', sa.String(), nullable=True),
sa.Column('sign', sa.String(), nullable=True),
sa.Column('avatar', sa.String(), nullable=True),
sa.Column('avatars', sa.String(), nullable=True),
sa.Column('user_id', sa.String(length=26), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')

View File

@ -10,16 +10,20 @@ from ..database.db import async_session
from ..database.models import UserAccount, UserProfile, Contact
async def select_user_by(condition: Literal['email', 'username'], value: str) -> Tuple[bool, UserAccount]:
async with async_session() as session:
if condition == 'email':
async def select_user_by(condition: Literal['email', 'username', 'id'], value: str) -> Tuple[bool, UserAccount]:
session = async_session()
res: ScalarResult[UserAccount] = ScalarResult[UserAccount]
match condition:
case 'email':
res = await session.scalars(select(UserAccount).where(UserAccount.email == value))
else:
case 'username':
res = await session.scalars(select(UserAccount).where(UserAccount.username == value))
case 'id':
res = await session.scalars(select(UserAccount).where(UserAccount.id == value))
user = res.first()
user = res.first()
return (True, user) if user else (False, None)
return (True, user) if user else (False, None)
async def insert_user(username: str, password: str, email: str):
@ -27,8 +31,10 @@ async def insert_user(username: str, password: str, email: str):
id = ulid.new().str
user = UserAccount(id=id, username=username, password=password, email=email)
profile = UserProfile(nickname=username)
contact = Contact(friends={id: {'friendRemark': None, 'friendGroup': '我的好友'}}, friend_groups=['我的好友'],
group_chats={})
contact = Contact(
friends={id: {'friendRemark': None, 'friendGroup': '我的好友'}}, friend_groups=['我的好友'],
group_chats={}
)
user.profile = profile
user.contact = contact
session.add(user)
@ -75,3 +81,28 @@ async def update_profile_avatar(id: str, avatar_name: str):
)
await session.commit()
await session.close()
async def update_account(account: Literal['username', 'email', 'password'], id: str, value: str):
session = async_session()
match account:
case 'username':
await session.execute(
update(UserAccount)
.where(UserAccount.id == id)
.values(username=value)
)
case 'email':
await session.execute(
update(UserAccount)
.where(UserAccount.id == id)
.values(email=value)
)
case 'password':
await session.execute(
update(UserAccount)
.where(UserAccount.id == id)
.values(password=value)
)
await session.commit()
await session.close()

View File

@ -1,7 +1,16 @@
from fastapi import Depends
from fastapi.responses import Response, JSONResponse
from jose import ExpiredSignatureError, JWTError
from .utils import token_handler as th
def verify_token(token: str = Depends(th.oauth2_scheme)):
th.verify_signin_token(token)
def verify_token(response: Response, token: str = Depends(th.oauth2_scheme)):
try:
token, _ = th.verify_signin_token(token)
if token:
response.headers['Authorization'] = token
except ExpiredSignatureError:
return JSONResponse({'code': 9999, 'msg': 'Token Expire'})
except JWTError:
return JSONResponse({'code': 9998, 'msg': 'Token Is Not Right'})

View File

@ -1,15 +1,18 @@
from fastapi import FastAPI, Depends
from .dependencies import verify_token
from .utils.email_code import smtp
from .routers.signin import router as signin_router
from .routers.signup import router as signup_router
from .routers.user_profile import router as user_profile_router
from .routers.user_account import router as user_account_router
app = FastAPI()
app.include_router(signup_router)
app.include_router(signin_router)
app.include_router(user_profile_router)
app.include_router(user_profile_router, dependencies=[Depends(verify_token)])
app.include_router(user_account_router, dependencies=[Depends(verify_token)])
@app.on_event('shutdown')

View File

@ -26,11 +26,17 @@ class UserAccountResponse(BaseResponseModel):
class TokenCreationResponse(BaseResponseModel):
data: str
token: str
class TokenSigninResponse(BaseResponseModel):
data: Optional[_UserAccount] = None
token: Optional[str] = None
class UserProfileResponse(BaseResponseModel):
data: _UserProfile
class UserAvatarResponse(BaseResponseModel):
data: str
data: str

View File

@ -3,17 +3,19 @@ from fastapi import Depends
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel
from jose import ExpiredSignatureError, JWTError
from ..crud.user_crud import select_user_by
from ..utils.password import verify_password
from ..utils.token_handler import create_signin_token, oauth2_scheme
from ..response_models.user_response import UserAccountResponse, TokenCreationResponse
from ..utils.token_handler import create_signin_token, oauth2_scheme, verify_signin_token
from ..response_models.user_response import UserAccountResponse, TokenCreationResponse, TokenSigninResponse
router = APIRouter(prefix='/signin', tags=['signin'])
class TokenPayload(BaseModel):
id: str
phone_mac: str
device_id: str
@router.post('/username', response_model=UserAccountResponse)
@ -36,9 +38,19 @@ async def signin_by_username(form_data: OAuth2PasswordRequestForm = Depends()):
@router.post('/token', response_model=TokenCreationResponse)
async def create_token(token_payload: TokenPayload):
token = create_signin_token(**token_payload.model_dump())
return {'code': 10200, 'msg': 'Create Token Successfully', 'data': token}
return {'code': 10200, 'msg': 'Create Token Successfully', 'token': token}
@router.get('/token')
@router.get('/token', response_model=TokenSigninResponse)
async def signin_by_token(token: str = Depends(oauth2_scheme)):
pass
try:
new_token, id = verify_signin_token(token)
_, user = await select_user_by('id', id)
if new_token:
return {'code': 10200, 'msg': 'Sign in Successfully', 'data': user.to_dict(), 'token': new_token}
else:
return {'code': 10200, 'msg': 'Sign in Successfully', 'data': user.to_dict(), 'token': token}
except ExpiredSignatureError:
return {'code': 9999, 'msg': 'Token has Expired', 'data': None, 'token': None}
except JWTError:
return {'code': 9998, 'msg': 'Token Is Not Right', 'data': None, 'token': None}

View File

@ -0,0 +1,73 @@
from fastapi import APIRouter, BackgroundTasks
from pydantic import BaseModel
from ..crud import user_crud
from ..utils import password
from ..utils.email_code import send_email, has_code, verify_code
router = APIRouter(prefix='/user_account', tags=['user_account'])
class ChangedAccount(BaseModel):
id: str
username: str | None = None
email: str | None = None
password: str | None = None
code: str | None = None
@router.post('/change/username')
async def change_username(changed_account: ChangedAccount):
is_existed, user = await user_crud.select_user_by('username', changed_account.username)
if is_existed:
return {'code': 10401, 'msg': f'This Username ({changed_account.username}) Has Been Used'}
await user_crud.update_account('username', changed_account.id, changed_account.username)
return {'code': 10400, 'msg': 'Update Username Successfully'}
@router.get('/get/email_code')
async def get_change_email_code(email: str, background_tasks: BackgroundTasks):
is_existed, _ = await user_crud.select_user_by('email', email)
if is_existed:
return {'code': 10401, 'msg': f'This Email ({email}) Has Been Used'}
if has_code(email):
return {'code': 10402, 'msg': f'Code of Email ({email}) Is Still Available'}
background_tasks.add_task(send_email, email)
return {'code': 10400, 'msg': 'Send Verification Code Successfully'}
@router.post('/change/email')
async def change_email(changed_account: ChangedAccount):
is_correct = verify_code(changed_account.email, changed_account.code)
if not is_correct:
return {'code': 10403, 'msg': f'Email Code ({changed_account.code}) Is Not Correct'}
await user_crud.update_account('email', changed_account.id, changed_account.email)
return {'code': 10400, 'msg': 'Update Email Successfully'}
@router.get('/get/password_code')
async def get_change_password_code(email: str, background_tasks: BackgroundTasks):
if has_code(email):
return {'code': 10402, 'msg': f'Code of Email ({email}) Is Still Available'}
background_tasks.add_task(send_email, email)
return {'code': 10400, 'msg': 'Send Verification Code Successfully'}
@router.post('/change/password')
async def change_password(changed_account: ChangedAccount):
is_correct = verify_code(changed_account.email, changed_account.code)
if not is_correct:
return {'code': 10403, 'msg': f'Email Code ({changed_account.code}) Is Not Correct'}
hashed_password = password.get_hashed_password(changed_account.password)
await user_crud.update_account('password', changed_account.id, hashed_password)
return {'code': 10400, 'msg': 'Update Email Successfully'}

View File

@ -1,25 +1,25 @@
from typing import Optional
from fastapi import APIRouter, Depends
from fastapi import APIRouter
from pydantic import BaseModel
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.encoders import jsonable_encoder
from anyio import open_file
from ..crud import user_crud
from ..dependencies import verify_token
from ..response_models.user_response import UserProfileResponse, UserAvatarResponse
from ..utils import static_file
router = APIRouter(prefix='/user_profile', tags=['user_profile'], dependencies=[Depends(verify_token)])
router = APIRouter(prefix='/user_profile', tags=['user_profile'])
class Uint8List(BaseModel):
file: list
class ModifiedProfile(BaseModel):
class ChangedProfile(BaseModel):
id: str
nickname: Optional[str] = None
location: Optional[str] = None
@ -31,7 +31,11 @@ class ModifiedProfile(BaseModel):
@router.get('/my', response_model=UserProfileResponse)
async def get_profile(id: str):
profile = await user_crud.select_profile(id)
return {'code': 10300, 'msg': 'Get My Profile Successfully', 'data': profile.to_dict_all()}
return JSONResponse(
content=jsonable_encoder(
{'code': 10300, 'msg': 'Get My Profile Successfully', 'data': profile.to_dict_all()},
)
)
@router.get('/avatar')
@ -40,8 +44,8 @@ async def download_avatar(avatar_filename: str):
return FileResponse(avatar_dir_path / avatar_filename)
@router.post('/modify/avatar', response_model=UserAvatarResponse)
async def modify_avatar(id: str, file: Uint8List):
@router.post('/change/avatar', response_model=UserAvatarResponse)
async def change_avatar(id: str, file: Uint8List):
avatar_dir_path = static_file.create_avatar_dir()
avatar_filename = static_file.create_avatar_filename()
@ -53,22 +57,22 @@ async def modify_avatar(id: str, file: Uint8List):
return {'code': 10300, 'msg': 'Update Avatar Successfully', 'data': avatar_filename}
@router.post('/modify/{aspect}')
async def modify_profile(aspect: str, modified_profile: ModifiedProfile):
@router.post('/change/{aspect}')
async def change_profile(aspect: str, changed_profile: ChangedProfile):
match aspect:
case 'basic':
await user_crud.update_profile_basic(
modified_profile.id,
modified_profile.nickname,
modified_profile.location,
modified_profile.birthday,
modified_profile.gender,
changed_profile.id,
changed_profile.nickname,
changed_profile.location,
changed_profile.birthday,
changed_profile.gender,
)
case 'sign':
await user_crud.update_profile_sign(
modified_profile.id,
modified_profile.sign
changed_profile.id,
changed_profile.sign
)
case _:
return {'code': 10301, 'msg': f'No /modify/{aspect} Path'}
return {'code': 10301, 'msg': f'No /change/{aspect} Path'}
return {'code': 10300, 'msg': f'Update {aspect} Profile Successfully'}

View File

@ -41,7 +41,6 @@ def send_email(to: str):
msg.set_content(email_content)
smtp.send_message(msg)
smtp.quit()
def generate_code(email: str) -> str:
@ -56,3 +55,9 @@ def verify_code(email: str, code: str) -> bool:
key = f'code:{email}'
value = redis_server.get(key)
return code == value
def has_code(email: str) -> bool:
key = f'code:{email}'
value = redis_server.get(key)
return True if value else False

View File

@ -9,7 +9,7 @@ alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
def create_avatar_dir() -> Path:
avatar_dir_path = Path(os.getcwd()) / 'static' / 'avatar'
avatar_dir_path = Path(os.getcwd()) / 'static' / 'avatars'
if not avatar_dir_path.exists():
avatar_dir_path.mkdir()
return avatar_dir_path

View File

@ -1,9 +1,9 @@
from datetime import datetime, timedelta
from typing import TypedDict
from typing import TypedDict, Tuple
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, ExpiredSignatureError
from jose import jwt, ExpiredSignatureError, JWTError
# openssl rand -hex 32
SECRET_KEY = '1c3c03b79d084f0c7b41ba11d1d9a4979f72d9fc6eaaaa0a855065e8a5be0468'
@ -17,27 +17,34 @@ class SigninClaim(TypedDict):
iss: str
iat: float
exp: float
phone_mac: str
device_id: str
def create_signin_token(id: str, phone_mac: str) -> str:
def create_signin_token(id: str, device_id: str) -> str:
claim: SigninClaim = {
'sub': id,
'iss': 'together',
'iat': datetime.now().timestamp(),
'exp': (datetime.now() + timedelta(days=23)).timestamp(),
'phone_mac': phone_mac
'device_id': device_id,
}
return jwt.encode(claim, SECRET_KEY, algorithm=ALGORITHM)
def verify_signin_token(token):
def verify_signin_token(token) -> Tuple[str | None, str]:
try:
claim: SigninClaim = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
if claim['exp'] - datetime.now().timestamp() <= 10*24*60*60:
_prolong_token(token)
new_token = _prolong_token(token)
return new_token, claim['sub']
else:
return None, claim['sub']
except ExpiredSignatureError:
pass
print(f'{token} expire ========================')
raise ExpiredSignatureError
except JWTError:
print(f'This token {token} is not sign by me ==========================')
raise JWTError
def _prolong_token(claim: SigninClaim) -> str: