shakauthossain commited on
Commit
deec4d6
·
verified ·
1 Parent(s): 9847103

Upload 32 files

Browse files
.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