This commit is contained in:
Robin
2025-10-04 20:08:48 +02:00
commit f05dda95c3
17 changed files with 330 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
#Ignore vscode AI rules
.github\instructions\codacy.instructions.md

15
client/main.py Normal file
View File

@@ -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()

2
client/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
customtkinter
websockets

BIN
pycord.db Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

6
server/config.py Normal file
View File

@@ -0,0 +1,6 @@
import secrets
# JWT settings
SECRET_KEY = secrets.token_urlsafe(32)
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

45
server/crud.py Normal file
View File

@@ -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

13
server/database.py Normal file
View File

@@ -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

120
server/main.py Normal file
View File

@@ -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)

27
server/models.py Normal file
View File

@@ -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")

7
server/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
SQLAlchemy
aiosqlite
passlib[bcrypt]
python-jose[cryptography]
python-multipart

41
server/schemas.py Normal file
View File

@@ -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

50
server/security.py Normal file
View File

@@ -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