Initial commit with authentication and routes for registering/login already set up
This commit is contained in:
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
25
app/database.py
Normal file
25
app/database.py
Normal file
@ -0,0 +1,25 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
import os
|
||||
|
||||
# Set up the database URL (adjust to your psycopg3 format)
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+psycopg://postgres:postgresbro@10.0.0.124/scrap")
|
||||
|
||||
# Create the async engine with psycopg3
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
|
||||
# Create the sessionmaker for async sessions
|
||||
SessionLocal = sessionmaker(
|
||||
bind=engine,
|
||||
class_=AsyncSession,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
# Dependency for getting the database session
|
||||
async def get_db():
|
||||
async with SessionLocal() as session:
|
||||
yield session
|
||||
29
app/main.py
Normal file
29
app/main.py
Normal file
@ -0,0 +1,29 @@
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
from contextlib import asynccontextmanager
|
||||
from app.routers import auth # Assuming you have a router for auth logic
|
||||
from app.database import engine
|
||||
from app.models import Base
|
||||
|
||||
# Define a lifespan context manager to handle startup and shutdown events
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Run code before the app starts
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
# Yield control to run the app
|
||||
yield
|
||||
|
||||
# Run code before the app shuts down (if needed)
|
||||
await engine.dispose()
|
||||
|
||||
# Initialize the FastAPI app with the lifespan event handler
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
# Register your API routes
|
||||
app.include_router(auth.router)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
||||
170
app/models.py
Normal file
170
app/models.py
Normal file
@ -0,0 +1,170 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import as_declarative, declared_attr
|
||||
from sqlalchemy.sql import func
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
@as_declarative()
|
||||
class Base:
|
||||
@declared_attr
|
||||
def __tablename__(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
# Automatically apply nullable=False to ForeignKey columns unless explicitly set
|
||||
@staticmethod
|
||||
def Column(*args, **kwargs):
|
||||
if any(isinstance(arg, ForeignKey) for arg in args) and 'nullable' not in kwargs:
|
||||
kwargs['nullable'] = False
|
||||
return Column(*args, **kwargs)
|
||||
|
||||
# Enum for User Role
|
||||
from sqlalchemy import Enum
|
||||
from enum import Enum as PyEnum
|
||||
|
||||
class UserRole(PyEnum):
|
||||
PRIVATE = "PRIVATE"
|
||||
BUSINESS = "BUSINESS"
|
||||
|
||||
# User model
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
company = Column(String, nullable=True)
|
||||
address = Column(String, nullable=False)
|
||||
postcode = Column(Integer, nullable=False)
|
||||
city = Column(String, nullable=False)
|
||||
latitude = Column(String, nullable=True)
|
||||
longitude = Column(String, nullable=True)
|
||||
phone = Column(String, nullable=False)
|
||||
privatePhone = Column(String, nullable=True)
|
||||
email = Column(String, unique=True, nullable=False)
|
||||
cvr = Column(String, nullable=True)
|
||||
password = Column(String, nullable=False)
|
||||
role = Column(Enum(UserRole), default=UserRole.PRIVATE)
|
||||
updatedAt = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
createdAt = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
auctions = relationship('Auction', back_populates='user')
|
||||
accounts = relationship('Account', back_populates='user')
|
||||
sessions = relationship('Session', back_populates='user')
|
||||
bids = relationship('Bid', back_populates='user')
|
||||
|
||||
# Vehicle model
|
||||
class Vehicle(Base):
|
||||
__tablename__ = 'vehicles'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
brand = Column(String, nullable=False)
|
||||
model = Column(String, nullable=False)
|
||||
variant = Column(String, nullable=True)
|
||||
year = Column(Integer, nullable=False)
|
||||
kilometers = Column(Integer, nullable=False)
|
||||
condition = Column(String, nullable=False)
|
||||
location = Column(String, nullable=False)
|
||||
latitude = Column(String, nullable=False)
|
||||
longitude = Column(String, nullable=False)
|
||||
gasType = Column(String, nullable=False)
|
||||
images = Column(Text, nullable=False) # Store image paths or references
|
||||
description = Column(String, nullable=False)
|
||||
service = Column(String, nullable=False)
|
||||
inspectedAt = Column(DateTime(timezone=True), nullable=True)
|
||||
updatedAt = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
createdAt = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
auctions = relationship('Auction', back_populates='vehicle')
|
||||
equipment = relationship('VehicleEquipment', back_populates='vehicle')
|
||||
|
||||
# Equipment model
|
||||
class Equipment(Base):
|
||||
__tablename__ = 'equipment'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
|
||||
vehicles = relationship('VehicleEquipment', back_populates='equipment')
|
||||
|
||||
# Vehicle-Equipment association table
|
||||
class VehicleEquipment(Base):
|
||||
__tablename__ = 'vehicle_equipment'
|
||||
vehicle_id = Column(Integer, ForeignKey('vehicles.id'), primary_key=True)
|
||||
equipment_id = Column(Integer, ForeignKey('equipment.id'), primary_key=True)
|
||||
|
||||
vehicle = relationship('Vehicle', back_populates='equipment')
|
||||
equipment = relationship('Equipment', back_populates='vehicles')
|
||||
|
||||
# Auction model
|
||||
class Auction(Base):
|
||||
__tablename__ = 'auctions'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
vehicleId = Column(Integer, ForeignKey('vehicles.id'), nullable=False)
|
||||
userId = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
askingPrice = Column(Float, nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
updatedAt = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
createdAt = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user = relationship('User', back_populates='auctions')
|
||||
vehicle = relationship('Vehicle', back_populates='auctions')
|
||||
bids = relationship('Bid', back_populates='auction')
|
||||
|
||||
# Bid model
|
||||
class Bid(Base):
|
||||
__tablename__ = 'bids'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
auctionId = Column(Integer, ForeignKey('auctions.id'), nullable=False)
|
||||
userId = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
bid = Column(Float, nullable=False)
|
||||
updatedAt = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
createdAt = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
auction = relationship('Auction', back_populates='bids')
|
||||
user = relationship('User', back_populates='bids')
|
||||
|
||||
# Account model
|
||||
class Account(Base):
|
||||
__tablename__ = 'accounts'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
userId = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
type = Column(String, nullable=False)
|
||||
provider = Column(String, nullable=False)
|
||||
providerAccountId = Column(String, nullable=False)
|
||||
refresh_token = Column(String, nullable=True)
|
||||
access_token = Column(String, nullable=True)
|
||||
expires_at = Column(Integer, nullable=True)
|
||||
|
||||
user = relationship('User', back_populates='accounts')
|
||||
|
||||
# Session model
|
||||
class Session(Base):
|
||||
__tablename__ = 'sessions'
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
sessionToken = Column(String, unique=True, nullable=False)
|
||||
userId = Column(Integer, ForeignKey('users.id'))
|
||||
expires = Column(DateTime, nullable=False)
|
||||
|
||||
user = relationship('User', back_populates='sessions')
|
||||
|
||||
# VerificationToken model
|
||||
class VerificationToken(Base):
|
||||
__tablename__ = 'verification_tokens'
|
||||
identifier = Column(String, primary_key=True)
|
||||
token = Column(String, unique=True, nullable=False)
|
||||
expires = Column(DateTime, nullable=False)
|
||||
|
||||
# Pydantic model for user registration
|
||||
class UserCreate(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
name: str
|
||||
phone: str
|
||||
address: str
|
||||
postcode: int
|
||||
city: str
|
||||
role: str
|
||||
company: Optional[str] = None
|
||||
privatePhone: Optional[str] = None
|
||||
cvr: Optional[str] = None
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: str
|
||||
password: str
|
||||
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
82
app/routers/auth.py
Normal file
82
app/routers/auth.py
Normal file
@ -0,0 +1,82 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from ..database import get_db
|
||||
from ..models import User, UserCreate, UserLogin
|
||||
from ..security import create_access_token, verify_access_token
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
import bcrypt
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Register a new user
|
||||
@router.post("/api/v1/register")
|
||||
async def register(user_data: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||
async with db.begin():
|
||||
# Check if email already exists
|
||||
query = select(User).filter((User.email == user_data.email))
|
||||
result = await db.execute(query)
|
||||
user = result.scalars().first()
|
||||
|
||||
if user:
|
||||
raise HTTPException(status_code=400, detail="Email already exists")
|
||||
|
||||
# If user is registering as business, require company, privatePhone, and cvr
|
||||
if user_data.role == 'BUSINESS':
|
||||
if not user_data.company or not user_data.privatePhone or not user_data.cvr:
|
||||
raise HTTPException(status_code=400, detail="Company, Private Phone, and CVR are required for business users.")
|
||||
|
||||
# Hash the password using bcrypt
|
||||
hashed_password = bcrypt.hashpw(user_data.password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
# Create a new user with the provided information
|
||||
new_user = User(
|
||||
email=user_data.email,
|
||||
password=hashed_password,
|
||||
name=user_data.name,
|
||||
role=user_data.role,
|
||||
phone=user_data.phone,
|
||||
address=user_data.address,
|
||||
postcode=user_data.postcode,
|
||||
city=user_data.postcode,
|
||||
company=user_data.company,
|
||||
privatePhone=user_data.privatePhone,
|
||||
cvr=user_data.cvr
|
||||
)
|
||||
|
||||
db.add(new_user)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "User created successfully"}
|
||||
|
||||
# User login
|
||||
@router.post("/api/v1/login")
|
||||
async def login(login_data: UserLogin, db: AsyncSession = Depends(get_db)):
|
||||
async with db.begin():
|
||||
# Check if email is an email
|
||||
query = select(User).filter(User.email == login_data.email)
|
||||
|
||||
result = await db.execute(query)
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user or not bcrypt.checkpw(login_data.password.encode('utf-8'), user.password.encode('utf-8')):
|
||||
raise HTTPException(status_code=400, detail="Invalid credentials")
|
||||
|
||||
access_token = create_access_token(data={"user_id": user.id})
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
# Protected route example
|
||||
@router.get("/api/v1/protected")
|
||||
async def protected_route(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)):
|
||||
user_id = verify_access_token(token)
|
||||
async with db.begin():
|
||||
query = select(User).filter(User.id == user_id)
|
||||
result = await db.execute(query)
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="User not found")
|
||||
|
||||
return {"message": f"Hello, {user.name}"}
|
||||
54
app/security.py
Normal file
54
app/security.py
Normal file
@ -0,0 +1,54 @@
|
||||
import bcrypt
|
||||
from jose import jwt, JWTError
|
||||
from datetime import datetime, timedelta
|
||||
from fastapi import HTTPException, status
|
||||
import os
|
||||
|
||||
# Secret and algorithm for JWT
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'your_jwt_secret_key') # Ensure this is set in your environment
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
|
||||
# Hash password using bcrypt directly
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Hashes the password using bcrypt."""
|
||||
salt = bcrypt.gensalt() # Generate a salt
|
||||
hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt) # Hash the password
|
||||
return hashed_password.decode('utf-8') # Return as a string
|
||||
|
||||
# Verify password using bcrypt directly
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verifies if the plain password matches the hashed password."""
|
||||
return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8'))
|
||||
|
||||
# Create JWT token
|
||||
def create_access_token(data: dict, expires_delta: timedelta = None):
|
||||
"""Creates a JWT token with expiration time."""
|
||||
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
|
||||
|
||||
# Verify JWT token
|
||||
def verify_access_token(token: str):
|
||||
"""Verifies the JWT token and returns the user_id if valid."""
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user_id
|
||||
except JWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
161
app/seed.py
Normal file
161
app/seed.py
Normal file
@ -0,0 +1,161 @@
|
||||
# seed.py
|
||||
from sqlalchemy.orm import Session
|
||||
from database import SessionLocal, init_db
|
||||
from models import Equipment
|
||||
|
||||
# List of equipment data
|
||||
equipmentData = [
|
||||
"aut.",
|
||||
"aut.gear/tiptronic",
|
||||
"ratgearskifte",
|
||||
"alu.",
|
||||
"15\" Alufælge",
|
||||
"16\" Alufælge",
|
||||
"17\" Alufælge",
|
||||
"18\" Alufælge",
|
||||
"19\" Alufælge",
|
||||
"20\" Alufælge",
|
||||
"21\" Alufælge",
|
||||
"22\" Alufælge",
|
||||
"vinterhjul",
|
||||
"varme i rat",
|
||||
"airc.",
|
||||
"fuldaut. klima",
|
||||
"2 zone klima",
|
||||
"3 zone klima",
|
||||
"4 zone klima",
|
||||
"køl i handskerum",
|
||||
"elektrisk",
|
||||
"kabinevarmer",
|
||||
"motorkabinevarmer",
|
||||
"alarm",
|
||||
"el-klapbare sidespejle",
|
||||
"el-klapbare sidespejle m. varme",
|
||||
"el-spejle m/varme",
|
||||
"automatisk parkerings system",
|
||||
"360° kamera",
|
||||
"bakkamera",
|
||||
"parkeringssensor (bag)",
|
||||
"parkeringssensor (for)",
|
||||
"adaptiv fartpilot",
|
||||
"automatisk",
|
||||
"start/stop",
|
||||
"el betjent bagklap",
|
||||
"dæktryksmåler",
|
||||
"adaptiv undervogn",
|
||||
"elektrisk parkeringsbremse",
|
||||
"træthedsregistrering",
|
||||
"skiltegenkendelse",
|
||||
"CD",
|
||||
"CD/radio",
|
||||
"radio med CDboks",
|
||||
"el komfortsæder",
|
||||
"sportssæder",
|
||||
"integrerede børnesæder",
|
||||
"3 individuelle sæder i bag",
|
||||
"lygtevasker",
|
||||
"tågelygter",
|
||||
"bi-xenon",
|
||||
"xenonlys",
|
||||
"automatisk lys",
|
||||
"fjernlysassistent",
|
||||
"kurvelys",
|
||||
"LED kørelys",
|
||||
"fuld LED forlygter",
|
||||
"airbag",
|
||||
"db. airbags",
|
||||
"4 airbags",
|
||||
"6 airbags",
|
||||
"7 airbags",
|
||||
"8 airbags",
|
||||
"9 airbags",
|
||||
"10 airbags",
|
||||
"ABS",
|
||||
"antispin",
|
||||
"c.lås",
|
||||
"fjernb. c.lås",
|
||||
"nøglefri betjening",
|
||||
"fartpilot",
|
||||
"kørecomputer",
|
||||
"infocenter",
|
||||
"startspærre",
|
||||
"varme i forrude",
|
||||
"auto. nedbl. Bakspejl",
|
||||
"udv. temp. måler",
|
||||
"regnsensor",
|
||||
"sædevarme",
|
||||
"højdejust. forsæder",
|
||||
"højdejust. førersæde",
|
||||
"el indst. forsæder",
|
||||
"el indst. førersæde m. memory",
|
||||
"soltag",
|
||||
"el-soltag",
|
||||
"glastag",
|
||||
"el-ruder",
|
||||
"4x el-ruder",
|
||||
"el-spejle",
|
||||
"DAB radio",
|
||||
"DAB+ radio",
|
||||
"navigation",
|
||||
"multifunktionsrat",
|
||||
"håndfrit til mobil",
|
||||
"bluetooth",
|
||||
"musikstreaming via bluetooth",
|
||||
"nightvision",
|
||||
"digitalt cockpit",
|
||||
"headup display",
|
||||
"Android Auto",
|
||||
"Apple CarPlay",
|
||||
"Internet",
|
||||
"trådløs",
|
||||
"mobilopladning",
|
||||
"SD kortlæser",
|
||||
"USB tilslutning",
|
||||
"AUX tilslutning",
|
||||
"armlæn",
|
||||
"isofix",
|
||||
"bagagerumsdækken",
|
||||
"kopholder",
|
||||
"stofindtræk",
|
||||
"dellæder",
|
||||
"læderindtræk",
|
||||
"kunstlæder",
|
||||
"splitbagsæde",
|
||||
"læderrat",
|
||||
"ESP",
|
||||
"servo",
|
||||
"vognbaneassistent",
|
||||
"blindvinkelsassistent",
|
||||
"automatisk nødbremsesystem",
|
||||
"sænket",
|
||||
"tagræling",
|
||||
"tonede ruder",
|
||||
"mørktonede ruder i bag",
|
||||
"1 ejer",
|
||||
"ikke ryger",
|
||||
"service ok",
|
||||
"brugtbilsattest",
|
||||
"træk",
|
||||
"aftag. træk",
|
||||
"svingbart træk (manuel)",
|
||||
"svingbart træk (elektrisk)",
|
||||
"diesel partikel filter",
|
||||
"undervognsbehandlet"
|
||||
]
|
||||
|
||||
def seed_equipment_data(db: Session):
|
||||
for equipment in equipmentData:
|
||||
db.add(Equipment(name=equipment))
|
||||
db.commit()
|
||||
|
||||
def main():
|
||||
init_db() # Ensure all tables are created
|
||||
db = SessionLocal()
|
||||
try:
|
||||
seed_equipment_data(db)
|
||||
print("Equipment data seeded.")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user