Spaces:
Runtime error
Runtime error
Upload 32 files
Browse files- .dockerignore +20 -0
- .env +13 -0
- .gitignore +20 -0
- Dockerfile +71 -0
- admin.py +23 -0
- app/auth.py +54 -0
- app/config.py +25 -0
- app/database.py +16 -0
- app/models.py +148 -0
- app/routes/__init__.py +0 -0
- app/routes/about.py +29 -0
- app/routes/auth.py +52 -0
- app/routes/blog.py +62 -0
- app/routes/certifications.py +42 -0
- app/routes/contact.py +45 -0
- app/routes/experience.py +60 -0
- app/routes/health.py +9 -0
- app/routes/hero.py +29 -0
- app/routes/projects.py +55 -0
- app/routes/skills.py +76 -0
- app/routes/upload.py +28 -0
- app/schemas.py +232 -0
- app/utils/email_utils.py +33 -0
- main.py +34 -0
- requirements.txt +10 -0
- static/uploads/2fba62d9-4ec2-4c7c-9234-9b1cafd90b78.png +0 -0
- static/uploads/4df4eb5a-d474-49e7-9c1a-59532d56632f.png +0 -0
- static/uploads/5e90e30d-3ce2-4567-9b79-95001a2a04af.ico +0 -0
- static/uploads/89492760-9906-442a-9386-d13df78b2a44.png +0 -0
- static/uploads/92c1782e-9a40-4ab7-b832-0ebc3562522e.ico +0 -0
- static/uploads/c90a0883-a9ae-4e50-a6dd-8ba494718910.png +0 -0
- static/uploads/ec465097-32bd-4767-a503-7208d71076a2.png +0 -0
.dockerignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
*.sqlite3
|
| 7 |
+
|
| 8 |
+
# Environment
|
| 9 |
+
.env
|
| 10 |
+
.env.*
|
| 11 |
+
venn/
|
| 12 |
+
*.log
|
| 13 |
+
|
| 14 |
+
# IDE/editor
|
| 15 |
+
.DS_Store
|
| 16 |
+
.idea/
|
| 17 |
+
.vscode/
|
| 18 |
+
|
| 19 |
+
# Uploads (optional if using CDN or cloud)
|
| 20 |
+
static/uploads/*
|
.env
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .env
|
| 2 |
+
DATABASE_URL=postgresql://neondb_owner:npg_o5XMimeA8tnZ@ep-super-haze-a1dd768a-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
|
| 3 |
+
JWT_SECRET_KEY=your_secret_access_key
|
| 4 |
+
JWT_REFRESH_SECRET_KEY=your_refresh_secret_key
|
| 5 |
+
ALGORITHM=HS256
|
| 6 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
| 7 |
+
REFRESH_TOKEN_EXPIRE_DAYS=7
|
| 8 |
+
EMAIL_HOST=smtp.yourprovider.com
|
| 9 |
+
EMAIL_PORT=587
|
| 10 |
+
EMAIL_USER=your@email.com
|
| 11 |
+
EMAIL_PASS=your_email_password
|
| 12 |
+
ALLOWED_ORIGINS_RAW=*
|
| 13 |
+
RATE_LIMIT=5
|
.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
*.sqlite3
|
| 7 |
+
|
| 8 |
+
# Environment
|
| 9 |
+
.env
|
| 10 |
+
.env.*
|
| 11 |
+
venn/
|
| 12 |
+
*.log
|
| 13 |
+
|
| 14 |
+
# IDE/editor
|
| 15 |
+
.DS_Store
|
| 16 |
+
.idea/
|
| 17 |
+
.vscode/
|
| 18 |
+
|
| 19 |
+
# Uploads (optional if using CDN or cloud)
|
| 20 |
+
static/uploads/*
|
Dockerfile
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dockerfile
|
| 2 |
+
|
| 3 |
+
FROM python:3.10
|
| 4 |
+
|
| 5 |
+
# Set environment variables
|
| 6 |
+
ENV PYTHONDONTWRITEBYTECODE 1
|
| 7 |
+
ENV PYTHONUNBUFFERED 1
|
| 8 |
+
|
| 9 |
+
# Create app directory
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Install dependencies
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --upgrade pip && pip install -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copy source code
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
# Expose port
|
| 20 |
+
EXPOSE 8000
|
| 21 |
+
|
| 22 |
+
# Run the app
|
| 23 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# docker-compose.yml
|
| 27 |
+
version: "3.9"
|
| 28 |
+
services:
|
| 29 |
+
fastapi:
|
| 30 |
+
build: .
|
| 31 |
+
container_name: portfolio-api
|
| 32 |
+
ports:
|
| 33 |
+
- "8000:8000"
|
| 34 |
+
volumes:
|
| 35 |
+
- .:/app
|
| 36 |
+
env_file:
|
| 37 |
+
- .env
|
| 38 |
+
depends_on:
|
| 39 |
+
- db
|
| 40 |
+
|
| 41 |
+
db:
|
| 42 |
+
image: postgres:15
|
| 43 |
+
container_name: postgres-db
|
| 44 |
+
restart: always
|
| 45 |
+
environment:
|
| 46 |
+
POSTGRES_USER: youruser
|
| 47 |
+
POSTGRES_PASSWORD: yourpassword
|
| 48 |
+
POSTGRES_DB: yourdb
|
| 49 |
+
ports:
|
| 50 |
+
- "5432:5432"
|
| 51 |
+
volumes:
|
| 52 |
+
- postgres_data:/var/lib/postgresql/data
|
| 53 |
+
|
| 54 |
+
volumes:
|
| 55 |
+
postgres_data:
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
# .dockerignore
|
| 59 |
+
__pycache__/
|
| 60 |
+
*.pyc
|
| 61 |
+
*.pyo
|
| 62 |
+
*.pyd
|
| 63 |
+
*.sqlite3
|
| 64 |
+
.env
|
| 65 |
+
venv/
|
| 66 |
+
.env.*
|
| 67 |
+
*.log
|
| 68 |
+
.DS_Store
|
| 69 |
+
.idea/
|
| 70 |
+
.vscode/
|
| 71 |
+
static/uploads/*
|
admin.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.database import SessionLocal
|
| 2 |
+
from app.models import User
|
| 3 |
+
from app.auth import get_password_hash
|
| 4 |
+
import uuid
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
db = SessionLocal()
|
| 8 |
+
|
| 9 |
+
user = User(
|
| 10 |
+
id=uuid.uuid4(),
|
| 11 |
+
email="shakauthossain0@gmail.com",
|
| 12 |
+
password_hash=get_password_hash("bangladesh"),
|
| 13 |
+
full_name="Admin User",
|
| 14 |
+
is_active=True,
|
| 15 |
+
created_at=datetime.utcnow(),
|
| 16 |
+
updated_at=datetime.utcnow()
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
db.add(user)
|
| 20 |
+
db.commit()
|
| 21 |
+
db.refresh(user)
|
| 22 |
+
|
| 23 |
+
print("✅ User created:", user.email)
|
app/auth.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/auth.py
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from jose import JWTError, jwt
|
| 5 |
+
from passlib.context import CryptContext
|
| 6 |
+
from fastapi import Depends, HTTPException, status
|
| 7 |
+
from sqlalchemy.orm import Session
|
| 8 |
+
from app import models, schemas
|
| 9 |
+
from app.database import get_db
|
| 10 |
+
from app.config import settings
|
| 11 |
+
from uuid import UUID
|
| 12 |
+
|
| 13 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 14 |
+
|
| 15 |
+
# --- Password Hashing ---
|
| 16 |
+
def verify_password(plain_password, hashed_password):
|
| 17 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 18 |
+
|
| 19 |
+
def get_password_hash(password):
|
| 20 |
+
return pwd_context.hash(password)
|
| 21 |
+
|
| 22 |
+
# --- JWT Utility ---
|
| 23 |
+
def create_access_token(data: dict, expires_delta: timedelta):
|
| 24 |
+
to_encode = data.copy()
|
| 25 |
+
expire = datetime.utcnow() + expires_delta
|
| 26 |
+
to_encode.update({"exp": expire})
|
| 27 |
+
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 28 |
+
return encoded_jwt
|
| 29 |
+
|
| 30 |
+
def create_refresh_token(data: dict, expires_delta: timedelta):
|
| 31 |
+
to_encode = data.copy()
|
| 32 |
+
expire = datetime.utcnow() + expires_delta
|
| 33 |
+
to_encode.update({"exp": expire})
|
| 34 |
+
encoded_jwt = jwt.encode(to_encode, settings.JWT_REFRESH_SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 35 |
+
return encoded_jwt
|
| 36 |
+
|
| 37 |
+
# --- Auth Dependency ---
|
| 38 |
+
def get_current_user(token: str = Depends(schemas.oauth2_scheme), db: Session = Depends(get_db)):
|
| 39 |
+
credentials_exception = HTTPException(
|
| 40 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 41 |
+
detail="Could not validate credentials",
|
| 42 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 43 |
+
)
|
| 44 |
+
try:
|
| 45 |
+
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.ALGORITHM])
|
| 46 |
+
user_id: str = payload.get("sub")
|
| 47 |
+
if user_id is None:
|
| 48 |
+
raise credentials_exception
|
| 49 |
+
except JWTError:
|
| 50 |
+
raise credentials_exception
|
| 51 |
+
user = db.query(models.User).filter(models.User.id == UUID(user_id)).first()
|
| 52 |
+
if user is None:
|
| 53 |
+
raise credentials_exception
|
| 54 |
+
return user
|
app/config.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
class Settings(BaseSettings):
|
| 5 |
+
DATABASE_URL: str
|
| 6 |
+
JWT_SECRET_KEY: str
|
| 7 |
+
JWT_REFRESH_SECRET_KEY: str
|
| 8 |
+
ALGORITHM: str = "HS256"
|
| 9 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
| 10 |
+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
| 11 |
+
ALLOWED_ORIGINS_RAW: str = ""
|
| 12 |
+
EMAIL_HOST: str
|
| 13 |
+
EMAIL_PORT: int
|
| 14 |
+
EMAIL_USER: str
|
| 15 |
+
EMAIL_PASS: str
|
| 16 |
+
RATE_LIMIT: int = 5
|
| 17 |
+
|
| 18 |
+
class Config:
|
| 19 |
+
env_file = ".env"
|
| 20 |
+
|
| 21 |
+
@property
|
| 22 |
+
def ALLOWED_ORIGINS(self) -> List[str]:
|
| 23 |
+
return [origin.strip() for origin in self.ALLOWED_ORIGINS_RAW.split(",") if origin.strip()]
|
| 24 |
+
|
| 25 |
+
settings = Settings()
|
app/database.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
from app.config import settings
|
| 5 |
+
|
| 6 |
+
engine = create_engine(settings.DATABASE_URL)
|
| 7 |
+
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
| 8 |
+
Base = declarative_base()
|
| 9 |
+
|
| 10 |
+
# Dependency
|
| 11 |
+
def get_db():
|
| 12 |
+
db = SessionLocal()
|
| 13 |
+
try:
|
| 14 |
+
yield db
|
| 15 |
+
finally:
|
| 16 |
+
db.close()
|
app/models.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/models.py
|
| 2 |
+
|
| 3 |
+
import uuid
|
| 4 |
+
from datetime import datetime, date
|
| 5 |
+
from sqlalchemy import Column, String, Boolean, DateTime, Text, Enum, Integer, ForeignKey, Date, JSON
|
| 6 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 7 |
+
from sqlalchemy.orm import relationship
|
| 8 |
+
from app.database import Base
|
| 9 |
+
|
| 10 |
+
class User(Base):
|
| 11 |
+
__tablename__ = "users"
|
| 12 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 13 |
+
email = Column(String, unique=True, index=True, nullable=False)
|
| 14 |
+
password_hash = Column(String, nullable=False)
|
| 15 |
+
full_name = Column(String, nullable=False)
|
| 16 |
+
is_active = Column(Boolean, default=True)
|
| 17 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 18 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 19 |
+
|
| 20 |
+
class Hero(Base):
|
| 21 |
+
__tablename__ = "hero"
|
| 22 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 23 |
+
title = Column(String)
|
| 24 |
+
subtitle = Column(String)
|
| 25 |
+
description = Column(Text)
|
| 26 |
+
cta_text = Column(String)
|
| 27 |
+
cta_link = Column(String)
|
| 28 |
+
background_image = Column(String)
|
| 29 |
+
profile_image = Column(String)
|
| 30 |
+
animation_style = Column(String)
|
| 31 |
+
show_scroll_indicator = Column(Boolean, default=False)
|
| 32 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 33 |
+
|
| 34 |
+
class About(Base):
|
| 35 |
+
__tablename__ = "about"
|
| 36 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 37 |
+
title = Column(String)
|
| 38 |
+
introduction = Column(Text)
|
| 39 |
+
description = Column(Text)
|
| 40 |
+
profile_image = Column(String)
|
| 41 |
+
highlights = Column(JSON)
|
| 42 |
+
skills = Column(JSON)
|
| 43 |
+
location = Column(String)
|
| 44 |
+
availability = Column(String)
|
| 45 |
+
email = Column(String)
|
| 46 |
+
phone = Column(String)
|
| 47 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 48 |
+
|
| 49 |
+
class Experience(Base):
|
| 50 |
+
__tablename__ = "experiences"
|
| 51 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 52 |
+
title = Column(String)
|
| 53 |
+
company = Column(String)
|
| 54 |
+
location = Column(String)
|
| 55 |
+
start_date = Column(Date)
|
| 56 |
+
end_date = Column(Date, nullable=True)
|
| 57 |
+
is_current = Column(Boolean, default=False)
|
| 58 |
+
description = Column(Text)
|
| 59 |
+
technologies = Column(JSON)
|
| 60 |
+
achievements = Column(JSON)
|
| 61 |
+
display_order = Column(Integer)
|
| 62 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 63 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 64 |
+
|
| 65 |
+
class Project(Base):
|
| 66 |
+
__tablename__ = "projects"
|
| 67 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 68 |
+
title = Column(String)
|
| 69 |
+
description = Column(String)
|
| 70 |
+
long_description = Column(Text)
|
| 71 |
+
image = Column(String)
|
| 72 |
+
technologies = Column(JSON)
|
| 73 |
+
category = Column(String)
|
| 74 |
+
status = Column(Enum("completed", "in-progress", "planned", name="project_status"))
|
| 75 |
+
is_featured = Column(Boolean, default=False)
|
| 76 |
+
github_url = Column(String, nullable=True)
|
| 77 |
+
live_url = Column(String, nullable=True)
|
| 78 |
+
start_date = Column(Date)
|
| 79 |
+
end_date = Column(Date, nullable=True)
|
| 80 |
+
highlights = Column(JSON)
|
| 81 |
+
display_order = Column(Integer)
|
| 82 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 83 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 84 |
+
|
| 85 |
+
class SkillCategory(Base):
|
| 86 |
+
__tablename__ = "skill_categories"
|
| 87 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 88 |
+
name = Column(String)
|
| 89 |
+
color = Column(String)
|
| 90 |
+
description = Column(String)
|
| 91 |
+
display_order = Column(Integer)
|
| 92 |
+
|
| 93 |
+
class Skill(Base):
|
| 94 |
+
__tablename__ = "skills"
|
| 95 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 96 |
+
name = Column(String)
|
| 97 |
+
category_id = Column(UUID(as_uuid=True), ForeignKey("skill_categories.id"))
|
| 98 |
+
proficiency = Column(Integer)
|
| 99 |
+
years_of_experience = Column(Integer)
|
| 100 |
+
icon = Column(String, nullable=True)
|
| 101 |
+
is_featured = Column(Boolean, default=False)
|
| 102 |
+
display_order = Column(Integer)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class BlogPost(Base):
|
| 106 |
+
__tablename__ = "blog_posts"
|
| 107 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 108 |
+
title = Column(String)
|
| 109 |
+
slug = Column(String, unique=True)
|
| 110 |
+
excerpt = Column(Text)
|
| 111 |
+
content = Column(Text)
|
| 112 |
+
author = Column(String)
|
| 113 |
+
publish_date = Column(Date)
|
| 114 |
+
status = Column(Enum("draft", "published", "scheduled", name="blog_status"))
|
| 115 |
+
is_featured = Column(Boolean, default=False)
|
| 116 |
+
image = Column(String)
|
| 117 |
+
tags = Column(JSON)
|
| 118 |
+
category = Column(String)
|
| 119 |
+
read_time = Column(Integer)
|
| 120 |
+
seo_title = Column(String)
|
| 121 |
+
seo_description = Column(String)
|
| 122 |
+
views = Column(Integer, default=0)
|
| 123 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
| 124 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 125 |
+
|
| 126 |
+
class Certification(Base):
|
| 127 |
+
__tablename__ = "certifications"
|
| 128 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 129 |
+
name = Column(String)
|
| 130 |
+
issuer = Column(String)
|
| 131 |
+
issue_date = Column(Date)
|
| 132 |
+
expiry_date = Column(Date, nullable=True)
|
| 133 |
+
credential_id = Column(String, nullable=True)
|
| 134 |
+
verification_url = Column(String, nullable=True)
|
| 135 |
+
image = Column(String, nullable=True)
|
| 136 |
+
description = Column(Text, nullable=True)
|
| 137 |
+
is_featured = Column(Boolean, default=False)
|
| 138 |
+
display_order = Column(Integer)
|
| 139 |
+
|
| 140 |
+
class ContactMessage(Base):
|
| 141 |
+
__tablename__ = "contact_messages"
|
| 142 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 143 |
+
name = Column(String)
|
| 144 |
+
email = Column(String)
|
| 145 |
+
subject = Column(String)
|
| 146 |
+
message = Column(Text)
|
| 147 |
+
is_read = Column(Boolean, default=False)
|
| 148 |
+
created_at = Column(DateTime, default=datetime.utcnow)
|
app/routes/__init__.py
ADDED
|
File without changes
|
app/routes/about.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/about.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from app import models, schemas
|
| 6 |
+
from app.database import get_db
|
| 7 |
+
from app.auth import get_current_user
|
| 8 |
+
|
| 9 |
+
router = APIRouter(tags=["About"])
|
| 10 |
+
|
| 11 |
+
@router.get("/", response_model=schemas.AboutResponse)
|
| 12 |
+
def get_about(db: Session = Depends(get_db)):
|
| 13 |
+
about = db.query(models.About).first()
|
| 14 |
+
if not about:
|
| 15 |
+
raise HTTPException(status_code=404, detail="About section not found")
|
| 16 |
+
return about
|
| 17 |
+
|
| 18 |
+
@router.put("/", response_model=schemas.AboutResponse)
|
| 19 |
+
def update_about(update: schemas.AboutBase, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 20 |
+
about = db.query(models.About).first()
|
| 21 |
+
if not about:
|
| 22 |
+
about = models.About(**update.dict())
|
| 23 |
+
db.add(about)
|
| 24 |
+
else:
|
| 25 |
+
for key, value in update.dict(exclude_unset=True).items():
|
| 26 |
+
setattr(about, key, value)
|
| 27 |
+
db.commit()
|
| 28 |
+
db.refresh(about)
|
| 29 |
+
return about
|
app/routes/auth.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/auth.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from fastapi.security import OAuth2PasswordRequestForm
|
| 6 |
+
from datetime import timedelta
|
| 7 |
+
from app import models, schemas
|
| 8 |
+
from app.auth import verify_password, get_password_hash, create_access_token, create_refresh_token
|
| 9 |
+
from app.database import get_db
|
| 10 |
+
from app.config import settings
|
| 11 |
+
from jose import JWTError, jwt
|
| 12 |
+
from app.auth import get_current_user
|
| 13 |
+
from app.schemas import UserOut
|
| 14 |
+
from fastapi.responses import JSONResponse
|
| 15 |
+
|
| 16 |
+
router = APIRouter(tags=["Authentication"])
|
| 17 |
+
|
| 18 |
+
@router.post("/login", response_model=schemas.Token)
|
| 19 |
+
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
| 20 |
+
user = db.query(models.User).filter(models.User.email == form_data.username).first()
|
| 21 |
+
if not user or not verify_password(form_data.password, user.password_hash):
|
| 22 |
+
raise HTTPException(status_code=401, detail="Incorrect email or password")
|
| 23 |
+
|
| 24 |
+
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 25 |
+
refresh_token_expires = timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
| 26 |
+
|
| 27 |
+
access_token = create_access_token(data={"sub": str(user.id)}, expires_delta=access_token_expires)
|
| 28 |
+
refresh_token = create_refresh_token(data={"sub": str(user.id)}, expires_delta=refresh_token_expires)
|
| 29 |
+
|
| 30 |
+
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
| 31 |
+
|
| 32 |
+
@router.post("/refresh", response_model=schemas.Token)
|
| 33 |
+
def refresh_token(refresh_token: str, db: Session = Depends(get_db)):
|
| 34 |
+
try:
|
| 35 |
+
payload = jwt.decode(refresh_token, settings.JWT_REFRESH_SECRET_KEY, algorithms=[settings.ALGORITHM])
|
| 36 |
+
user_id = payload.get("sub")
|
| 37 |
+
except:
|
| 38 |
+
raise HTTPException(status_code=401, detail="Invalid refresh token")
|
| 39 |
+
|
| 40 |
+
user = db.query(models.User).filter(models.User.id == user_id).first()
|
| 41 |
+
if not user:
|
| 42 |
+
raise HTTPException(status_code=401, detail="User not found")
|
| 43 |
+
|
| 44 |
+
access_token = create_access_token(data={"sub": str(user.id)}, expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES))
|
| 45 |
+
refresh_token = create_refresh_token(data={"sub": str(user.id)}, expires_delta=timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS))
|
| 46 |
+
|
| 47 |
+
return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}
|
| 48 |
+
|
| 49 |
+
@router.post("/logout", status_code=status.HTTP_200_OK)
|
| 50 |
+
def logout(user: UserOut = Depends(get_current_user)):
|
| 51 |
+
# Invalidate token logic would go here if you used a blacklist
|
| 52 |
+
return JSONResponse(content={"message": "Logout successful."})
|
app/routes/blog.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/blog.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from sqlalchemy import or_
|
| 6 |
+
from app import models, schemas
|
| 7 |
+
from app.database import get_db
|
| 8 |
+
from app.auth import get_current_user
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
router = APIRouter(tags=["Blog"])
|
| 12 |
+
|
| 13 |
+
@router.get("/", response_model=list[schemas.BlogPostResponse])
|
| 14 |
+
def list_posts(skip: int = 0, limit: int = 10, search: str = Query(""), db: Session = Depends(get_db)):
|
| 15 |
+
query = db.query(models.BlogPost)
|
| 16 |
+
if search:
|
| 17 |
+
query = query.filter(
|
| 18 |
+
or_(
|
| 19 |
+
models.BlogPost.title.ilike(f"%{search}%"),
|
| 20 |
+
models.BlogPost.excerpt.ilike(f"%{search}%"),
|
| 21 |
+
models.BlogPost.tags.cast(str).ilike(f"%{search}%")
|
| 22 |
+
)
|
| 23 |
+
)
|
| 24 |
+
return query.order_by(models.BlogPost.publish_date.desc()).offset(skip).limit(limit).all()
|
| 25 |
+
|
| 26 |
+
@router.get("/{slug}", response_model=schemas.BlogPostResponse)
|
| 27 |
+
def get_post(slug: str, db: Session = Depends(get_db)):
|
| 28 |
+
post = db.query(models.BlogPost).filter(models.BlogPost.slug == slug).first()
|
| 29 |
+
if not post:
|
| 30 |
+
raise HTTPException(status_code=404, detail="Post not found")
|
| 31 |
+
post.views += 1
|
| 32 |
+
db.commit()
|
| 33 |
+
db.refresh(post)
|
| 34 |
+
return post
|
| 35 |
+
|
| 36 |
+
@router.post("/", response_model=schemas.BlogPostResponse)
|
| 37 |
+
def create_post(payload: schemas.BlogPostCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 38 |
+
post = models.BlogPost(**payload.dict())
|
| 39 |
+
db.add(post)
|
| 40 |
+
db.commit()
|
| 41 |
+
db.refresh(post)
|
| 42 |
+
return post
|
| 43 |
+
|
| 44 |
+
@router.put("/{id}", response_model=schemas.BlogPostResponse)
|
| 45 |
+
def update_post(id: uuid.UUID, payload: schemas.BlogPostUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 46 |
+
post = db.query(models.BlogPost).filter(models.BlogPost.id == id).first()
|
| 47 |
+
if not post:
|
| 48 |
+
raise HTTPException(status_code=404, detail="Post not found")
|
| 49 |
+
for key, value in payload.dict(exclude_unset=True).items():
|
| 50 |
+
setattr(post, key, value)
|
| 51 |
+
db.commit()
|
| 52 |
+
db.refresh(post)
|
| 53 |
+
return post
|
| 54 |
+
|
| 55 |
+
@router.delete("/{id}", response_model=schemas.Msg)
|
| 56 |
+
def delete_post(id: uuid.UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 57 |
+
post = db.query(models.BlogPost).filter(models.BlogPost.id == id).first()
|
| 58 |
+
if not post:
|
| 59 |
+
raise HTTPException(status_code=404, detail="Post not found")
|
| 60 |
+
db.delete(post)
|
| 61 |
+
db.commit()
|
| 62 |
+
return {"detail": "Deleted successfully"}
|
app/routes/certifications.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/certifications.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from uuid import UUID
|
| 6 |
+
from app import models, schemas
|
| 7 |
+
from app.database import get_db
|
| 8 |
+
from app.auth import get_current_user
|
| 9 |
+
|
| 10 |
+
router = APIRouter(tags=["Certifications"])
|
| 11 |
+
|
| 12 |
+
@router.get("/", response_model=list[schemas.CertificationResponse])
|
| 13 |
+
def list_certifications(db: Session = Depends(get_db)):
|
| 14 |
+
return db.query(models.Certification).order_by(models.Certification.display_order.asc()).all()
|
| 15 |
+
|
| 16 |
+
@router.post("/", response_model=schemas.CertificationResponse)
|
| 17 |
+
def create_certification(payload: schemas.CertificationCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 18 |
+
cert = models.Certification(**payload.dict())
|
| 19 |
+
db.add(cert)
|
| 20 |
+
db.commit()
|
| 21 |
+
db.refresh(cert)
|
| 22 |
+
return cert
|
| 23 |
+
|
| 24 |
+
@router.put("/{id}", response_model=schemas.CertificationResponse)
|
| 25 |
+
def update_certification(id: UUID, payload: schemas.CertificationUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 26 |
+
cert = db.query(models.Certification).filter(models.Certification.id == id).first()
|
| 27 |
+
if not cert:
|
| 28 |
+
raise HTTPException(status_code=404, detail="Certification not found")
|
| 29 |
+
for key, value in payload.dict(exclude_unset=True).items():
|
| 30 |
+
setattr(cert, key, value)
|
| 31 |
+
db.commit()
|
| 32 |
+
db.refresh(cert)
|
| 33 |
+
return cert
|
| 34 |
+
|
| 35 |
+
@router.delete("/{id}", response_model=schemas.Msg)
|
| 36 |
+
def delete_certification(id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 37 |
+
cert = db.query(models.Certification).filter(models.Certification.id == id).first()
|
| 38 |
+
if not cert:
|
| 39 |
+
raise HTTPException(status_code=404, detail="Certification not found")
|
| 40 |
+
db.delete(cert)
|
| 41 |
+
db.commit()
|
| 42 |
+
return {"detail": "Deleted successfully"}
|
app/routes/contact.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/contact.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from app import models, schemas
|
| 6 |
+
from app.database import get_db
|
| 7 |
+
from app.utils.email_utils import send_contact_email
|
| 8 |
+
from app.auth import get_current_user
|
| 9 |
+
import uuid
|
| 10 |
+
|
| 11 |
+
router = APIRouter(tags=["Contact"])
|
| 12 |
+
|
| 13 |
+
@router.post("/", response_model=schemas.Msg)
|
| 14 |
+
def submit_message(payload: schemas.ContactMessageCreate, db: Session = Depends(get_db)):
|
| 15 |
+
success = send_contact_email(payload.name, payload.email, payload.subject, payload.message)
|
| 16 |
+
|
| 17 |
+
message = models.ContactMessage(**payload.dict())
|
| 18 |
+
db.add(message)
|
| 19 |
+
db.commit()
|
| 20 |
+
|
| 21 |
+
if not success:
|
| 22 |
+
raise HTTPException(status_code=500, detail="Failed to send email")
|
| 23 |
+
return {"detail": "Message sent successfully"}
|
| 24 |
+
|
| 25 |
+
@router.get("/messages", response_model=list[schemas.ContactMessageResponse])
|
| 26 |
+
def list_messages(db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 27 |
+
return db.query(models.ContactMessage).order_by(models.ContactMessage.created_at.desc()).all()
|
| 28 |
+
|
| 29 |
+
@router.put("/messages/{id}/read", response_model=schemas.Msg)
|
| 30 |
+
def mark_as_read(id: uuid.UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 31 |
+
message = db.query(models.ContactMessage).filter(models.ContactMessage.id == id).first()
|
| 32 |
+
if not message:
|
| 33 |
+
raise HTTPException(status_code=404, detail="Message not found")
|
| 34 |
+
message.is_read = True
|
| 35 |
+
db.commit()
|
| 36 |
+
return {"detail": "Marked as read"}
|
| 37 |
+
|
| 38 |
+
@router.delete("/messages/{id}", response_model=schemas.Msg)
|
| 39 |
+
def delete_message(id: uuid.UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 40 |
+
message = db.query(models.ContactMessage).filter(models.ContactMessage.id == id).first()
|
| 41 |
+
if not message:
|
| 42 |
+
raise HTTPException(status_code=404, detail="Message not found")
|
| 43 |
+
db.delete(message)
|
| 44 |
+
db.commit()
|
| 45 |
+
return {"detail": "Deleted successfully"}
|
app/routes/experience.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/experience.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from app import models, schemas
|
| 6 |
+
from app.database import get_db
|
| 7 |
+
from app.auth import get_current_user
|
| 8 |
+
from uuid import UUID
|
| 9 |
+
|
| 10 |
+
router = APIRouter(tags=["Experience"])
|
| 11 |
+
|
| 12 |
+
@router.get("/", response_model=list[schemas.ExperienceResponse])
|
| 13 |
+
def get_experiences(db: Session = Depends(get_db)):
|
| 14 |
+
return db.query(models.Experience).order_by(models.Experience.display_order.asc()).all()
|
| 15 |
+
|
| 16 |
+
@router.post("/", response_model=schemas.ExperienceResponse)
|
| 17 |
+
async def create_experience(
|
| 18 |
+
request: Request,
|
| 19 |
+
db: Session = Depends(get_db),
|
| 20 |
+
user=Depends(get_current_user)
|
| 21 |
+
):
|
| 22 |
+
body = await request.body()
|
| 23 |
+
print("🔍 RAW BODY:", body.decode())
|
| 24 |
+
|
| 25 |
+
from fastapi.encoders import jsonable_encoder
|
| 26 |
+
from pydantic import ValidationError
|
| 27 |
+
import json
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
data = json.loads(body)
|
| 31 |
+
payload = schemas.ExperienceCreate(**data)
|
| 32 |
+
except ValidationError as ve:
|
| 33 |
+
print("❌ VALIDATION ERROR:", ve)
|
| 34 |
+
raise HTTPException(status_code=422, detail=ve.errors())
|
| 35 |
+
|
| 36 |
+
exp = models.Experience(**payload.dict())
|
| 37 |
+
db.add(exp)
|
| 38 |
+
db.commit()
|
| 39 |
+
db.refresh(exp)
|
| 40 |
+
return exp
|
| 41 |
+
|
| 42 |
+
@router.put("/{id}", response_model=schemas.ExperienceResponse)
|
| 43 |
+
def update_experience(id: UUID, payload: schemas.ExperienceUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 44 |
+
exp = db.query(models.Experience).filter(models.Experience.id == id).first()
|
| 45 |
+
if not exp:
|
| 46 |
+
raise HTTPException(status_code=404, detail="Experience not found")
|
| 47 |
+
for key, value in payload.dict(exclude_unset=True).items():
|
| 48 |
+
setattr(exp, key, value)
|
| 49 |
+
db.commit()
|
| 50 |
+
db.refresh(exp)
|
| 51 |
+
return exp
|
| 52 |
+
|
| 53 |
+
@router.delete("/{id}", response_model=schemas.Msg)
|
| 54 |
+
def delete_experience(id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 55 |
+
exp = db.query(models.Experience).filter(models.Experience.id == id).first()
|
| 56 |
+
if not exp:
|
| 57 |
+
raise HTTPException(status_code=404, detail="Experience not found")
|
| 58 |
+
db.delete(exp)
|
| 59 |
+
db.commit()
|
| 60 |
+
return {"detail": "Deleted successfully"}
|
app/routes/health.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/health.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter
|
| 4 |
+
|
| 5 |
+
router = APIRouter(tags=["Health"])
|
| 6 |
+
|
| 7 |
+
@router.get("/", summary="Health Check")
|
| 8 |
+
def health_check():
|
| 9 |
+
return {"status": "ok"}
|
app/routes/hero.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/hero.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from app import models, schemas
|
| 6 |
+
from app.database import get_db
|
| 7 |
+
from app.auth import get_current_user
|
| 8 |
+
|
| 9 |
+
router = APIRouter(tags=["Hero"])
|
| 10 |
+
|
| 11 |
+
@router.get("/", response_model=schemas.HeroResponse)
|
| 12 |
+
def get_hero(db: Session = Depends(get_db)):
|
| 13 |
+
hero = db.query(models.Hero).first()
|
| 14 |
+
if not hero:
|
| 15 |
+
raise HTTPException(status_code=404, detail="Hero section not found")
|
| 16 |
+
return hero
|
| 17 |
+
|
| 18 |
+
@router.put("/", response_model=schemas.HeroResponse)
|
| 19 |
+
def update_hero(update: schemas.HeroUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 20 |
+
hero = db.query(models.Hero).first()
|
| 21 |
+
if not hero:
|
| 22 |
+
hero = models.Hero(**update.dict())
|
| 23 |
+
db.add(hero)
|
| 24 |
+
else:
|
| 25 |
+
for key, value in update.dict(exclude_unset=True).items():
|
| 26 |
+
setattr(hero, key, value)
|
| 27 |
+
db.commit()
|
| 28 |
+
db.refresh(hero)
|
| 29 |
+
return hero
|
app/routes/projects.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/projects.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from app import models, schemas
|
| 6 |
+
from app.database import get_db
|
| 7 |
+
from app.auth import get_current_user
|
| 8 |
+
from uuid import UUID
|
| 9 |
+
|
| 10 |
+
router = APIRouter(tags=["Projects"])
|
| 11 |
+
|
| 12 |
+
@router.get("/", response_model=list[schemas.ProjectResponse])
|
| 13 |
+
def list_projects(category: str = Query(None), featured: bool = Query(None), db: Session = Depends(get_db)):
|
| 14 |
+
query = db.query(models.Project)
|
| 15 |
+
if category:
|
| 16 |
+
query = query.filter(models.Project.category == category)
|
| 17 |
+
if featured is not None:
|
| 18 |
+
query = query.filter(models.Project.is_featured == featured)
|
| 19 |
+
return query.order_by(models.Project.display_order.asc()).all()
|
| 20 |
+
|
| 21 |
+
@router.get("/{id}", response_model=schemas.ProjectResponse)
|
| 22 |
+
def get_project(id: UUID, db: Session = Depends(get_db)):
|
| 23 |
+
project = db.query(models.Project).filter(models.Project.id == id).first()
|
| 24 |
+
if not project:
|
| 25 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 26 |
+
return project
|
| 27 |
+
|
| 28 |
+
@router.post("/", response_model=schemas.ProjectResponse)
|
| 29 |
+
def create_project(payload: schemas.ProjectCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 30 |
+
project = models.Project(**payload.dict())
|
| 31 |
+
print(payload)
|
| 32 |
+
db.add(project)
|
| 33 |
+
db.commit()
|
| 34 |
+
db.refresh(project)
|
| 35 |
+
return project
|
| 36 |
+
|
| 37 |
+
@router.put("/{id}", response_model=schemas.ProjectResponse)
|
| 38 |
+
def update_project(id: UUID, payload: schemas.ProjectUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 39 |
+
project = db.query(models.Project).filter(models.Project.id == id).first()
|
| 40 |
+
if not project:
|
| 41 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 42 |
+
for key, value in payload.dict(exclude_unset=True).items():
|
| 43 |
+
setattr(project, key, value)
|
| 44 |
+
db.commit()
|
| 45 |
+
db.refresh(project)
|
| 46 |
+
return project
|
| 47 |
+
|
| 48 |
+
@router.delete("/{id}", response_model=schemas.Msg)
|
| 49 |
+
def delete_project(id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 50 |
+
project = db.query(models.Project).filter(models.Project.id == id).first()
|
| 51 |
+
if not project:
|
| 52 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 53 |
+
db.delete(project)
|
| 54 |
+
db.commit()
|
| 55 |
+
return {"detail": "Deleted successfully"}
|
app/routes/skills.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/routes/skills.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from uuid import UUID
|
| 6 |
+
from app import models, schemas
|
| 7 |
+
from app.database import get_db
|
| 8 |
+
from app.auth import get_current_user
|
| 9 |
+
|
| 10 |
+
router = APIRouter(tags=["Skills"])
|
| 11 |
+
|
| 12 |
+
# Skill Categories
|
| 13 |
+
@router.get("/categories", response_model=list[schemas.SkillCategoryResponse])
|
| 14 |
+
def get_categories(db: Session = Depends(get_db)):
|
| 15 |
+
return db.query(models.SkillCategory).order_by(models.SkillCategory.display_order.asc()).all()
|
| 16 |
+
|
| 17 |
+
@router.post("/categories", response_model=schemas.SkillCategoryResponse)
|
| 18 |
+
def create_category(payload: schemas.SkillCategoryCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 19 |
+
category = models.SkillCategory(**payload.dict())
|
| 20 |
+
db.add(category)
|
| 21 |
+
db.commit()
|
| 22 |
+
db.refresh(category)
|
| 23 |
+
return category
|
| 24 |
+
|
| 25 |
+
@router.put("/categories/{id}", response_model=schemas.SkillCategoryResponse)
|
| 26 |
+
def update_category(id: UUID, payload: schemas.SkillCategoryUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 27 |
+
category = db.query(models.SkillCategory).filter(models.SkillCategory.id == id).first()
|
| 28 |
+
if not category:
|
| 29 |
+
raise HTTPException(status_code=404, detail="Category not found")
|
| 30 |
+
for key, value in payload.dict(exclude_unset=True).items():
|
| 31 |
+
setattr(category, key, value)
|
| 32 |
+
db.commit()
|
| 33 |
+
db.refresh(category)
|
| 34 |
+
return category
|
| 35 |
+
|
| 36 |
+
@router.delete("/categories/{id}", response_model=schemas.Msg)
|
| 37 |
+
def delete_category(id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 38 |
+
category = db.query(models.SkillCategory).filter(models.SkillCategory.id == id).first()
|
| 39 |
+
if not category:
|
| 40 |
+
raise HTTPException(status_code=404, detail="Category not found")
|
| 41 |
+
db.delete(category)
|
| 42 |
+
db.commit()
|
| 43 |
+
return {"detail": "Deleted successfully"}
|
| 44 |
+
|
| 45 |
+
# Skills
|
| 46 |
+
@router.get("/", response_model=list[schemas.SkillResponse])
|
| 47 |
+
def get_skills(db: Session = Depends(get_db)):
|
| 48 |
+
return db.query(models.Skill).order_by(models.Skill.display_order.asc()).all()
|
| 49 |
+
|
| 50 |
+
@router.post("/", response_model=schemas.SkillResponse)
|
| 51 |
+
def create_skill(payload: schemas.SkillCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 52 |
+
skill = models.Skill(**payload.dict())
|
| 53 |
+
db.add(skill)
|
| 54 |
+
db.commit()
|
| 55 |
+
db.refresh(skill)
|
| 56 |
+
return skill
|
| 57 |
+
|
| 58 |
+
@router.put("/{id}", response_model=schemas.SkillResponse)
|
| 59 |
+
def update_skill(id: UUID, payload: schemas.SkillUpdate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 60 |
+
skill = db.query(models.Skill).filter(models.Skill.id == id).first()
|
| 61 |
+
if not skill:
|
| 62 |
+
raise HTTPException(status_code=404, detail="Skill not found")
|
| 63 |
+
for key, value in payload.dict(exclude_unset=True).items():
|
| 64 |
+
setattr(skill, key, value)
|
| 65 |
+
db.commit()
|
| 66 |
+
db.refresh(skill)
|
| 67 |
+
return skill
|
| 68 |
+
|
| 69 |
+
@router.delete("/{id}", response_model=schemas.Msg)
|
| 70 |
+
def delete_skill(id: UUID, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
| 71 |
+
skill = db.query(models.Skill).filter(models.Skill.id == id).first()
|
| 72 |
+
if not skill:
|
| 73 |
+
raise HTTPException(status_code=404, detail="Skill not found")
|
| 74 |
+
db.delete(skill)
|
| 75 |
+
db.commit()
|
| 76 |
+
return {"detail": "Deleted successfully"}
|
app/routes/upload.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import uuid
|
| 3 |
+
from fastapi import APIRouter, UploadFile, File, HTTPException, status
|
| 4 |
+
from fastapi.responses import JSONResponse
|
| 5 |
+
|
| 6 |
+
UPLOAD_DIR = "static/uploads"
|
| 7 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 8 |
+
|
| 9 |
+
router = APIRouter(tags=["Upload"])
|
| 10 |
+
|
| 11 |
+
@router.post("/upload")
|
| 12 |
+
def upload_image(file: UploadFile = File(...)):
|
| 13 |
+
try:
|
| 14 |
+
extension = file.filename.split(".")[-1]
|
| 15 |
+
if extension.lower() not in ("jpg", "jpeg", "png", "gif", "webp", "ico"):
|
| 16 |
+
raise HTTPException(status_code=400, detail="Unsupported file type")
|
| 17 |
+
|
| 18 |
+
file_id = f"{uuid.uuid4()}.{extension}"
|
| 19 |
+
file_path = os.path.join(UPLOAD_DIR, file_id)
|
| 20 |
+
|
| 21 |
+
with open(file_path, "wb") as buffer:
|
| 22 |
+
buffer.write(file.file.read())
|
| 23 |
+
|
| 24 |
+
file_url = f"/static/uploads/{file_id}"
|
| 25 |
+
return JSONResponse(status_code=status.HTTP_201_CREATED, content={"url": file_url})
|
| 26 |
+
|
| 27 |
+
except Exception as e:
|
| 28 |
+
raise HTTPException(status_code=500, detail=f"File upload failed: {str(e)}")
|
app/schemas.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Directory: app/schemas.py
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, date
|
| 4 |
+
from typing import Optional, List
|
| 5 |
+
from pydantic import BaseModel, EmailStr, Field, validator
|
| 6 |
+
from uuid import UUID
|
| 7 |
+
from fastapi.security import OAuth2PasswordBearer
|
| 8 |
+
|
| 9 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
| 10 |
+
|
| 11 |
+
# --- Shared Schemas ---
|
| 12 |
+
class Msg(BaseModel):
|
| 13 |
+
detail: str
|
| 14 |
+
|
| 15 |
+
# --- User ---
|
| 16 |
+
class UserBase(BaseModel):
|
| 17 |
+
email: EmailStr
|
| 18 |
+
full_name: str
|
| 19 |
+
|
| 20 |
+
class UserCreate(UserBase):
|
| 21 |
+
password: str
|
| 22 |
+
|
| 23 |
+
class UserOut(UserBase):
|
| 24 |
+
id: UUID
|
| 25 |
+
is_active: bool
|
| 26 |
+
created_at: datetime
|
| 27 |
+
updated_at: datetime
|
| 28 |
+
|
| 29 |
+
# --- Token ---
|
| 30 |
+
class Token(BaseModel):
|
| 31 |
+
access_token: str
|
| 32 |
+
refresh_token: str
|
| 33 |
+
token_type: str = "bearer"
|
| 34 |
+
|
| 35 |
+
# --- Hero ---
|
| 36 |
+
class HeroBase(BaseModel):
|
| 37 |
+
title: str
|
| 38 |
+
subtitle: str
|
| 39 |
+
description: str
|
| 40 |
+
cta_text: str
|
| 41 |
+
cta_link: str
|
| 42 |
+
background_image: str
|
| 43 |
+
profile_image: str
|
| 44 |
+
animation_style: str
|
| 45 |
+
show_scroll_indicator: bool
|
| 46 |
+
|
| 47 |
+
class HeroUpdate(HeroBase):
|
| 48 |
+
pass
|
| 49 |
+
|
| 50 |
+
class HeroResponse(HeroBase):
|
| 51 |
+
id: UUID
|
| 52 |
+
updated_at: datetime
|
| 53 |
+
|
| 54 |
+
# --- About ---
|
| 55 |
+
class AboutBase(BaseModel):
|
| 56 |
+
title: str
|
| 57 |
+
introduction: str
|
| 58 |
+
description: str
|
| 59 |
+
profile_image: str
|
| 60 |
+
highlights: List[str]
|
| 61 |
+
skills: List[str]
|
| 62 |
+
location: str
|
| 63 |
+
availability: str
|
| 64 |
+
email: str
|
| 65 |
+
phone: str
|
| 66 |
+
|
| 67 |
+
class AboutResponse(AboutBase):
|
| 68 |
+
id: UUID
|
| 69 |
+
updated_at: datetime
|
| 70 |
+
|
| 71 |
+
# --- Experience ---
|
| 72 |
+
class ExperienceBase(BaseModel):
|
| 73 |
+
title: Optional[str] = None
|
| 74 |
+
company: Optional[str] = None
|
| 75 |
+
location: Optional[str] = None
|
| 76 |
+
start_date: date = Field(..., alias="startDate")
|
| 77 |
+
end_date: Optional[date] = Field(None, alias="endDate")
|
| 78 |
+
is_current: Optional[bool] = Field(None, alias="current")
|
| 79 |
+
description: Optional[str] = None
|
| 80 |
+
technologies: Optional[List[str]] = None
|
| 81 |
+
achievements: Optional[List[str]] = None
|
| 82 |
+
display_order: Optional[int] = None
|
| 83 |
+
|
| 84 |
+
class Config:
|
| 85 |
+
allow_population_by_field_name = True
|
| 86 |
+
|
| 87 |
+
class ExperienceCreate(ExperienceBase):
|
| 88 |
+
@validator("start_date", "end_date", pre=True)
|
| 89 |
+
def parse_partial_date(cls, value):
|
| 90 |
+
if value == "":
|
| 91 |
+
return None
|
| 92 |
+
if isinstance(value, str) and len(value) == 7:
|
| 93 |
+
return datetime.strptime(value + "-01", "%Y-%m-%d").date()
|
| 94 |
+
return value
|
| 95 |
+
|
| 96 |
+
class ExperienceUpdate(ExperienceBase):
|
| 97 |
+
pass
|
| 98 |
+
|
| 99 |
+
class ExperienceResponse(ExperienceBase):
|
| 100 |
+
id: UUID
|
| 101 |
+
created_at: datetime
|
| 102 |
+
updated_at: datetime
|
| 103 |
+
|
| 104 |
+
class Config:
|
| 105 |
+
orm_mode = True
|
| 106 |
+
populate_by_name = True
|
| 107 |
+
|
| 108 |
+
# --- Project ---
|
| 109 |
+
class ProjectBase(BaseModel):
|
| 110 |
+
title: str
|
| 111 |
+
description: str
|
| 112 |
+
long_description: str
|
| 113 |
+
image: Optional[str] = None
|
| 114 |
+
technologies: List[str] = []
|
| 115 |
+
category: str
|
| 116 |
+
status: str
|
| 117 |
+
is_featured: bool
|
| 118 |
+
github_url: Optional[str] = None
|
| 119 |
+
live_url: Optional[str] = None
|
| 120 |
+
start_date: date
|
| 121 |
+
end_date: Optional[date] = None
|
| 122 |
+
highlights: List[str] = []
|
| 123 |
+
display_order: int
|
| 124 |
+
|
| 125 |
+
class ProjectCreate(ProjectBase):
|
| 126 |
+
pass
|
| 127 |
+
|
| 128 |
+
class ProjectUpdate(ProjectBase):
|
| 129 |
+
pass
|
| 130 |
+
|
| 131 |
+
class ProjectResponse(ProjectBase):
|
| 132 |
+
id: UUID
|
| 133 |
+
created_at: datetime
|
| 134 |
+
updated_at: datetime
|
| 135 |
+
|
| 136 |
+
# --- Skill Category ---
|
| 137 |
+
class SkillCategoryBase(BaseModel):
|
| 138 |
+
name: str
|
| 139 |
+
color: str
|
| 140 |
+
description: str
|
| 141 |
+
display_order: Optional[int] = None
|
| 142 |
+
|
| 143 |
+
class SkillCategoryCreate(SkillCategoryBase):
|
| 144 |
+
pass
|
| 145 |
+
|
| 146 |
+
class SkillCategoryUpdate(SkillCategoryBase):
|
| 147 |
+
pass
|
| 148 |
+
|
| 149 |
+
class SkillCategoryResponse(SkillCategoryBase):
|
| 150 |
+
id: UUID
|
| 151 |
+
|
| 152 |
+
# --- Skill ---
|
| 153 |
+
class SkillBase(BaseModel):
|
| 154 |
+
name: str
|
| 155 |
+
category_id: UUID
|
| 156 |
+
proficiency: int
|
| 157 |
+
years_of_experience: int
|
| 158 |
+
icon: Optional[str]
|
| 159 |
+
is_featured: bool
|
| 160 |
+
display_order: Optional[int] = None
|
| 161 |
+
|
| 162 |
+
class SkillCreate(SkillBase):
|
| 163 |
+
pass
|
| 164 |
+
|
| 165 |
+
class SkillUpdate(SkillBase):
|
| 166 |
+
pass
|
| 167 |
+
|
| 168 |
+
class SkillResponse(SkillBase):
|
| 169 |
+
id: UUID
|
| 170 |
+
|
| 171 |
+
# --- Blog ---
|
| 172 |
+
class BlogPostBase(BaseModel):
|
| 173 |
+
title: str
|
| 174 |
+
slug: str
|
| 175 |
+
excerpt: str
|
| 176 |
+
content: str
|
| 177 |
+
author: str
|
| 178 |
+
publish_date: date
|
| 179 |
+
status: str
|
| 180 |
+
is_featured: bool
|
| 181 |
+
image: Optional[str] = None
|
| 182 |
+
tags: List[str]
|
| 183 |
+
category: str
|
| 184 |
+
read_time: int
|
| 185 |
+
seo_title: str
|
| 186 |
+
seo_description: str
|
| 187 |
+
|
| 188 |
+
class BlogPostCreate(BlogPostBase):
|
| 189 |
+
pass
|
| 190 |
+
|
| 191 |
+
class BlogPostUpdate(BlogPostBase):
|
| 192 |
+
pass
|
| 193 |
+
|
| 194 |
+
class BlogPostResponse(BlogPostBase):
|
| 195 |
+
id: UUID
|
| 196 |
+
views: int
|
| 197 |
+
created_at: datetime
|
| 198 |
+
updated_at: datetime
|
| 199 |
+
|
| 200 |
+
# --- Certification ---
|
| 201 |
+
class CertificationBase(BaseModel):
|
| 202 |
+
name: str
|
| 203 |
+
issuer: str
|
| 204 |
+
issue_date: date
|
| 205 |
+
expiry_date: Optional[date]
|
| 206 |
+
credential_id: Optional[str]
|
| 207 |
+
verification_url: Optional[str]
|
| 208 |
+
image: Optional[str]
|
| 209 |
+
description: Optional[str]
|
| 210 |
+
is_featured: bool
|
| 211 |
+
display_order: int
|
| 212 |
+
|
| 213 |
+
class CertificationCreate(CertificationBase):
|
| 214 |
+
pass
|
| 215 |
+
|
| 216 |
+
class CertificationUpdate(CertificationBase):
|
| 217 |
+
pass
|
| 218 |
+
|
| 219 |
+
class CertificationResponse(CertificationBase):
|
| 220 |
+
id: UUID
|
| 221 |
+
|
| 222 |
+
# --- Contact Message ---
|
| 223 |
+
class ContactMessageCreate(BaseModel):
|
| 224 |
+
name: str
|
| 225 |
+
email: str
|
| 226 |
+
subject: str
|
| 227 |
+
message: str
|
| 228 |
+
|
| 229 |
+
class ContactMessageResponse(ContactMessageCreate):
|
| 230 |
+
id: UUID
|
| 231 |
+
is_read: bool
|
| 232 |
+
created_at: datetime
|
app/utils/email_utils.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# File: app/utils/email_utils.py
|
| 2 |
+
|
| 3 |
+
import smtplib
|
| 4 |
+
from email.mime.text import MIMEText
|
| 5 |
+
from email.mime.multipart import MIMEMultipart
|
| 6 |
+
from app.config import settings
|
| 7 |
+
|
| 8 |
+
def send_contact_email(name: str, email: str, subject: str, message: str):
|
| 9 |
+
msg = MIMEMultipart()
|
| 10 |
+
msg['From'] = settings.EMAIL_USER
|
| 11 |
+
msg['To'] = settings.EMAIL_USER
|
| 12 |
+
msg['Subject'] = f"[Portfolio Contact] {subject}"
|
| 13 |
+
|
| 14 |
+
body = f"""
|
| 15 |
+
Name: {name}
|
| 16 |
+
Email: {email}
|
| 17 |
+
Subject: {subject}
|
| 18 |
+
|
| 19 |
+
Message:
|
| 20 |
+
{message}
|
| 21 |
+
"""
|
| 22 |
+
msg.attach(MIMEText(body, 'plain'))
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
|
| 26 |
+
server.starttls()
|
| 27 |
+
server.login(settings.EMAIL_USER, settings.EMAIL_PASS)
|
| 28 |
+
server.send_message(msg)
|
| 29 |
+
server.quit()
|
| 30 |
+
return True
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Email sending failed: {e}")
|
| 33 |
+
return False
|
main.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.config import settings
|
| 4 |
+
from app.database import engine, Base
|
| 5 |
+
from fastapi.staticfiles import StaticFiles
|
| 6 |
+
from app.routes import (
|
| 7 |
+
auth, hero, about, experience, projects, skills, blog, certifications, contact, health, upload
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
Base.metadata.create_all(bind=engine)
|
| 11 |
+
|
| 12 |
+
app = FastAPI(title="Portfolio CMS API", version="1.0.0")
|
| 13 |
+
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
CORSMiddleware,
|
| 16 |
+
allow_origins=settings.ALLOWED_ORIGINS,
|
| 17 |
+
allow_credentials=True,
|
| 18 |
+
allow_methods=["*"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Routes
|
| 23 |
+
app.include_router(auth.router, prefix="/auth")
|
| 24 |
+
app.include_router(hero.router, prefix="/hero")
|
| 25 |
+
app.include_router(about.router, prefix="/about")
|
| 26 |
+
app.include_router(experience.router, prefix="/experience")
|
| 27 |
+
app.include_router(projects.router, prefix="/projects")
|
| 28 |
+
app.include_router(skills.router, prefix="/skills")
|
| 29 |
+
app.include_router(blog.router, prefix="/blog")
|
| 30 |
+
app.include_router(certifications.router, prefix="/certifications")
|
| 31 |
+
app.include_router(contact.router, prefix="/contact")
|
| 32 |
+
app.include_router(health.router, prefix="/health")
|
| 33 |
+
app.include_router(upload.router, prefix="/upload")
|
| 34 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
python-dotenv
|
| 4 |
+
sqlalchemy
|
| 5 |
+
psycopg2-binary
|
| 6 |
+
passlib[bcrypt]
|
| 7 |
+
python-jose
|
| 8 |
+
python-multipart
|
| 9 |
+
email-validator
|
| 10 |
+
pydantic-settings
|
static/uploads/2fba62d9-4ec2-4c7c-9234-9b1cafd90b78.png
ADDED
|
static/uploads/4df4eb5a-d474-49e7-9c1a-59532d56632f.png
ADDED
|
static/uploads/5e90e30d-3ce2-4567-9b79-95001a2a04af.ico
ADDED
|
|
static/uploads/89492760-9906-442a-9386-d13df78b2a44.png
ADDED
|
static/uploads/92c1782e-9a40-4ab7-b832-0ebc3562522e.ico
ADDED
|
|
static/uploads/c90a0883-a9ae-4e50-a6dd-8ba494718910.png
ADDED
|
static/uploads/ec465097-32bd-4767-a503-7208d71076a2.png
ADDED
|