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

View File

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

View File

@ -10,16 +10,20 @@ from ..database.db import async_session
from ..database.models import UserAccount, UserProfile, Contact from ..database.models import UserAccount, UserProfile, Contact
async def select_user_by(condition: Literal['email', 'username'], value: str) -> Tuple[bool, UserAccount]: async def select_user_by(condition: Literal['email', 'username', 'id'], value: str) -> Tuple[bool, UserAccount]:
async with async_session() as session: session = async_session()
if condition == 'email': res: ScalarResult[UserAccount] = ScalarResult[UserAccount]
match condition:
case 'email':
res = await session.scalars(select(UserAccount).where(UserAccount.email == value)) res = await session.scalars(select(UserAccount).where(UserAccount.email == value))
else: case 'username':
res = await session.scalars(select(UserAccount).where(UserAccount.username == value)) 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): 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 id = ulid.new().str
user = UserAccount(id=id, username=username, password=password, email=email) user = UserAccount(id=id, username=username, password=password, email=email)
profile = UserProfile(nickname=username) profile = UserProfile(nickname=username)
contact = Contact(friends={id: {'friendRemark': None, 'friendGroup': '我的好友'}}, friend_groups=['我的好友'], contact = Contact(
group_chats={}) friends={id: {'friendRemark': None, 'friendGroup': '我的好友'}}, friend_groups=['我的好友'],
group_chats={}
)
user.profile = profile user.profile = profile
user.contact = contact user.contact = contact
session.add(user) session.add(user)
@ -75,3 +81,28 @@ async def update_profile_avatar(id: str, avatar_name: str):
) )
await session.commit() await session.commit()
await session.close() 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 import Depends
from fastapi.responses import Response, JSONResponse
from jose import ExpiredSignatureError, JWTError
from .utils import token_handler as th from .utils import token_handler as th
def verify_token(token: str = Depends(th.oauth2_scheme)): def verify_token(response: Response, token: str = Depends(th.oauth2_scheme)):
th.verify_signin_token(token) 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 fastapi import FastAPI, Depends
from .dependencies import verify_token
from .utils.email_code import smtp from .utils.email_code import smtp
from .routers.signin import router as signin_router from .routers.signin import router as signin_router
from .routers.signup import router as signup_router from .routers.signup import router as signup_router
from .routers.user_profile import router as user_profile_router from .routers.user_profile import router as user_profile_router
from .routers.user_account import router as user_account_router
app = FastAPI() app = FastAPI()
app.include_router(signup_router) app.include_router(signup_router)
app.include_router(signin_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') @app.on_event('shutdown')

View File

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

View File

@ -3,17 +3,19 @@ from fastapi import Depends
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from pydantic import BaseModel from pydantic import BaseModel
from jose import ExpiredSignatureError, JWTError
from ..crud.user_crud import select_user_by from ..crud.user_crud import select_user_by
from ..utils.password import verify_password from ..utils.password import verify_password
from ..utils.token_handler import create_signin_token, oauth2_scheme from ..utils.token_handler import create_signin_token, oauth2_scheme, verify_signin_token
from ..response_models.user_response import UserAccountResponse, TokenCreationResponse from ..response_models.user_response import UserAccountResponse, TokenCreationResponse, TokenSigninResponse
router = APIRouter(prefix='/signin', tags=['signin']) router = APIRouter(prefix='/signin', tags=['signin'])
class TokenPayload(BaseModel): class TokenPayload(BaseModel):
id: str id: str
phone_mac: str device_id: str
@router.post('/username', response_model=UserAccountResponse) @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) @router.post('/token', response_model=TokenCreationResponse)
async def create_token(token_payload: TokenPayload): async def create_token(token_payload: TokenPayload):
token = create_signin_token(**token_payload.model_dump()) 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)): 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 typing import Optional
from fastapi import APIRouter, Depends from fastapi import APIRouter
from pydantic import BaseModel 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 anyio import open_file
from ..crud import user_crud from ..crud import user_crud
from ..dependencies import verify_token
from ..response_models.user_response import UserProfileResponse, UserAvatarResponse from ..response_models.user_response import UserProfileResponse, UserAvatarResponse
from ..utils import static_file 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): class Uint8List(BaseModel):
file: list file: list
class ModifiedProfile(BaseModel): class ChangedProfile(BaseModel):
id: str id: str
nickname: Optional[str] = None nickname: Optional[str] = None
location: Optional[str] = None location: Optional[str] = None
@ -31,7 +31,11 @@ class ModifiedProfile(BaseModel):
@router.get('/my', response_model=UserProfileResponse) @router.get('/my', response_model=UserProfileResponse)
async def get_profile(id: str): async def get_profile(id: str):
profile = await user_crud.select_profile(id) 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') @router.get('/avatar')
@ -40,8 +44,8 @@ async def download_avatar(avatar_filename: str):
return FileResponse(avatar_dir_path / avatar_filename) return FileResponse(avatar_dir_path / avatar_filename)
@router.post('/modify/avatar', response_model=UserAvatarResponse) @router.post('/change/avatar', response_model=UserAvatarResponse)
async def modify_avatar(id: str, file: Uint8List): async def change_avatar(id: str, file: Uint8List):
avatar_dir_path = static_file.create_avatar_dir() avatar_dir_path = static_file.create_avatar_dir()
avatar_filename = static_file.create_avatar_filename() 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} return {'code': 10300, 'msg': 'Update Avatar Successfully', 'data': avatar_filename}
@router.post('/modify/{aspect}') @router.post('/change/{aspect}')
async def modify_profile(aspect: str, modified_profile: ModifiedProfile): async def change_profile(aspect: str, changed_profile: ChangedProfile):
match aspect: match aspect:
case 'basic': case 'basic':
await user_crud.update_profile_basic( await user_crud.update_profile_basic(
modified_profile.id, changed_profile.id,
modified_profile.nickname, changed_profile.nickname,
modified_profile.location, changed_profile.location,
modified_profile.birthday, changed_profile.birthday,
modified_profile.gender, changed_profile.gender,
) )
case 'sign': case 'sign':
await user_crud.update_profile_sign( await user_crud.update_profile_sign(
modified_profile.id, changed_profile.id,
modified_profile.sign changed_profile.sign
) )
case _: 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'} 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) msg.set_content(email_content)
smtp.send_message(msg) smtp.send_message(msg)
smtp.quit()
def generate_code(email: str) -> str: def generate_code(email: str) -> str:
@ -56,3 +55,9 @@ def verify_code(email: str, code: str) -> bool:
key = f'code:{email}' key = f'code:{email}'
value = redis_server.get(key) value = redis_server.get(key)
return code == value 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: 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(): if not avatar_dir_path.exists():
avatar_dir_path.mkdir() avatar_dir_path.mkdir()
return avatar_dir_path return avatar_dir_path

View File

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