commit f05dda95c3f4ee0f5feea7f49b14c9e469bdb59c Author: Robin Date: Sat Oct 4 20:08:48 2025 +0200 +Upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c469d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + + +#Ignore vscode AI rules +.github\instructions\codacy.instructions.md diff --git a/client/main.py b/client/main.py new file mode 100644 index 0000000..85448c7 --- /dev/null +++ b/client/main.py @@ -0,0 +1,15 @@ +import customtkinter + +class App(customtkinter.CTk): + def __init__(self): + super().__init__() + + self.title("Pycord Client") + self.geometry("800x600") + + self.label = customtkinter.CTkLabel(self, text="Willkommen bei Pycord!", font=("Roboto", 24)) + self.label.pack(pady=20) + +if __name__ == "__main__": + app = App() + app.mainloop() diff --git a/client/requirements.txt b/client/requirements.txt new file mode 100644 index 0000000..eaa008b --- /dev/null +++ b/client/requirements.txt @@ -0,0 +1,2 @@ +customtkinter +websockets diff --git a/pycord.db b/pycord.db new file mode 100644 index 0000000..9cca31b Binary files /dev/null and b/pycord.db differ diff --git a/server/__pycache__/crud.cpython-313.pyc b/server/__pycache__/crud.cpython-313.pyc new file mode 100644 index 0000000..b578e24 Binary files /dev/null and b/server/__pycache__/crud.cpython-313.pyc differ diff --git a/server/__pycache__/database.cpython-313.pyc b/server/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..1e37286 Binary files /dev/null and b/server/__pycache__/database.cpython-313.pyc differ diff --git a/server/__pycache__/models.cpython-313.pyc b/server/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..1263398 Binary files /dev/null and b/server/__pycache__/models.cpython-313.pyc differ diff --git a/server/__pycache__/schemas.cpython-313.pyc b/server/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000..ca76a5e Binary files /dev/null and b/server/__pycache__/schemas.cpython-313.pyc differ diff --git a/server/__pycache__/security.cpython-313.pyc b/server/__pycache__/security.cpython-313.pyc new file mode 100644 index 0000000..8a7af58 Binary files /dev/null and b/server/__pycache__/security.cpython-313.pyc differ diff --git a/server/config.py b/server/config.py new file mode 100644 index 0000000..3fc6bcc --- /dev/null +++ b/server/config.py @@ -0,0 +1,6 @@ +import secrets + +# JWT settings +SECRET_KEY = secrets.token_urlsafe(32) +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 diff --git a/server/crud.py b/server/crud.py new file mode 100644 index 0000000..f291426 --- /dev/null +++ b/server/crud.py @@ -0,0 +1,45 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + +import models +import schemas +from security import get_password_hash, verify_password + +# --- User CRUD --- + +async def get_user_by_username(db: AsyncSession, username: str): + result = await db.execute(select(models.User).filter(models.User.username == username)) + return result.scalars().first() + +async def create_user(db: AsyncSession, user: schemas.UserCreate): + hashed_password = get_password_hash(user.password) + db_user = models.User(username=user.username, hashed_password=hashed_password) + db.add(db_user) + await db.commit() + await db.refresh(db_user) + return db_user + +async def authenticate_user(db: AsyncSession, username: str, password: str): + user = await get_user_by_username(db, username=username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + +# --- Server CRUD --- + +async def get_server(db: AsyncSession, server_id: int): + result = await db.execute(select(models.Server).filter(models.Server.id == server_id)) + return result.scalars().first() + +async def get_servers(db: AsyncSession, skip: int = 0, limit: int = 100): + result = await db.execute(select(models.Server).offset(skip).limit(limit)) + return result.scalars().all() + +async def create_server(db: AsyncSession, server: schemas.ServerCreate, owner_id: int): + db_server = models.Server(**server.dict(), owner_id=owner_id) + db.add(db_server) + await db.commit() + await db.refresh(db_server) + return db_server diff --git a/server/database.py b/server/database.py new file mode 100644 index 0000000..23d3f84 --- /dev/null +++ b/server/database.py @@ -0,0 +1,13 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker + +DATABASE_URL = "sqlite+aiosqlite:///./pycord.db" + +engine = create_async_engine(DATABASE_URL, echo=True) +async_session = sessionmaker( + engine, class_=AsyncSession, expire_on_commit=False +) + +async def get_session() -> AsyncSession: + async with async_session() as session: + yield session \ No newline at end of file diff --git a/server/main.py b/server/main.py new file mode 100644 index 0000000..ac8b439 --- /dev/null +++ b/server/main.py @@ -0,0 +1,120 @@ +import uvicorn +from contextlib import asynccontextmanager +from datetime import timedelta +from typing import List +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.ext.asyncio import AsyncSession + +from database import engine, get_session +from models import Base, User, Server +import crud +import schemas +import security +from config import ACCESS_TOKEN_EXPIRE_MINUTES + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # on startup + async with engine.begin() as conn: + # await conn.run_sync(Base.metadata.drop_all) + await conn.run_sync(Base.metadata.create_all) + yield + # on shutdown + # (nothing to do here for now) + + +app = FastAPI(lifespan=lifespan) + +# --- Authentication --- + +@app.post("/token", response_model=schemas.Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_session)): + user = await crud.authenticate_user(db, username=form_data.username, password=form_data.password) + if not user: + raise HTTPException( + status_code=401, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = security.create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + +# --- Users --- + +@app.post("/users/", response_model=schemas.User) +async def create_user(user: schemas.UserCreate, db: AsyncSession = Depends(get_session)): + db_user = await crud.get_user_by_username(db, username=user.username) + if db_user: + raise HTTPException(status_code=400, detail="Username already registered") + return await crud.create_user(db=db, user=user) + +@app.get("/users/me/", response_model=schemas.User) +async def read_users_me(current_user: User = Depends(security.get_current_user)): + return current_user + +# --- Servers --- + +@app.post("/servers/", response_model=schemas.Server) +async def create_server( + server: schemas.ServerCreate, + db: AsyncSession = Depends(get_session), + current_user: User = Depends(security.get_current_user) +): + return await crud.create_server(db=db, server=server, owner_id=current_user.id) + +@app.get("/servers/", response_model=List[schemas.Server]) +async def read_servers(skip: int = 0, limit: int = 100, db: AsyncSession = Depends(get_session)): + servers = await crud.get_servers(db, skip=skip, limit=limit) + return servers + +@app.get("/servers/{server_id}", response_model=schemas.Server) +async def read_server(server_id: int, db: AsyncSession = Depends(get_session)): + db_server = await crud.get_server(db, server_id=server_id) + if db_server is None: + raise HTTPException(status_code=404, detail="Server not found") + return db_server + +# --- General --- + +@app.get("/") +async def read_root(): + return {"message": "Pycord server is running"} + +# --- WebSocket --- + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + +manager = ConnectionManager() + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat") + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/server/models.py b/server/models.py new file mode 100644 index 0000000..c247dd5 --- /dev/null +++ b/server/models.py @@ -0,0 +1,27 @@ +from sqlalchemy import ( + Column, + Integer, + String, + ForeignKey, +) +from sqlalchemy.orm import relationship, declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + + owned_servers = relationship("Server", back_populates="owner") + +class Server(Base): + __tablename__ = "servers" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="owned_servers") \ No newline at end of file diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..76d3608 --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,7 @@ +fastapi +uvicorn[standard] +SQLAlchemy +aiosqlite +passlib[bcrypt] +python-jose[cryptography] +python-multipart \ No newline at end of file diff --git a/server/schemas.py b/server/schemas.py new file mode 100644 index 0000000..f4d7a72 --- /dev/null +++ b/server/schemas.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel +from typing import List + +# --- Token Schemas --- + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + username: str | None = None + +# --- Server Schemas --- + +class ServerBase(BaseModel): + name: str + +class ServerCreate(ServerBase): + pass + +class Server(ServerBase): + id: int + owner_id: int + + class Config: + orm_mode = True + +# --- User Schemas --- + +class UserBase(BaseModel): + username: str + +class UserCreate(UserBase): + password: str + +class User(UserBase): + id: int + owned_servers: List[Server] = [] + + class Config: + orm_mode = True diff --git a/server/security.py b/server/security.py new file mode 100644 index 0000000..1fa182a --- /dev/null +++ b/server/security.py @@ -0,0 +1,50 @@ +from datetime import datetime, timedelta +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession + +from config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES +import crud +from database import get_session +import schemas + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password): + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_session)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = schemas.TokenData(username=username) + except JWTError: + raise credentials_exception + user = await crud.get_user_by_username(db, username=token_data.username) + if user is None: + raise credentials_exception + return user