Compare commits

...
Sign in to create a new pull request.

26 commits

Author SHA1 Message Date
bdrtr
250d3f1f15 crud for collections 2025-05-14 18:29:46 +03:00
bdrtr
bf71979982 new database system 2025-05-14 16:31:46 +03:00
bdrtr
039b877241 item-create 2025-05-08 22:43:42 +03:00
bdrtr
7c35097c88 items-models-import-error 2025-05-08 19:58:41 +03:00
bdrtr
938f950646 generated _collections class 2025-05-07 20:15:51 +03:00
bdrtr
842c127817 user process 2025-05-07 18:33:35 +03:00
bdrtr
36da53a562 base model 2025-05-06 14:06:15 +03:00
83389e0c10 Revert "Merge pull request 'add updates to show bedir' (#14) from another into main"
This reverts commit 99f611b3d0, reversing
changes made to d5588dd055.
2025-05-05 21:25:00 +03:00
99f611b3d0 Merge pull request 'add updates to show bedir' (#14) from another into main
Reviewed-on: #14
2025-05-05 21:07:45 +03:00
1d3d74d9d6 add updates to show bedir 2025-05-05 20:38:37 +03:00
d5588dd055 add dependency 2025-05-05 20:11:38 +03:00
c2c9ada99e remove unused imports from router.py 2025-05-05 19:07:40 +03:00
9edf8846dd remove excess routes for now 2025-05-05 19:07:17 +03:00
c80fcda493 remove unused tokendata 2025-05-05 19:06:39 +03:00
63fb875d1b delete unused imports 2025-05-05 19:04:46 +03:00
035b0cf172 add bcrypt 2025-05-05 18:20:04 +03:00
02c22fba58 add explanation 2025-05-05 18:20:04 +03:00
1a87ac79ee ignore data folder and use it for database 2025-05-05 18:19:50 +03:00
08bafaebc1 fix: update JWT package reference to pyjwt for clarity 2025-05-05 18:19:33 +03:00
f6356059ba delete uncommented sections 2025-05-05 18:19:33 +03:00
948eb75416 use simpler dotenv file 2025-05-05 18:19:33 +03:00
171a94f174 add some missing libraries 2025-05-05 18:17:18 +03:00
8fa5d38f82 add flake support 2025-05-05 18:17:18 +03:00
472aa83b3e README.md Güncelle 2025-05-05 17:50:08 +03:00
bdrtr
9e64832ba0 every changed processed 2025-05-05 17:47:53 +03:00
bdrtr
8cefa60d3a struct 2025-05-05 15:02:44 +03:00
17 changed files with 748 additions and 273 deletions

9
.gitignore vendored
View file

@ -155,10 +155,7 @@ dmypy.json
# Cython debug symbols # Cython debug symbols
cython_debug/ cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# database
data/

View file

@ -1,2 +1,9 @@
# backend # backend
This project requires a `.env` file with the context of:
```
SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
DATABASE_URL=postgresql://postgres_user:postgres_password@localhost:5434/postgres_db
```

View file

@ -0,0 +1 @@

View file

@ -1,26 +1,32 @@
from enum import Enum from enum import Enum
from backend.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES import random
from backend.config import pwd_context import smtplib
from backend.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES ,pwd_context, get_session_db, Base, user_collection
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import Depends, HTTPException from fastapi import Depends, HTTPException
from typing import Annotated from typing import Annotated
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from pydantic.networks import EmailStr
from sqlalchemy import Integer, DateTime, ForeignKey
from sqlalchemy.orm import Session, relationship, mapped_column, Mapped
from sqlalchemy.dialects.postgresql import ARRAY
from email.message import EmailMessage
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..collectionObj.models import CollectionsDB #iç içe import döngüsünü önlemek için TYPE_CHECKING kullanıyoruz
import jwt import jwt
class Token(BaseModel): class Token(BaseModel):
access_token : str access_token: str
token_type : str token_type: str
class TokenData(BaseModel):
username : str | None = None
role : str | None = None
status : str | None = None
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login") ### ENUMS ###
#### ENUMS ####
class Role(str, Enum): class Role(str, Enum):
user = "user" user = "user"
admin = "admin" admin = "admin"
@ -32,47 +38,61 @@ class Status(str, Enum):
banned = "banned" banned = "banned"
suspended = "suspended" suspended = "suspended"
class User(BaseModel): ### KULLANICI MODELLERİ ### sqlalchemy ve pydantic modelleri farklıdır
username : str | None = None class UserBase(BaseModel): #bu bir veri tabanı modeli değil !!!! lütfen dikkat et
user_id : int | None = None username: str | None = None #Option yerine Union kullanabilirsin
role : Role | None = None role: Role | None = None
status : Status | None = None status: Status | None = None
class UserInDb(UserBase):
class UserInDb(User): user_id: int | None = None
hashed_password : str | None = None email: EmailStr | None = None
hashed_password: str | None = None
class UserPublic(BaseModel): class UserPublic(BaseModel):
username : str | None = None username : str | None = None
role : Role | None = None role : Role | None = None
status : Status | None = None status : Status | None = None
user_id : int | None = None
class UserCreate(BaseModel):
username: str | None = None
role: Role | None = None
email : EmailStr | None = None
status: Status | None = None
password : str | None = None
fake_db = { ### VERİTABANI MODELİ ###
"bedir": { class DBUser(Base):
"username": "bedir", __tablename__ = "users_table"
"user_id": 1,
"hashed_password": "$2a$12$mYGWGo9c3Di3SJyYjYf3XOAsu5nP8jekf3KTItO9pbUBEm5BcapRO", # Bcrypt örneği
"role": Role.user,
"status": Status.active,
},
"alice": {
"username": "alice",
"user_id": 2,
"hashed_password": "$2b$12$Alic3FakeHashedPasSw0rdxxxxxxxyyyyyyzzzzzz",
"role": Role.user,
"status": Status.suspended,
},
"adminuser": {
"username": "adminuser",
"user_id": 3,
"hashed_password": "$2b$12$AdminFakeHashedPasSw0rdxxxxxxxyyyyyyzzzzzz",
"role": Role.admin,
"status": Status.active,
}
}
user_id: Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True)
#collection_id : Mapped[list[int]] = mapped_column(Integer, ForeignKey("collections_table.collection_id"), nullable=True) # collection_id ile ilişki
username : Mapped[str] = mapped_column(unique=True, index=True, nullable=False)
email : Mapped[str] = mapped_column(unique=True, index=True, nullable=False)
hashed_password : Mapped[str] = mapped_column(nullable=False)
role : Mapped[Role] = mapped_column(default=Role.user)
status : Mapped[Status] = mapped_column(default=Status.active)
created_date : Mapped[datetime] = mapped_column(DateTime, default=datetime.now()) #datetime.datetime -> python, DateTime -> sqlalchemy
bio : Mapped[str] = mapped_column(default="No bio")
follow_users : Mapped[list[int]] = mapped_column(ARRAY(Integer), default=[]) # takip edilen kullanıcılar
# -> buralar diğer tablolar ile olan ilişkiler
#items : Mapped[list['Items']] = relationship("Items", back_populates="user", cascade="all, delete-orphan") items'e direk değil collection üzerinden erişiyoruz
collections : Mapped[list['CollectionsDB']] = relationship(
"CollectionsDB",
secondary=user_collection,
back_populates="users",
lazy='select'
) # collection'lar ile olan ilişki
### AUTH ###
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
### SERVİSLER ###
def verify_password(plain_password: str, hashed_password: str) -> bool: def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password) return pwd_context.verify(plain_password, hashed_password)
@ -80,18 +100,21 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
def get_password_hash(password: str) -> str: def get_password_hash(password: str) -> str:
return pwd_context.hash(password) return pwd_context.hash(password)
def authenticate_user(fake_db, username: str, password: str) -> UserInDb | bool: def authenticate_user(
user = fake_db.get(username) session: Annotated[Session, Depends(get_session_db)],
if not user: username: str,
return False password: str
if not verify_password(password, user["hashed_password"]): ) -> UserInDb | None:
return False
user = session.query(DBUser).filter(DBUser.username == username).first()
if user is None or not verify_password(password, user.hashed_password): #sqlalchemy'de bu şekilde kontrol ediliyor None ile
return None
return user return user
def create_access_token( def create_access_token(
data : dict, data: dict,
expires_delta : Annotated[timedelta, None] = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES), expires_delta: Annotated[timedelta, None] = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
) -> str: ) -> str:
to_encode = data.copy() to_encode = data.copy()
expire = datetime.now(timezone.utc) + expires_delta expire = datetime.now(timezone.utc) + expires_delta
@ -100,79 +123,137 @@ def create_access_token(
return encoded_jwt return encoded_jwt
def get_user(db, username: str) -> UserInDb | None: async def get_user(
if username in db: session: Annotated[Session, Depends(get_session_db)],
user_dict = db[username] username: str
return UserInDb(**user_dict) ) -> UserInDb | None:
return None
user = session.query(DBUser).filter(DBUser.username == username).first()
return user
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
session: Annotated[Session, Depends(get_session_db)]
) -> UserPublic:
def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> UserPublic | None:
credentials_exception = HTTPException( credentials_exception = HTTPException(
status_code=401, status_code=401,
detail="Burda bir hata var", detail="Invalid credentials currently",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
try: try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
token_data = TokenData(**payload) username : str | None = payload.get("sub")
token_data.username = payload.get("sub") user = UserInDb.model_validate(payload)
username : str = token_data.username
if username is None: if username is None:
raise credentials_exception raise credentials_exception
except jwt.PyJWTError: except jwt.PyJWTError:
raise credentials_exception raise credentials_exception
user = get_user(fake_db, username=username) user = await get_user(session, username)
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return user return user
async def get_current_active_user( async def get_current_active_user(
current_user : Annotated[UserInDb, Depends(get_current_user)] current_user: Annotated[UserInDb, Depends(get_current_user)]
) -> UserPublic | None: ) -> UserPublic:
if current_user.status == Status.banned: if current_user.status == Status.banned:
raise HTTPException(status_code=400, detail="Inactive user") raise HTTPException(status_code=400, detail="Inactive user")
return current_user return current_user
"""
class User(BaseModel):
username : str
name : str | None = None
surname : str | None = None
email : EmailStr | None = None
role : Role | None = None
status : Status | None = None
bio : str | None = None
created_date : datetime | None = None
collections : list[str] | None = None ### Kullanıcı kaydı
items = list[str] | None = None def register_user(
session: Annotated[Session, Depends(get_session_db)],
user: Annotated[UserCreate, Depends()]
) -> UserPublic:
class UserInDB(User): user_dict = user.dict() # kullanıcıdan gelen verileri alıyoruz çunku şifreyi hashleyeceğiz
hashed_password : str | None = None user_dict['hashed_password'] = get_password_hash(user.password) # şifreyi hashliyoruz
class UserSelfProfile(BaseModel): if not verify_password(user.password, user_dict['hashed_password']):
username : str raise HTTPException(status_code=400, detail="Password hashing failed") # şifre hashleme işlemi başarısız oldu
name : str | None = None
surname : str | None = None
email : EmailStr | None = None
role : Role | None = None
status : Status | None = None
bio : str | None = None
created_date : datetime | None = None
collections : list[str] | None = None # Kullanıcı adı ve e-posta adresinin benzersiz olduğunu kontrol et
items = list[str] | None = None existing_user = session.query(DBUser).filter(
(DBUser.username == user.username) | (DBUser.email == user.email)
).first()
class UserPublicProfile(BaseModel): if existing_user:
username : str raise HTTPException(status_code=400, detail="Username or email already registered")
role : Role | None = None
bio : str | None = None user_dict['created_date'] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") # kullanıcı oluşturulma tarihi
created_date : datetime | None = None user_dict.pop('password') ##password'u veri tabanına eklemiyoruz zaten sınıfımızda tanımlı değil hata verir
collections : list[str] | None = None db_user = DBUser(**user_dict) #alchemy ile pydantic modelleri farklıdır bir birine
items = list[str] | None = None session.add(db_user) # donuşum yaparken dikkat et
session.commit()
session.refresh(db_user)
return db_user
"""
def find_user_w_email(
session: Annotated[Session, Depends(get_session_db)],
email: EmailStr | None = None,
):
exist_user = session.query(DBUser).filter(DBUser.email == email).first() #email ile kullanıcıyı bul
if exist_user is None:
raise HTTPException(status_code=400, detail="User not found")
if exist_user.status == Status.banned:
raise HTTPException(status_code=400, detail="Inactive user")
return True
def send_password_to_email(
session: Annotated[Session, Depends(get_session_db)],
email: EmailStr | None = None,
) -> str:
msg = EmailMessage() #obje oluştur
msg['Subject'] = 'Password Reset'
msg['From'] = 'hansneiumann@gmail.com'
msg['To'] = email
veritification_code = generate_password_reset_number()
msg.set_content(veritification_code)
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
smtp.login("hansneiumann@gmail.com", "rwaq mbil lzut dgja")
smtp.send_message(msg)
update_password_w_email(session, email=email, password=veritification_code) #şifreyi güncelle
def generate_password_reset_number() -> str:
return str(random.randint(10000000, 99999999)) # 8 haneli rastgele bir sayı döndür
def update_password_w_email(
session: Annotated[Session, Depends(get_session_db)],
password: str | None = None,
email: EmailStr | None = None,
) -> dict:
hashed_password = get_password_hash(password)
session.query(DBUser).filter(DBUser.email == email).update({"hashed_password": hashed_password})
session.commit()
return {"message": "Password updated successfully"}
def update_password_w_user(
session: Annotated[Session, Depends(get_session_db)],
user: Annotated[DBUser , None],
password: str | None = None,
) -> any:
hashed_password = get_password_hash(password)
session.query(DBUser).filter(DBUser.user_id == user.user_id).update({"hashed_password": hashed_password})
session.commit()

View file

@ -1,10 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks
from .models import UserInDb, User, Role, Token, UserPublic from .models import Token, UserPublic, authenticate_user, create_access_token, UserCreate, find_user_w_email, get_current_user, register_user, send_password_to_email, update_password_w_user
from .models import get_current_active_user, authenticate_user, create_access_token , fake_db, get_current_user from datetime import timedelta
from datetime import timedelta, datetime, timezone from typing import Annotated
from ..config import ACCESS_TOKEN_EXPIRE_MINUTES from ..config import get_session_db
from typing import Annotated, Optional
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from pydantic.networks import EmailStr
router = APIRouter( router = APIRouter(
@ -14,37 +15,61 @@ router = APIRouter(
dependencies=[], dependencies=[],
) )
@router.get("/me")
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)],
) -> UserPublic:
return current_user
def ADMIN(current_user: Annotated[UserInDb, Depends(get_current_user)]):
if current_user.role != Role.admin:
raise HTTPException(status_code=400, detail="You are not admin")
return current_user
@router.get('/home')
async def home(current_user : Annotated[User, Depends(ADMIN)]):
return {"message" : f"Welcome to home page {current_user.username}"}
@router.post('/login') @router.post('/login')
async def login_for_access_token( async def login_for_access_token(
form_data : Annotated[OAuth2PasswordRequestForm, Depends()], form_data : Annotated[OAuth2PasswordRequestForm, Depends()],
session : Annotated[Session, Depends(get_session_db)],
) -> Token: ) -> Token:
user = authenticate_user(fake_db, form_data.username, form_data.password) user = authenticate_user(session, form_data.username, form_data.password)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password", detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token_expires = timedelta(minutes=30)
access_token = create_access_token( access_token = create_access_token(
data={"sub": user['username'], "role": user['role'], 'status': user['status']}, expires_delta=access_token_expires data={"sub": user.username, "role": user.role, 'status': user.status}, expires_delta=access_token_expires
) )
return Token(access_token=access_token, token_type="bearer") return Token(access_token=access_token, token_type="bearer")
@router.post('/register', response_model=UserPublic) #userPublic güvenli bir model
async def create_user(
session : Annotated[Session, Depends(get_session_db)],
user : Annotated[UserCreate, Depends()]
):
return register_user(session, user)
@router.post('/password_reset')
async def password_reset(
session : Annotated[Session, Depends(get_session_db)],
email : Annotated[EmailStr, None] = None,
task: Annotated[BackgroundTasks, None] = None, # BackgroundTasks, task'ı arka planda çalıştırmak için kullanıyoruz
):
if not find_user_w_email(session, email):
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="bad request",
)
task.add_task(send_password_to_email, session, email)
return {"message": "New password has been sent to your email."}
@router.post('/update_password')
async def update_password(
user: Annotated[str, Depends(get_current_user)],
session: Annotated[Session, Depends(get_session_db)],
new_password: Annotated[str, None] = None,
) -> dict:
update_password_w_user(session, user, new_password)
return {"message": "Password updated successfully."}

