+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