From 6b7a57dc6d75e3cd169a9d668096c483c7311b0c Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Fri, 19 Sep 2025 18:17:31 +0300 Subject: [PATCH] JWT tokens 1.0 --- .gitignore | 3 +- requirements | 3 +- server/backend/JWT.py | 27 ++++++++++++++ server/backend/endpoints.py | 28 ++++++++++++--- server/backend/pydentic.py | 5 ++- server/database/DB/example.db | Bin 16384 -> 16384 bytes server/database/db.py | 7 ++++ server/front/login/index.html | 5 +-- server/front/login/js.js | 65 ++++++++++++++++++++++++++++++++++ server/front/main/index.html | 1 + server/front/main/js.js | 4 +++ server/front/register/js.js | 2 +- 12 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 server/backend/JWT.py create mode 100644 server/front/login/js.js create mode 100644 server/front/main/js.js diff --git a/.gitignore b/.gitignore index 7dd3667..a17b01d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ Thumbs.db hint.py #env -*.env \ No newline at end of file +*.env +db.py \ No newline at end of file diff --git a/requirements b/requirements index 8f0ad04..fa64fe4 100644 --- a/requirements +++ b/requirements @@ -4,4 +4,5 @@ pytest == 8.4.1 aiosqlite == 0.21.0 greenlet == 3.2.4 passlib == 1.7.4 -bcrypt == 4.3.0 \ No newline at end of file +bcrypt == 4.3.0 +python-jose[cryptography] == 3.5.0 \ No newline at end of file diff --git a/server/backend/JWT.py b/server/backend/JWT.py new file mode 100644 index 0000000..88f2fa5 --- /dev/null +++ b/server/backend/JWT.py @@ -0,0 +1,27 @@ +from datetime import datetime, timedelta +from jose import JWTError, jwt +from fastapi import HTTPException, Depends, status +from fastapi.security import OAuth2PasswordBearer + +SECRET_KEY = "super-secret-string" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") + +async def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15)) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +async def current_user(token: str = Depends(oauth2_scheme)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + email: str = payload.get("sub") + if email is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + return email + except JWTError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") \ No newline at end of file diff --git a/server/backend/endpoints.py b/server/backend/endpoints.py index 68f7f15..3a98b4c 100644 --- a/server/backend/endpoints.py +++ b/server/backend/endpoints.py @@ -1,7 +1,10 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, status, Depends from fastapi.middleware.cors import CORSMiddleware -from . import pydentic +from . import pydentic, JWT +from datetime import datetime, timedelta +from pydantic import EmailStr from server.database import db + import asyncio api = FastAPI() @@ -14,6 +17,10 @@ api.add_middleware( allow_headers=["*"], # Разрешить любые заголовки ) +@api.get("/protected") +async def protected(current_user: str = Depends(JWT.current_user)): + return {"msg": f"Hello, {current_user}"} + @api.get("/", response_model=pydentic.IdofPersons) async def get_all_rows(): for row in await db.get_all_rows(): @@ -21,8 +28,8 @@ async def get_all_rows(): return row else: raise HTTPException(status_code=404, detail="The user isn't found") -@api.get("/get_user/{id}", response_model=pydentic.IdofPersons) -async def get_user(id:int): +@api.get("/get_user_by_id/{id}", response_model=pydentic.IdofPersons) +async def get_user(id: int, current_user: str = Depends(JWT.current_user)): user = await db.GetUser(id) if user: return user @@ -63,4 +70,15 @@ async def update_user(id: int, updated_row: pydentic.UserUpdate): await db.UpdateUser(user) else: pass - return user \ No newline at end of file + return user +@api.post("/login") +async def login_user(row: pydentic.UserLogin): + user = await db.LoginUser(row) + if not user: + raise HTTPException(status_code=401, detail="The user isn't found") + + token = await JWT.create_access_token( + {"sub": user.email}, + timedelta(minutes=JWT.ACCESS_TOKEN_EXPIRE_MINUTES) + ) + return {"access_token": token, "token_type": "bearer"} \ No newline at end of file diff --git a/server/backend/pydentic.py b/server/backend/pydentic.py index 466825a..ddacc84 100644 --- a/server/backend/pydentic.py +++ b/server/backend/pydentic.py @@ -34,4 +34,7 @@ class UserUpdate(BaseModel): password:Optional[constr(min_length=8)] = Field(None, description="Password with min 8 chars, letters and digits") @validator('password') def password_validator(cls, password): - return check_password_complexity(cls, password) \ No newline at end of file + return check_password_complexity(cls, password) +class UserLogin(BaseModel): + email:EmailStr = Field(..., min_length=6, max_length=254, description="user's email") + password:str = Field(..., description="Password") \ No newline at end of file diff --git a/server/database/DB/example.db b/server/database/DB/example.db index 61b2ec3bf3f4d9b750cf4c8f83d87585a9656385..2a6781bd8bf57cde87cf647e112f52193fdef538 100644 GIT binary patch delta 187 zcmZo@U~Fh$oFL68Gf~EwQD$Sp5`HdbzE2GN^Y~r)K5Z6MxXTw6#mvs2FUc9m%FZAw z$=S%7l9`+6kfN7TTvC*om#$)zq+)2K;uuzF78x9pUmTf|WtLHu=8>isUhZF*XyNVU zUFGRh5Li-GnVFSom=@$;WR?^jkze7I>pt04em@s8{}BfMBm5WnpKKO1*vu~=%B;`B hEXxS9g>~{pePf`aR}B0g_#-5`Hcwepd$mdHk+?UpEUX+~tdkVrFO1m*i|@Ee&C1 zXHW*QauW?Ra~#SG)AWjpONuh{(p8L-R1A$&aufBF0}YLwgF;Kf^?Zzr0`n}gBlWUe z68)2c-O~yJv%|dogR%@NixOj4*dtjcyUOneT6d9w|04er{wJFS4L0)&h%)Ol@-r(l j0__lIHWp!?d{N&RsOSR&{|EkG{I7tDZt_olZZ8P{8AdiX diff --git a/server/database/db.py b/server/database/db.py index b67c8ab..afe0e1d 100644 --- a/server/database/db.py +++ b/server/database/db.py @@ -71,6 +71,13 @@ async def DeleteUser(id): if user: await session.delete(user) await session.commit() +async def LoginUser(user_info): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User).where(User.email == user_info.email)) + user = result.scalar_one_or_none() + if user and verify_password(user_info.password, user.password): + return user + return None async def main(): await init_db() await CreateUser() diff --git a/server/front/login/index.html b/server/front/login/index.html index 5f76402..f202c7c 100644 --- a/server/front/login/index.html +++ b/server/front/login/index.html @@ -10,8 +10,8 @@
+ \ No newline at end of file diff --git a/server/front/login/js.js b/server/front/login/js.js new file mode 100644 index 0000000..2044542 --- /dev/null +++ b/server/front/login/js.js @@ -0,0 +1,65 @@ +document.getElementById('loginForm').addEventListener('submit', async function (e) { + e.preventDefault(); + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + const userData = { + email, + password + }; + try { + const response = await fetch('http://localhost:8000/login', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }); + + const data = await response.json(); // читаем только один раз + + if (response.ok) { + localStorage.setItem("token", data.access_token); // сохраняем только при успехе + window.location.href = './../main/index.html'; + } else { + if (Array.isArray(data.detail)) { + const messages = data.detail.map(e => { + const field = e.loc.filter(locPart => locPart !== 'body').join(' -> '); + return `${field}: ${e.msg}`; + }); + showError(messages); + } else if (typeof data.detail === 'string') { + showError([data.detail]); + } else { + showError(['Unknown error']); + } + } + } catch (err) { + showError(['Connection error: ' + err.message]); + } +}); +function showError(messages){ + let errorElem = document.getElementById('formError'); + let container = document.getElementById('glass-container'); + if (!errorElem){ + errorElem = document.createElement('div'); + errorElem.style.transition="3s"; + errorElem.id = 'formError'; + errorElem.style.color = 'red'; + errorElem.style.marginTop = '20px'; + errorElem.style.fontSize = '14px'; + errorElem.style.fontWeight = '100'; + errorElem.style.marginBottom = '20px'; + errorElem.style.lineHeight="120%"; + errorElem.style.height = 'auto'; + const form = document.getElementById('loginForm'); + form.insertAdjacentElement('afterend', errorElem); + }; + errorElem.innerHTML = ''; + messages.forEach(msg => { + const li = document.createElement('li'); + li.style.listStyleType="none"; + li.textContent = msg; + errorElem.appendChild(li); + }); +} \ No newline at end of file diff --git a/server/front/main/index.html b/server/front/main/index.html index 06b5a57..595914b 100644 --- a/server/front/main/index.html +++ b/server/front/main/index.html @@ -15,5 +15,6 @@
+ \ No newline at end of file diff --git a/server/front/main/js.js b/server/front/main/js.js new file mode 100644 index 0000000..870c806 --- /dev/null +++ b/server/front/main/js.js @@ -0,0 +1,4 @@ +const token = localStorage.getItem("token"); +if (!token) { + window.location.href = "./../register/index.html"; +} \ No newline at end of file diff --git a/server/front/register/js.js b/server/front/register/js.js index f995aca..deb1512 100644 --- a/server/front/register/js.js +++ b/server/front/register/js.js @@ -59,7 +59,7 @@ function showError(messages) { container.style.height = "auto"; const form = document.getElementById('registerForm'); form.insertAdjacentElement('afterend', errorElem); - } + }; errorElem.innerHTML = ''; messages.forEach(msg => { const li = document.createElement('li');