commit fac5f36fc38bea9e7c5a3af7ffb7bf3ae5161c4a Author: Meor Date: Tue Sep 16 02:18:17 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7dd3667 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# виртуальное окружение +venv/ +.venv/ + +# кэш питона +__pycache__/ +*.pyc +*.pyo +*.pyd + +# IDE и редакторы +.vscode/ +.idea/ + +# OS мусор +.DS_Store +Thumbs.db + +#Подсказки +hint.py + +#env +*.env \ No newline at end of file diff --git a/Plan b/Plan new file mode 100644 index 0000000..18bf1de --- /dev/null +++ b/Plan @@ -0,0 +1,7 @@ +###Приложение для мониторинга серверов### +#План: сделать сайт для отображения информации о системных компонентах +#Сделать авторизацию на нем с валидацией pydantic и создание пользователя с fastapi + sqlalchemy +#создать простую страничку с текстом по разным серверам +#придумать способ получать эти данные с серверов или хотяб со своей машины +#отправлять их так же вначале в fast-api, а он в бд и выводить будет на сайте +#Уведомлять по тг, если значения равын чему то diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/requirements b/requirements new file mode 100644 index 0000000..8f0ad04 --- /dev/null +++ b/requirements @@ -0,0 +1,7 @@ +fastapi == 0.116.1 +SQLAlchemy == 2.0.42 +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 diff --git a/run.py b/run.py new file mode 100644 index 0000000..8f82704 --- /dev/null +++ b/run.py @@ -0,0 +1,5 @@ +import uvicorn +from server.backend import endpoints # импортируем FastAPI экземпляр из файла app.py + +if __name__ == "__main__": + uvicorn.run("server.backend.endpoints:api", host="127.0.0.1", port=8000, reload=True) \ No newline at end of file diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/backend/__init__.py b/server/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/backend/endpoints.py b/server/backend/endpoints.py new file mode 100644 index 0000000..68f7f15 --- /dev/null +++ b/server/backend/endpoints.py @@ -0,0 +1,66 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from . import pydentic +from server.database import db +import asyncio + +api = FastAPI() + +api.add_middleware( + CORSMiddleware, + allow_origins=["*"], # "*" — разрешить всем; можно указать список конкретных доменов + allow_credentials=True, + allow_methods=["*"], # GET, POST, PUT, DELETE и т.д. + allow_headers=["*"], # Разрешить любые заголовки +) + +@api.get("/", response_model=pydentic.IdofPersons) +async def get_all_rows(): + for row in await db.get_all_rows(): + if row: + 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): + user = await db.GetUser(id) + if user: + return user + else: + raise HTTPException(status_code=404, detail="The user isn't found") +@api.post("/user_create", response_model=pydentic.IdofPersons) +async def create_user(row:pydentic.CreateUser): + new_user_id = max(item.id for item in await db.get_all_rows()) + new_row = pydentic.IdofPersons(id = new_user_id, email=row.email, description=row.description, activated = row.activated, password = row.password) + await db.CreateUser(new_row) + return new_row +@api.delete("/user_delete/{id}", response_model=pydentic.IdofPersons) +async def delete_user(id: int): + user = await db.GetUser(id) + if not user: + raise HTTPException(status_code=404, detail="The user isn't found") + await db.DeleteUser(id) + return user +@api.put("/user_update/{id}", response_model=pydentic.IdofPersons) +async def update_user(id: int, updated_row: pydentic.UserUpdate): + user = await db.GetUser(id) + if not user: + raise HTTPException(status_code=404, detail="The user isn't found") + changed = False + if updated_row.email is not None and updated_row.email != user.email: + user.email = updated_row.email + changed = True + if updated_row.description is not None and updated_row.description != user.description: + user.description = updated_row.description + changed = True + if updated_row.activated is not None and updated_row.activated != user.activated: + user.activated = updated_row.activated + changed = True + if updated_row.password is not None and updated_row.password != user.password: + user.password = updated_row.password + changed = True + if changed: + await db.UpdateUser(user) + else: + pass + return user \ No newline at end of file diff --git a/server/backend/pydentic.py b/server/backend/pydentic.py new file mode 100644 index 0000000..466825a --- /dev/null +++ b/server/backend/pydentic.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, Field, EmailStr, constr,validator +from typing import List, Optional +from enum import IntEnum +#Валидация пароля +import re +def check_password_complexity(cls, password): + if password is None: + return password + if not re.search(r'[A-Za-z]', password): + raise ValueError('Password must contain at least one letter') + if not re.search(r'\d', password): + raise ValueError('Password must contain at least one digit') + if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): + raise ValueError('Password must contain at least one special symbol') + return password + +#Валидация полей с пользователями +class UsersInfo(BaseModel): + email:EmailStr = Field(..., min_length=6, max_length=254, description="email of the user") + description: str = Field(..., description="description of the user") + activated:bool = Field(..., description="Has the user activated their account") + password:constr(min_length=8) = Field(..., description="Password with min 8 chars, letters and digits") + @validator('password') + def password_validator(cls, password): + return check_password_complexity(cls, password) +class IdofPersons(UsersInfo): + id:int = Field(..., description="Unique identifier of the user") +class CreateUser(UsersInfo): + pass +class UserUpdate(BaseModel): + email:Optional[EmailStr] = Field(None, min_length=6, max_length=254, description="users' email") + description:Optional[str] = Field(None, description="description of the user") + activated:Optional[bool] = Field(None, description="Has the user activated their account") + 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 diff --git a/server/database/DB/example.db b/server/database/DB/example.db new file mode 100644 index 0000000..61b2ec3 Binary files /dev/null and b/server/database/DB/example.db differ diff --git a/server/database/__init__.py b/server/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/database/db.py b/server/database/db.py new file mode 100644 index 0000000..b67c8ab --- /dev/null +++ b/server/database/db.py @@ -0,0 +1,82 @@ + +import asyncio +#from sqlalchemy import create_engine #Не async +from sqlalchemy.orm import DeclarativeBase, sessionmaker +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy import Column, Integer, String, Boolean, select + +from pathlib import Path +db_folder = Path(__file__).parent / "DB" +db_folder.mkdir(parents=True, exist_ok=True) +db_path = db_folder / "example.db" +async_engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", echo=True) +#sqlite+aiosqlite — тип БД + async-драйвер ///example.db — путь к файлу (три слэша, если путь относительный; четыре, если абсолютный + +from passlib.context import CryptContext +#Hash password +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +def hash_password(password: str) -> str: + return pwd_context.hash(password) +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + +class Base(DeclarativeBase): + pass + +AsyncSessionLocal = sessionmaker(async_engine,class_=AsyncSession, expire_on_commit=False) + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String(254), unique=True, nullable=False) + description = Column(String, nullable=False) + activated = Column(Boolean, default=False) + password = Column(String, nullable=False) + +async def init_db(): + async with async_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) +async def CreateUser(user_info): + async with AsyncSessionLocal() as session: + new_user = User(email=user_info.email, description=user_info.description, activated=user_info.activated, password=hash_password(user_info.password)) + session.add(new_user) + await session.commit() + await session.refresh(new_user) + print(new_user.id) +async def GetUser(id): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User).where(User.id==id)) + user = result.scalar_one_or_none() + return user +async def get_all_rows(): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User)) + users = result.scalars().all() + return users +async def UpdateUser(user_info): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User).where(User.id==user_info.id)) + user = result.scalar_one_or_none() + if user: + user.email = user_info.email + user.description = user_info.description + user.activated = user_info.activated + user.password = hash_password(user_info.password) + await session.commit() +async def DeleteUser(id): + async with AsyncSessionLocal() as session: + result = await session.execute(select(User).where(User.id==id)) + user = result.scalar_one_or_none() + if user: + await session.delete(user) + await session.commit() +async def main(): + await init_db() + await CreateUser() + await get_all_rows() + # await UpdateUser(1) + # await GetUser(1) + # await DeleteUser(1) +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/server/front/login/background-image.jpeg b/server/front/login/background-image.jpeg new file mode 100644 index 0000000..179abf4 Binary files /dev/null and b/server/front/login/background-image.jpeg differ diff --git a/server/front/login/index.html b/server/front/login/index.html new file mode 100644 index 0000000..5f76402 --- /dev/null +++ b/server/front/login/index.html @@ -0,0 +1,27 @@ + + + + + + Login + + + +
+ +
+ + \ No newline at end of file diff --git a/server/front/login/style.css b/server/front/login/style.css new file mode 100644 index 0000000..739bf7d --- /dev/null +++ b/server/front/login/style.css @@ -0,0 +1,121 @@ +* { + margin: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-image: url('background-image.jpeg'); + background-size: cover; +} + +.glass-container { + width: 300px; + height: 350px; + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 10px; + border: 1px solid #fff; +} + +.glass-container::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + border-radius: 10px; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: -1; +} + +.login-box { + max-width: 250px; + margin: 0 auto; + text-align: center; +} + +h2 { + color: #fff; + margin-top: 30px; + margin-bottom: -20px; +} + +form { + display: flex; + flex-direction: column; + margin-top: 20px; +} + +input { + padding: 10px; + margin-top: 25px; + border: none; + border-radius: 10px; + background: transparent; + border: 1px solid #fff; + color: #fff; + font-size: 13px; +} + +input::placeholder { + color: #fff; +} + +input:focus { + outline: none; +} + +.options { + display: flex; + align-items: center; + margin-top: 15px; + font-size: 12px; + color: white; +} + +.options input { + margin-right: 5px; + margin-top: 0px; +} + +.options a { + text-decoration: none; + color: white; + margin-left: auto; +} + +button { + background: #fff; + color: black; + padding: 10px; + border: none; + border-radius: 10px; + cursor: pointer; + margin-top: 15px; +} + +button:hover { + background: transparent; + color: white; + outline: 1px solid #fff; +} + +p { + font-size: 12px; + color: #fff; + margin-top: 15px; +} + +#register { + text-decoration: none; + color: #fff; + font-weight: bold; +} diff --git a/server/front/main/index.html b/server/front/main/index.html new file mode 100644 index 0000000..06b5a57 --- /dev/null +++ b/server/front/main/index.html @@ -0,0 +1,19 @@ + + + + + + Main + + + +
+
+

data

+
+

Data

+
+
+
+ + \ No newline at end of file diff --git a/server/front/main/style.css b/server/front/main/style.css new file mode 100644 index 0000000..87f71e1 --- /dev/null +++ b/server/front/main/style.css @@ -0,0 +1,60 @@ +* { + margin: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-image: url('./../login/background-image.jpeg'); + background-size: cover; +} + +.glass-container { + width: 600px; + height: 700px; + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 10px; + border: 1px solid #fff; +} + +.glass-container::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + border-radius: 10px; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: -1; +} + +.data-box { + max-width: 250px; + margin: 0 auto; + text-align: center; +} + +h2 { + color: #fff; + margin-top: 30px; + margin-bottom: -20px; +} + +form { + display: flex; + flex-direction: column; + margin-top: 20px; +} +p { + font-size: 12px; + color: #fff; + margin-top: 15px; +} diff --git a/server/front/register/index.html b/server/front/register/index.html new file mode 100644 index 0000000..216b208 --- /dev/null +++ b/server/front/register/index.html @@ -0,0 +1,24 @@ + + + + + + Register + + + +
+
+

Register

+
+ + + + +

Do you have an account? Login

+
+
+
+ + + \ No newline at end of file diff --git a/server/front/register/js.js b/server/front/register/js.js new file mode 100644 index 0000000..f995aca --- /dev/null +++ b/server/front/register/js.js @@ -0,0 +1,70 @@ +document.getElementById('registerForm').addEventListener('submit', async function (e) { + e.preventDefault(); + + const email = document.getElementById('email').value; + const password = document.getElementById('password').value; + const confirmPassword = document.getElementById('confirm_password').value; + + if (password !== confirmPassword) { + showError(['Passwords are different!']); + return; + } + + const userData = { + email, + description: "string", + activated: true, + password + }; + + try { + const response = await fetch('http://localhost:8000/user_create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(userData) + }); + + if (response.ok) { + window.location.href = './../login/index.html'; + } else { + const err = await response.json(); + if (Array.isArray(err.detail)) { + const messages = err.detail.map(e => { + const field = e.loc.filter(locPart => locPart !== 'body').join(' -> '); + return `${field}: ${e.msg}`; + }); + showError(messages); + } else if (typeof err.detail === 'string') { + showError([err.detail]); + } + } + } catch { + showError(['Connection timeout']); + } +}); + +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%"; + container.style.height = "auto"; + const form = document.getElementById('registerForm'); + 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/register/style.css b/server/front/register/style.css new file mode 100644 index 0000000..65cf560 --- /dev/null +++ b/server/front/register/style.css @@ -0,0 +1,102 @@ +* { + margin: 0; + box-sizing: border-box; + font-family: 'Poppins', sans-serif; +} + +body { + margin: 0; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-image: url('./../login/background-image.jpeg'); + background-size: cover; +} + +.glass-container { + width: 300px; + height: 350px; + position: relative; + z-index: 1; + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + border-radius: 10px; + border: 1px solid #fff; +} + +.glass-container::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + border-radius: 10px; + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); + z-index: -1; +} + +.register-box { + max-width: 250px; + margin: 0 auto; + text-align: center; +} + +h2 { + color: #fff; + margin-top: 30px; + margin-bottom: -20px; +} + +form { + display: flex; + flex-direction: column; + margin-top: 20px; +} + +input { + padding: 10px; + margin-top: 25px; + border: none; + border-radius: 10px; + background: transparent; + border: 1px solid #fff; + color: #fff; + font-size: 13px; +} + +input::placeholder { + color: #fff; +} + +input:focus { + outline: none; +} + +button { + background: #fff; + color: black; + padding: 10px; + border: none; + border-radius: 10px; + cursor: pointer; + margin-top: 15px; +} + +button:hover { + background: transparent; + color: white; + outline: 1px solid #fff; +} + +p { + font-size: 12px; + color: #fff; + margin-top: 15px; +} + +#Login { + text-decoration: none; + color: #fff; + font-weight: bold; +}