+Upload
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#Ignore vscode AI rules
|
||||||
|
.github\instructions\codacy.instructions.md
|
15
client/main.py
Normal file
15
client/main.py
Normal 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
2
client/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
customtkinter
|
||||||
|
websockets
|
BIN
server/__pycache__/crud.cpython-313.pyc
Normal file
BIN
server/__pycache__/crud.cpython-313.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/database.cpython-313.pyc
Normal file
BIN
server/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/models.cpython-313.pyc
Normal file
BIN
server/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/schemas.cpython-313.pyc
Normal file
BIN
server/__pycache__/schemas.cpython-313.pyc
Normal file
Binary file not shown.
BIN
server/__pycache__/security.cpython-313.pyc
Normal file
BIN
server/__pycache__/security.cpython-313.pyc
Normal file
Binary file not shown.
6
server/config.py
Normal file
6
server/config.py
Normal 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
45
server/crud.py
Normal 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
13
server/database.py
Normal 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
120
server/main.py
Normal 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
27
server/models.py
Normal 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
7
server/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
SQLAlchemy
|
||||||
|
aiosqlite
|
||||||
|
passlib[bcrypt]
|
||||||
|
python-jose[cryptography]
|
||||||
|
python-multipart
|
41
server/schemas.py
Normal file
41
server/schemas.py
Normal 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
50
server/security.py
Normal 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
|
Reference in New Issue
Block a user