mirror of
https://github.com/grey-cat-1908/formaptix-server.git
synced 2024-09-22 19:52:00 +03:00
accounts (admin && user)
This commit is contained in:
parent
d9cf862040
commit
12ee0d4e7b
11 changed files with 146 additions and 32 deletions
|
@ -2,11 +2,12 @@ from models import settings
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
from .user import *
|
|
||||||
|
|
||||||
engine = create_async_engine(settings.database)
|
engine = create_async_engine(settings.database)
|
||||||
sessions = async_sessionmaker(engine)
|
sessions = async_sessionmaker(engine)
|
||||||
|
|
||||||
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
from .user import User
|
||||||
|
|
1
main.py
1
main.py
|
@ -18,6 +18,7 @@ async def lifespan(app: FastAPI):
|
||||||
await connection.run_sync(database.Base.metadata.create_all)
|
await connection.run_sync(database.Base.metadata.create_all)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
app.include_router(routes.router)
|
app.include_router(routes.router)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import pydantic
|
||||||
|
|
||||||
class BaseModel(pydantic.BaseModel):
|
class BaseModel(pydantic.BaseModel):
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
from .settings import settings
|
from .settings import settings
|
||||||
|
|
|
@ -17,3 +17,11 @@ class Token(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
username: str
|
username: str
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteUser(BaseModel):
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePassword(BaseModel):
|
||||||
|
password: str
|
||||||
|
|
19
poetry.lock
generated
19
poetry.lock
generated
|
@ -533,6 +533,23 @@ azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0
|
||||||
toml = ["tomli (>=2.0.1)"]
|
toml = ["tomli (>=2.0.1)"]
|
||||||
yaml = ["pyyaml (>=6.0.1)"]
|
yaml = ["pyyaml (>=6.0.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.9.0"
|
||||||
|
description = "JSON Web Token implementation in Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
|
||||||
|
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
crypto = ["cryptography (>=3.4.0)"]
|
||||||
|
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||||
|
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
|
||||||
|
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
@ -694,4 +711,4 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "c63b5753bf55ec6d3ae3cf225ad9d388fa1705cc2ce49718822221a8c19b2d66"
|
content-hash = "1d617553a03b53b743a89007a49b7167e21a07b5c19277b56b8b436af92c1f00"
|
||||||
|
|
|
@ -14,6 +14,7 @@ pydantic = "^2.8.2"
|
||||||
SQLAlchemy = "^2.0.32"
|
SQLAlchemy = "^2.0.32"
|
||||||
pydantic-settings = "^2.4.0"
|
pydantic-settings = "^2.4.0"
|
||||||
asyncpg = "^0.29.0"
|
asyncpg = "^0.29.0"
|
||||||
|
pyjwt = "^2.9.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
isort = "^5.13.2"
|
isort = "^5.13.2"
|
||||||
|
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
|
@ -1,7 +1,9 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from . import admin
|
from . import admin
|
||||||
|
from . import user
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
router.include_router(admin.router)
|
router.include_router(admin.router)
|
||||||
|
router.include_router(user.router)
|
||||||
|
|
|
@ -1,29 +1,15 @@
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Annotated
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select, delete
|
||||||
|
|
||||||
import database
|
import database
|
||||||
from models import settings, user
|
from models import settings, user, DeleteUser
|
||||||
|
from .utils import Admin, hash_password
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin")
|
router = APIRouter(prefix="/admin")
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str, salt: str) -> str:
|
|
||||||
return hashlib.sha512((password + salt).encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def verify_admin(token: str):
|
|
||||||
if token != settings.admin_password:
|
|
||||||
raise HTTPException(401, "Unauthorized")
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
Admin = Annotated[bool, Depends(verify_admin, use_cache=False)]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/user")
|
@router.post("/user")
|
||||||
async def create_user(auth: user.Auth, admin_token: Admin):
|
async def create_user(auth: user.Auth, admin_token: Admin):
|
||||||
if len(auth.username.strip()) == 0:
|
if len(auth.username.strip()) == 0:
|
||||||
|
@ -36,17 +22,26 @@ async def create_user(auth: user.Auth, admin_token: Admin):
|
||||||
salt = secrets.token_hex(8)
|
salt = secrets.token_hex(8)
|
||||||
|
|
||||||
async with database.sessions.begin() as session:
|
async with database.sessions.begin() as session:
|
||||||
stmt = select(database.User).where(database.User.username == auth.username)
|
stmt = select(database.User).where(
|
||||||
db_user = session.execute(stmt).scalar_one_or_none()
|
database.User.username == auth.username.strip()
|
||||||
if db_user is not None:
|
)
|
||||||
|
db_request = await session.execute(stmt)
|
||||||
|
user = db_request.scalar_one_or_none()
|
||||||
|
if user is not None:
|
||||||
raise HTTPException(400, "User with this username already exists")
|
raise HTTPException(400, "User with this username already exists")
|
||||||
|
|
||||||
new_user = database.User(
|
new_user = database.User(
|
||||||
username=auth.username,
|
username=auth.username.strip(),
|
||||||
password=hash_password(auth.password, salt),
|
password=hash_password(auth.password.strip(), salt),
|
||||||
salt=salt,
|
salt=salt,
|
||||||
)
|
)
|
||||||
session.add(new_user)
|
session.add(new_user)
|
||||||
await session.flush()
|
|
||||||
|
|
||||||
return {'status': 'Success'}
|
|
||||||
|
@router.delete("/user")
|
||||||
|
async def delete_user(user: DeleteUser, admin_token: Admin):
|
||||||
|
async with database.sessions.begin() as session:
|
||||||
|
stmt = delete(database.User).where(
|
||||||
|
database.User.username == user.username.strip()
|
||||||
|
)
|
||||||
|
await session.execute(stmt)
|
||||||
|
|
|
@ -1,8 +1,47 @@
|
||||||
import hashlib
|
import secrets
|
||||||
|
|
||||||
from fastapi import APIRouter
|
import jwt
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
import database
|
import database
|
||||||
|
import models
|
||||||
|
from models import settings
|
||||||
|
from .utils import hash_password, User
|
||||||
|
|
||||||
router = APIRouter(prefix="/user")
|
router = APIRouter(prefix="/user")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login")
|
||||||
|
async def login(auth: models.Auth):
|
||||||
|
async with database.sessions.begin() as session:
|
||||||
|
stmt = select(database.User).where(
|
||||||
|
database.User.username == auth.username.strip()
|
||||||
|
)
|
||||||
|
request = await session.execute(stmt)
|
||||||
|
user = request.scalar_one_or_none()
|
||||||
|
|
||||||
|
if (
|
||||||
|
user is None
|
||||||
|
or hash_password(auth.password.strip(), user.salt) != user.password
|
||||||
|
):
|
||||||
|
raise HTTPException(403, "Forbidden")
|
||||||
|
|
||||||
|
return models.Token(
|
||||||
|
id=user.id,
|
||||||
|
username=user.username,
|
||||||
|
token=jwt.encode(
|
||||||
|
{"sub": user.id}, settings.secret + user.password, "HS256"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/update/password")
|
||||||
|
async def update_password(user: User, new: models.UpdatePassword):
|
||||||
|
if len(new.password.strip()) == 0:
|
||||||
|
raise HTTPException(400, "Password must not be empty")
|
||||||
|
|
||||||
|
async with database.sessions.begin() as session:
|
||||||
|
session.add(user)
|
||||||
|
user.salt = secrets.token_hex(8)
|
||||||
|
user.password = hash_password(new.password.strip(), user.salt)
|
||||||
|
|
50
routes/utils.py
Normal file
50
routes/utils.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from typing import Annotated
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
import jwt
|
||||||
|
from fastapi import Depends, HTTPException, Header
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from models import settings
|
||||||
|
import database
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str, salt: str) -> str:
|
||||||
|
return hashlib.sha512((password + salt).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_admin(password: Annotated[str, Header(alias="x-token")]):
|
||||||
|
if password.strip() != settings.admin_password:
|
||||||
|
raise HTTPException(401, "Unauthorized")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_user(token: Annotated[str, Header(alias="x-token")]) -> database.User:
|
||||||
|
try:
|
||||||
|
data = jwt.decode(
|
||||||
|
token, algorithms=["HS256"], options={"verify_signature": False}
|
||||||
|
)
|
||||||
|
except jwt.exceptions.DecodeError:
|
||||||
|
raise HTTPException(401, "Invalid token")
|
||||||
|
|
||||||
|
if "sub" not in data and not isinstance(data["sub"], int):
|
||||||
|
raise HTTPException(401, "Invalid token")
|
||||||
|
|
||||||
|
async with database.sessions.begin() as session:
|
||||||
|
stmt = select(database.User).where(database.User.id == data.get("sub"))
|
||||||
|
db_request = await session.execute(stmt)
|
||||||
|
user = db_request.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(401, "Invalid token")
|
||||||
|
|
||||||
|
try:
|
||||||
|
jwt.decode(token, settings.secret + user.password, algorithms=["HS256"])
|
||||||
|
except jwt.exceptions.InvalidSignatureError:
|
||||||
|
raise HTTPException(401, "Invalid token")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
User = Annotated[database.User, Depends(verify_user, use_cache=False)]
|
||||||
|
Admin = Annotated[bool, Depends(verify_admin, use_cache=False)]
|
Loading…
Reference in a new issue