View file

View file

173
collectionObj/models.py Normal file
View file

@ -0,0 +1,173 @@
from fastapi import HTTPException, Depends
from sqlalchemy import Integer, String, Boolean
from pydantic import BaseModel
from sqlalchemy.orm import Session, relationship, mapped_column, Mapped
from ..config import Base, get_session_db, user_collection, collection_item
from ..auth.models import DBUser
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..items.models import Items, Item
###### SCHEMAS #########
class CollectionBase(BaseModel):
collection_name : str | None = None
collection_description : str | None = None
visibility : bool | None = None
class CollectionCreate(CollectionBase):
pass
class CollectionPublic(CollectionBase):
collection_id : int | None = None
class Config:
from_attributes = True #sqlalchemy ile pydantic arasında geçiş yapabilmek için kullanılır
class CollectionUpdate(CollectionBase):
pass
##### veri tabanı modelleri #####
class CollectionsDB(Base):
__tablename__ = "collections_table"
collection_id : Mapped[int] = mapped_column(Integer, primary_key=True, index=True, autoincrement=True)
#user_id : Mapped[int] = mapped_column(Integer, ForeignKey("users_table.user_id"), nullable=False) # user_id ile ilişki
#item_id : Mapped[list[int]] = mapped_column(Integer, ForeignKey("items_table.item_id"), nullable=False) # item_id ile ilişki
visibility : Mapped[bool] = mapped_column(Boolean, default=True)
collection_name : Mapped[str] = mapped_column(String, nullable=False)
collection_description : Mapped[str] = mapped_column(String, default="No description")
# ilişkiler
users : Mapped[list['DBUser']] = relationship(
"DBUser",
secondary=user_collection,
back_populates="collections",
lazy='select'
) #back_populates karşı tarafın ismi
items : Mapped[list['Items']] = relationship(
"Items",
secondary=collection_item,
back_populates="collections" ,
lazy='select'
)
#### collection bir item listesi birde kullanıcı listesi tutacak
def create_colletion(
collection: CollectionCreate | None = None,
user_id : int | None = None
) -> bool:
"""
Collection oluşturma fonksiyonu
"""
if collection is None:
raise HTTPException(status_code=400, detail="Collection is None returned")
session = next(get_session_db()) # -> get_session_db() fonksiyonu daima generator döndürür next ile çağırmalısın
user = session.query(DBUser).filter(DBUser.user_id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
try:
new_collection = CollectionsDB(
collection_name=collection.collection_name,
collection_description=collection.collection_description,
visibility=collection.visibility
)
new_collection.users.append(user)
session.add(new_collection)
session.commit()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating collection: {e}")
return True
def get_collections(
user_id : int | None = None
) -> list[CollectionPublic] | None:
"""
Kullanıcının collectionlarını döndürür
"""
if user_id is None:
raise HTTPException(status_code=400, detail="User id is None")
session = next(get_session_db()) # -> get_session_db() fonksiyonu daima generator döndürür next ile çağırmalısın
collections = session.query(CollectionsDB).filter(CollectionsDB.users.any(user_id=user_id)).all()
if collections is None:
raise HTTPException(status_code=404, detail="No collections found")
return collections
def update_collection(
collection: CollectionUpdate | None = None,
user_id : int | None = None,
collection_id : int | None = None
) -> bool:
"""
Collection güncelleme fonksiyonu
"""
if collection is None:
raise HTTPException(status_code=400, detail="Collection is None returned")
session = next(get_session_db()) # -> get_session_db() fonksiyonu daima generator döndürür next ile çağırmalısın
user = session.query(DBUser).filter(DBUser.user_id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
collection_to_update = session.query(CollectionsDB).filter(CollectionsDB.collection_id == collection_id).first()
if collection_to_update is None:
raise HTTPException(status_code=404, detail="Collection not found")
try:
collection_to_update.collection_name = collection.collection_name
collection_to_update.collection_description = collection.collection_description
collection_to_update.visibility = collection.visibility
session.commit()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error updating collection: {e}")
return True
def delete_collection(
user_id : int | None = None,
collection_id : int | None = None
) -> bool:
"""
Collection silme fonksiyonu
"""
if user_id is None or collection_id is None:
raise HTTPException(status_code=400, detail="User id or collection id is None")
session = next(get_session_db()) # -> get_session_db() fonksiyonu daima generator döndürür next ile çağırmalısın
user = session.query(DBUser).filter(DBUser.user_id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
collection_to_delete = session.query(CollectionsDB).filter(CollectionsDB.collection_id == collection_id).first()
if collection_to_delete is None:
raise HTTPException(status_code=404, detail="Collection not found")
try:
session.delete(collection_to_delete)
session.commit()
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error deleting collection: {e}")
return True

60
collectionObj/router.py Normal file
View file

@ -0,0 +1,60 @@
from fastapi import FastAPI, APIRouter
from .models import CollectionPublic, CollectionCreate, CollectionUpdate
from .models import get_collections, create_colletion, update_collection, delete_collection
router = APIRouter(
prefix="/collections",
tags=["collections"],
responses={404: {"description": "Not found"}},
dependencies=[],
)
@router.get("/{user_id}")
async def get_collections_api(user_id: int) -> list[CollectionPublic]:
"""
Kullanıcının collectionlarını döndürür
"""
_collections : list[CollectionPublic] = get_collections(user_id=user_id)
return _collections
@router.post("/{user_id}")
async def create_collection(
user_id: int,
collection: CollectionCreate
) -> bool:
"""
Collection oluşturma fonksiyonu
"""
_result = create_colletion(user_id=user_id, collection=collection)
return _result
@router.put("/{user_id}/{collection_id}")
async def update_collection_api(
user_id: int,
collection_id : int,
collection: CollectionUpdate
) -> bool:
"""
Collection güncelleme fonksiyonu
"""
_result = update_collection(user_id=user_id, collection_id=collection_id, collection=collection)
return _result
@router.delete("/{user_id}/{collection_id}")
async def delete_collection_api(
user_id: int,
collection_id : int
) -> bool:
"""
Collection silme fonksiyonu
"""
_result = delete_collection(user_id=user_id, collection_id=collection_id)
return _result

View file

@ -1,8 +1,8 @@
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, DeclarativeBase
from sqlalchemy.orm import sessionmaker
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import Table, Column, Integer, String, Float, Boolean, ForeignKey
from passlib.context import CryptContext from passlib.context import CryptContext
from dotenv import load_dotenv from dotenv import load_dotenv
import os import os
@ -10,22 +10,55 @@ import os
load_dotenv() load_dotenv()
Base = declarative_base() #basic class for declarative models pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = os.getenv("ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))
DATABASE_URL = os.getenv("DATABASE_URL")
# Engine oluştur
engine = create_engine(DATABASE_URL, echo=False)
# Session factory oluştur
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
#Base = declarative_base() #sqlalchemy için bu sınıfı kullanıyoruz 'class DBUser(Base)' şeklinde tanımlıyoruz
class Base(DeclarativeBase):
pass #yeni sqlalchemy sürümünde bu sınıfı kullanıyoruz
#models te içe aktarmayı unutma
def init_db():
#Base.metadata.drop_all(engine) # Veritabanını her başlangıcta siler burayada dikkat !!!!!!!!
Base.metadata.create_all(bind=engine) # Veritabanını oluşturur
# Session dependency (FastAPI için)
def get_session_db() -> 'Generator[Session, None]':
db = SessionLocal()
try:
yield db
finally:
db.close()
user_collection = Table( # user -> collection
"user_collection",
Base.metadata,
Column("user_id", Integer, ForeignKey("users_table.user_id"), primary_key=True),
Column("collection_id", Integer, ForeignKey("collections_table.collection_id"), primary_key=True),
)
collection_item = Table( # collection -> item
"collection_item",
Base.metadata,
Column("collection_id", ForeignKey("collections_table.collection_id"), primary_key=True),
Column("item_id", ForeignKey("items_table.item_id"), primary_key=True)
)
DATABASE_URL = f"postgresql://{os.getenv('USERNAME_DB')}:{os.getenv('PASSWORD_DB')}@{os.getenv('HOST_DB')}:{os.getenv('PORT_DB')}/{os.getenv('NAME_DB')}"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
Base.metadata.create_all(bind=engine)
### SECRET KEY ### ### SECRET KEY ###
SECRET_KEY = os.getenv("SECRET_KEY")
ALGORITHM = os.getenv("ALGORITHM")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES"))
pwd_context = CryptContext(schemes=[f"{os.getenv('CRYPTO_TYPE')}"], deprecated="auto")
origins = [ origins = [
"http://localhost", "http://localhost",
"http://localhost:8080", "http://localhost:8080",
@ -34,6 +67,9 @@ origins = [
] ]
app = FastAPI() app = FastAPI()
@app.on_event("startup")
def startup_event():
init_db()
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@ -42,3 +78,6 @@ app.add_middleware(
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )

View file

@ -11,8 +11,4 @@ services:
ports: ports:
- "5434:5432" - "5434:5432"
volumes: volumes:
- postgres_data:/db - ./data/postgres:/var/lib/postgresql/data
volumes:
postgres_data:
driver: local

27
flake.lock generated Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1746332716,
"narHash": "sha256-VBmKSkmw9PYBCEGhBKzORjx+nwNZkPZyHcUHE21A/ws=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6b1c028bce9c89e9824cde040d6986d428296055",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

82
flake.nix Normal file
View file

@ -0,0 +1,82 @@
{
description = "Backend development flake";
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
outputs = {nixpkgs, ... }: let
forAllSystems = nixpkgs.lib.genAttrs [
"aarch64-linux"
"i686-linux"
"x86_64-linux"
"aarch64-darwin"
"x86_64-darwin"
];
in {
devShells = forAllSystems (system: let
pkgs = import nixpkgs {
inherit system;
config = {
android_sdk.accept_license = true;
allowUnfree = true;
};
};
in {
default = pkgs.mkShell {
packages = with pkgs; [
(python312.withPackages (
ppkgs:
with python312Packages; [
pip # python package manager
fastapi # web framework
pandas # data manipulation
pydantic # data validation
uvicorn # ASGI server
sqlalchemy # ORM
python-multipart # fastapi multipart form data
pyjwt # JWT authentication
psycopg2-binary
passlib
bcrypt
email-validator
]
))
fastapi-cli
sqlitestudio
];
shellHook = ''
docker compose down
docker compose up -d
'';
};
});
# app for backing up the data
apps = forAllSystems (system: let
pkgs = import nixpkgs {
inherit system;
config = {
android_sdk.accept_license = true;
allowUnfree = true;
};
};
in {
default = pkgs.fastapi-cli;
backup-db = pkgs.writeShellApplication {
name = "backup-db";
runtimeInputs = [ pkgs.zip ];
text = ''
# date
DATE=$(date +%Y-%m-%d)
# backup directory
BACKUP_DIR=~/aifred-backup/
# create backup directory if it doesn't exist
mkdir -p $BACKUP_DIR
# backup file name
BACKUP_FILE=$BACKUP_DIR/backup-$DATE.zip
zip -r $BACKUP_FILE data/
# move backup file to backup directory
mv #BACKUP_FILE $BACKUP_DIR
'';
};
});
};
}

78
items/models.py Normal file
View file

@ -0,0 +1,78 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated
from sqlalchemy import DateTime
from pydantic import BaseModel
from fastapi import Depends
from sqlalchemy.orm import Session, relationship, mapped_column, Mapped
from sqlalchemy import String, Float, Integer, ForeignKey
from sqlalchemy.dialects.postgresql import ARRAY
from ..config import Base, get_session_db, collection_item
from typing import TYPE_CHECKING
from ..auth.models import Role, Status, UserBase
from ..collectionObj.models import CollectionsDB
class UserProfileBase(UserBase):
bio : str | None = None
created_date : datetime | None = None
# collection : list[str] | None = None
class UserProfileID(UserProfileBase):
user_id : int | None = None
class UserProfilePublic(UserProfileBase):
pass
class UserProfilePrivate(UserProfilePublic):
#collection : list[str] | None = None
role : Role | None = None
status : Status | None = None
follow_user : list[int] | None = None
items : list['Item'] | None = None
######## ITEMS ######
class BaseItem(BaseModel):
item_created_date : datetime | None = None
item_location : str | None = None
item_type : str | None = None
item_content : str | None = None
class ItemCreate(BaseItem): # item oluşturma için ekstra bir ihtiyaci olmaz
pass
class Item(BaseItem):
item_id : int | None = None
user_id : int | None = None
item_score : float | None = None
class Config:
from_attributes = True #sqlalchemy ile pydantic arasında geçiş yapabilmek için kullanılır
##### VERİTABANI MODELİ #####
# Tüm modeller AUTH'da veri tabanına işlendi yukardaki
#modeller veri tabanında mevcuttur. Değiştirmek için AUTH'daki
# DBUser modelini değiştirip tekrar veri tabanına işleyebilirsin
class Items(Base):
__tablename__ = "items_table"
item_id : Mapped[int] = mapped_column(primary_key=True, index=True, autoincrement=True)
#collection_id : Mapped[list[int]] = mapped_column(Integer, ForeignKey("collections_table.collection_id"), nullable=True) # collection_id ile ilişki
item_created_date : Mapped[datetime] = mapped_column(DateTime, default=datetime.now())
item_location: Mapped[str] = mapped_column(String, default="No location")
item_type: Mapped[str] = mapped_column(String, default="No type")
item_content: Mapped[str] = mapped_column(String, default="No content")
item_score: Mapped[float] = mapped_column(Float, default=0.0)
# ilişkiler
collections : Mapped[list['CollectionsDB']]= relationship(
"CollectionsDB",
secondary=collection_item,
back_populates="items",
lazy='select'
) #back_populates karşı tarafın ismi

19
items/router.py Normal file
View file

@ -0,0 +1,19 @@
from .models import ItemCreate, UserProfileBase, UserProfileID, UserProfilePrivate, UserProfilePublic
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..config import get_session_db
from typing import Annotated
from ..auth.models import get_current_active_user
router = APIRouter(
prefix="/items",
tags=["items"],
responses={404: {"description": "Not found"}},
dependencies=[],
)
#tüm crud işlemleri yeni veri tabanı modeli ile yapılacak

122
main.py
View file

@ -1,122 +1,8 @@
from .config import app from .config import app
from .auth.router import router as auth_router from .auth.router import router as auth_router
from .items.router import router as items_router
from .collectionObj.router import router as collections_router
app.include_router(auth_router) app.include_router(auth_router)
app.include_router(collections_router)
app.include_router(items_router)
'''
from fastapi import FastAPI
from pydantic import BaseModel, Field, EmailStr
from enum import Enum as PyEnum
import datetime
from sqlalchemy import Column, Integer, String, DateTime, Float, Text, Boolean, ForeignKey, Enum
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base
from .config import Base #databaese connection
from .config import app #base app
# Enums database
class Role(str, PyEnum):
admin = "admin"
user = "user"
mod = "mod"
class Status(str, PyEnum):
active = "active"
banned = "banned"
suspended = "suspended"
class ItemType(str, PyEnum):
text = "text"
image = "image"
class VoteType(str, PyEnum):
up = "up"
down = "down"
# SQLAlchemy Models
class User(Base):
__tablename__ = "users"
user_id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, nullable=False)
name = Column(String)
surname = Column(String)
email = Column(String, unique=True, nullable=False)
role = Column(Enum(Role), default=Role.user)
status = Column(Enum(Status), default=Status.active)
bio = Column(String(144))
created_date = Column(DateTime, default=datetime.datetime.utcnow)
collections = relationship("Collection", back_populates="user")
items = relationship("Item", back_populates="user")
class Collection(Base):
__tablename__ = "collections"
collection_id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.user_id"))
visibility = Column(Boolean, default=True) # True = public, False = private
collection_name = Column(String, nullable=False)
collection_bio = Column(String)
user = relationship("User", back_populates="collections")
items = relationship("Item", back_populates="collection")
class Item(Base):
__tablename__ = "items"
item_id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.user_id"))
date = Column(DateTime, default=datetime.datetime.utcnow)
location_x = Column(Float)
location_y = Column(Float)
item_type = Column(Enum(ItemType))
content_text = Column(Text, nullable=True)
content_image_path = Column(String, nullable=True)
collection_id = Column(Integer, ForeignKey("collections.collection_id"))
score = Column(Integer, default=0)
user = relationship("User", back_populates="items")
collection = relationship("Collection", back_populates="items")
votes = relationship("Vote", back_populates="item")
class Vote(Base):
__tablename__ = "votes"
vote_id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.user_id"))
item_id = Column(Integer, ForeignKey("items.item_id"))
vote_type = Column(Enum(VoteType))
date = Column(DateTime, default=datetime.datetime.utcnow)
item = relationship("Item", back_populates="votes")
# Pydantic Schemas
class UserCreate(BaseModel):
username: str
name: str
surname: str
email: EmailStr
bio: str = Field(max_length=144)
class ItemCreate(BaseModel):
location_x: float
location_y: float
item_type: ItemType
content_text: str | None = None
content_image_path: str | None = None
collection_id: int
class CollectionCreate(BaseModel):
collection_name: str
collection_bio: str
visibility: bool = True
class VoteCreate(BaseModel):
item_id: int
vote_type: VoteType
'''

View file

@ -1,5 +1,6 @@
annotated-types==0.7.0 annotated-types==0.7.0
anyio==4.9.0 anyio==4.9.0
bcrypt==4.3.0
certifi==2025.4.26 certifi==2025.4.26
click==8.1.8 click==8.1.8
dnspython==2.7.0 dnspython==2.7.0
@ -30,6 +31,7 @@ rich-toolkit==0.14.4
shellingham==1.5.4 shellingham==1.5.4
sniffio==1.3.1 sniffio==1.3.1
SQLAlchemy==2.0.40 SQLAlchemy==2.0.40
sqlmodel==0.0.24
starlette==0.46.2 starlette==0.46.2
typer==0.15.3 typer==0.15.3
typing-inspection==0.4.0 typing-inspection==0.4.0
@ -38,3 +40,5 @@ uvicorn==0.34.2
uvloop==0.21.0 uvloop==0.21.0
watchfiles==1.0.5 watchfiles==1.0.5
websockets==15.0.1 websockets==15.0.1
passlib[bcrypt]==1.7.4