commit 27466a255feab84b0b57f7e939ee14054c8792e3 Author: roma-dxunvrs Date: Sun Nov 30 12:38:46 2025 +0300 Demo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8ce240 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0cfecd --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Генерируем приватный ключ в папку certs(пока что она открыта) +openssl genrsa -out jwt-private.pem 2048 +На основе него публичный +openssl rsa -in jwt-private.pem -outform PEM -pubout -out jwt-public.pem + diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..dd08cfa --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,16 @@ +from pathlib import Path +from pydantic import BaseModel +from pydantic_settings import BaseSettings + +BASE_DIR = Path(__file__).parent.parent.parent + +class AuthJWT(BaseModel): + private_key_path: Path = BASE_DIR/"certs"/"jwt-private.pem" + public_key_path: Path = BASE_DIR/"certs"/"jwt-public.pem" + algorithm: str = "RS256" + access_token_expire_minutes: int = 15 + +class Settings(BaseSettings): + auth_jwt: AuthJWT = AuthJWT() + +settings = Settings() \ No newline at end of file diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/demo.py b/app/routers/demo.py new file mode 100644 index 0000000..a2f7385 --- /dev/null +++ b/app/routers/demo.py @@ -0,0 +1,95 @@ +from app.schemas.user import UserSchema +from app.schemas.token import TokenInfo +from app.utils.bcrypt_utils import * +from app.utils.jwt_utlis import * + +from jwt.exceptions import InvalidTokenError +from fastapi import APIRouter, Depends, Form, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +http_bearer = HTTPBearer() + +router = APIRouter(prefix="/jwt", tags=["JWT"]) + +john = UserSchema( + username="john", + password=hash_password("qwerty"), + email="john@example.com" +) + +sam = UserSchema( + username="sam", + password=hash_password("secret"), +) + +users_db: dict[str, UserSchema] = { + john.username: john, + sam.username: sam +} + +def validate_auth_user( + username: str = Form(), + password: str = Form() +): + unauthed_exc = HTTPException( + status_code=401, + detail="Invalid username or password" + ) + if not (user := users_db.get(username)): + raise unauthed_exc + + if validate_password( + password=password, + hashed_password=user.password + ): + return user + raise unauthed_exc + +def get_current_token_payload( + credentials: HTTPAuthorizationCredentials = Depends(http_bearer) +) -> UserSchema: + token = credentials.credentials + try: + payload = decode_jwt( + token=token + ) + except InvalidTokenError: + raise HTTPException( + status_code=401, + detail="Invalid token error" + ) + return payload + +def get_current_auth_user( + payload: dict = Depends(get_current_token_payload) +) -> UserSchema: + username: str = payload.get("username") + if not (user := users_db.get(username)): + raise HTTPException( + status_code=401, + detail="Token invalid" # fr user not found + ) + return user + +@router.post("/login/", response_model=TokenInfo) +def auth_user_issue_jwt( + user: UserSchema = Depends(validate_auth_user) +): + jwt_payload = { + "username": user.username, + "email": user.email + } + token = encode_jwt(jwt_payload) + return TokenInfo( + access_token=token, + token_type="Bearer" + ) + +@router.get("/users/me/") +def auth_user_check_self_info( + user: UserSchema = Depends(get_current_auth_user) +): + return { + "username": user.username, + "email": user.email + } \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/token.py b/app/schemas/token.py new file mode 100644 index 0000000..4dbc71b --- /dev/null +++ b/app/schemas/token.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class TokenInfo(BaseModel): + access_token: str + token_type: str \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..02c56ec --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, EmailStr, ConfigDict + + +class UserSchema(BaseModel): + model_config = ConfigDict(strict=True) + + username: str + password: bytes + email: EmailStr | None = None + active: bool = True \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/bcrypt_utils.py b/app/utils/bcrypt_utils.py new file mode 100644 index 0000000..224d320 --- /dev/null +++ b/app/utils/bcrypt_utils.py @@ -0,0 +1,17 @@ +import bcrypt + +def hash_password( + password: str +) -> bytes: + salt = bcrypt.gensalt() + pwd_bytes: bytes = password.encode() + return bcrypt.hashpw(pwd_bytes, salt) + +def validate_password( + password: str, + hashed_password: bytes +) -> bool: + return bcrypt.checkpw( + password=password.encode(), + hashed_password=hashed_password + ) \ No newline at end of file diff --git a/app/utils/jwt_utlis.py b/app/utils/jwt_utlis.py new file mode 100644 index 0000000..da56db9 --- /dev/null +++ b/app/utils/jwt_utlis.py @@ -0,0 +1,30 @@ +from datetime import timedelta, datetime + +import jwt +from app.core.config import settings + +def encode_jwt(payload: dict, + private_key: str = settings.auth_jwt.private_key_path.read_text(), + algorithm=settings.auth_jwt.algorithm, + expire_timedelta: timedelta | None = None, + expire_minutes: int = settings.auth_jwt.access_token_expire_minutes): + to_encode = payload.copy() + now = datetime.utcnow() + if expire_timedelta: + expire = now + timedelta + else: + expire = now + timedelta(minutes=expire_minutes) + to_encode.update( + exp=expire, + iat=now + ) + encoded = jwt.encode(to_encode, + private_key, + algorithm=algorithm) + return encoded + +def decode_jwt(token: str | bytes, + public_key: str = settings.auth_jwt.public_key_path.read_text(), + algorithm: str = settings.auth_jwt.algorithm): + decoded = jwt.decode(token, public_key, algorithms=[algorithm]) + return decoded \ No newline at end of file diff --git a/certs/jwt-private.pem b/certs/jwt-private.pem new file mode 100644 index 0000000..13ed48a --- /dev/null +++ b/certs/jwt-private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvCJK5l6y0AbeW +1yMhbvR2LrJz0F+oXwWsx/pK+h73I48UhITWRKrktYYrnEyc/T2ip2x5EaYwYQpi +mZ1+cX5iIqWO/7QtlKSXQfKlccU9YDXKrkK7p1lHkPnSIRXC+x8/IuicseLe8Iue +smYR6qfIs6rt6HehtlXj8yVFmc2XVZsWEIyERCXLbxYJSka/kGKzL1OwQpS8kubu +LToTxGhJey8rdm30mEA+oWAHqmuKJYCTfC9qwzb3D4XhNTIe2vMfyeZN/m5pSK9h +XwaE0up8MiMMtr/r4kAcMMjGxa02G9sKa2twXvEz3swrhiEZmaJ2tsHqlH3mcHFJ +KfrxRXS3AgMBAAECggEACRTDyaQOtMqMan1AK44X86dZx0J3XamLoAd1rlaQ+eq6 ++kO9DjTco1l7qMWTzM1V8xttBR1r5ufeykQE2zheNNwHuB/Lve9C8goQFPj0uv0Q +rWXvIzdI6lQ7C39MVr2aq51PCw1iCbkNm8hog7BBNkHqfpKbqlgAcTKYEdorkLKT +q1GVlCJTH1ObsBextTCB7sNYfMx9aF3r796C2PF9n75/JAn2ju29ZvaRlKnV50XO +ETOpBVUu8soTDBMSeJibSBqF/673p0mJtHf21bQOJMfF08udl8OR1sxnY/utp4wo +oc6qoEFkA8F2gRKHPNRcCokSNIrGFilMAkw+xysiYQKBgQDnYOuXIanFPeTwZhS6 +vtlahLHeLM8SC/ZAEqtalTEGJXxquCkKKwaxveWy0He2HBEDNn70bAWgFNRWTVP5 +2dj2wzCinwWpuZTMHOzFnuOaD0AE5aul/NsmrWB8EaXiPgDZ1zffW+b5RDWQ4GSO +9PM9yoGn1AfIowZV0BpWPt8ZBwKBgQDBqLuLr0FV18P0DykvLerxScjCjdfH5FOB +76LbLwtaN66P+nRcJWZJDEMcqDEOCa48W4yQtJR1xfsU2omlnXt7gK1YDm0zdqYY +ZCsNQmn1IGmd854FwfJ0ZnflM0JkpgYPhc1BkGjFDzd24fLxdozhOYw/JQl1/qlD +NhBzZWpK0QKBgGdRD+HcVkKM8L2n13nL3qJcvk/HEm+sh7j0nS9Fjy5/bp74vNqd +e8/CS98c11eqEK6uluHtmlVw95/Rx725WPhXEFCNUaoIBDlgcRmNaXRRevS02YC0 +9+a9ZGgCOWBc72RWhcxm7SxBGOooSL7NAhCFbH/UXeVpZlrLIfeA+33hAoGBAJxF +4dmteeagr6LBnTVSM3W0WcoNliPa7zw6OUvwIZQSvM24iK9XCrxFpd55JuSyESTp +p3uaDPcg5SUF2O+JLn0R8E/PjHVr+EpUgp03i7NgDXqFfW4d/N3LVlLhU2FOzDM4 +gNK4iqMCTXqkiFwrkzGoM7E83O6XqtTuysWj/+sRAoGAQdBZPveQy2dsIw5BHrew +5s9Xz1kpc2ZYOKmW3tcWAL0DCL1QeLdFiKLK4EjeTILsB9q2VboWExMGTGPw1OZM +a6ohIB++/SsFVgBRaaHXJ0wixsHYS5kfnpUxtYk5eMgO/q4DvJ0X30nFZARKY2SC +O1OjFagJYTbGmUj5Azkwhi8= +-----END PRIVATE KEY----- diff --git a/certs/jwt-public.pem b/certs/jwt-public.pem new file mode 100644 index 0000000..b934b22 --- /dev/null +++ b/certs/jwt-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwiSuZestAG3ltcjIW70 +di6yc9BfqF8FrMf6Svoe9yOPFISE1kSq5LWGK5xMnP09oqdseRGmMGEKYpmdfnF+ +YiKljv+0LZSkl0HypXHFPWA1yq5Cu6dZR5D50iEVwvsfPyLonLHi3vCLnrJmEeqn +yLOq7eh3obZV4/MlRZnNl1WbFhCMhEQly28WCUpGv5Bisy9TsEKUvJLm7i06E8Ro +SXsvK3Zt9JhAPqFgB6priiWAk3wvasM29w+F4TUyHtrzH8nmTf5uaUivYV8GhNLq +fDIjDLa/6+JAHDDIxsWtNhvbCmtrcF7xM97MK4YhGZmidrbB6pR95nBxSSn68UV0 +twIDAQAB +-----END PUBLIC KEY----- diff --git a/main.py b/main.py new file mode 100644 index 0000000..f3a2ca4 --- /dev/null +++ b/main.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from app.routers import demo +import uvicorn + +app = FastAPI() +app.include_router(demo.router) + +if __name__ == "__main__": + uvicorn.run("main:app", reload=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d11c1b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,23 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.12.0 +bcrypt==5.0.0 +cffi==2.0.0 +click==8.3.1 +cryptography==46.0.3 +dnspython==2.8.0 +email-validator==2.3.0 +fastapi==0.122.0 +h11==0.16.0 +idna==3.11 +pycparser==2.23 +pydantic==2.12.5 +pydantic-settings==2.12.0 +pydantic_core==2.41.5 +PyJWT==2.10.1 +python-dotenv==1.2.1 +python-multipart==0.0.20 +starlette==0.50.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +uvicorn==0.38.0