Skip to content

Commit a10964a

Browse files
committed
Add backend files and Render configuration
1 parent 4c392bc commit a10964a

11 files changed

Lines changed: 338 additions & 0 deletions

File tree

backend/Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Use the official Python image
2+
FROM python:3.12-slim
3+
4+
# Set the working directory
5+
WORKDIR /app
6+
7+
# Install dependencies
8+
COPY requirements.txt .
9+
RUN pip install --no-cache-dir -r requirements.txt
10+
11+
# Copy the application code
12+
COPY . .
13+
14+
# Expose the port Render will use
15+
EXPOSE 10000
16+
17+
# Start the FastAPI application
18+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "10000"]

backend/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# MathCodeLab Certificate Verification System
2+
3+
A professional, scalable certificate verification system for MathCodeLab, using FastAPI and SQLite, with a static frontend for public verification.
4+
5+
## Architecture
6+
- **Frontend:** Static HTML/CSS/JS (GitHub Pages)
7+
- **Backend:** FastAPI (Python), SQLite (SQLAlchemy)
8+
- **API URL:** https://api.mathcodelab.de
9+
10+
## File Structure
11+
```
12+
verify/
13+
index.html
14+
verify.css
15+
verify.js
16+
backend/
17+
app/
18+
main.py
19+
database.py
20+
models.py
21+
schemas.py
22+
crud.py
23+
security.py
24+
scripts/
25+
create_certificate.py
26+
seed_demo_data.py
27+
requirements.txt
28+
README.md
29+
.env.example
30+
```
31+
32+
## Local Setup
33+
1. **Clone the repo and enter backend:**
34+
```bash
35+
cd backend
36+
python3 -m venv venv
37+
source venv/bin/activate
38+
pip install -r requirements.txt
39+
cp .env.example .env
40+
# Edit .env as needed
41+
```
42+
2. **Initialize the database:**
43+
```bash
44+
python -c "from app.database import Base, engine; Base.metadata.create_all(bind=engine)"
45+
```
46+
3. **Seed demo data:**
47+
```bash
48+
python scripts/seed_demo_data.py
49+
```
50+
4. **Run the server:**
51+
```bash
52+
uvicorn app.main:app --reload
53+
```
54+
55+
## API Endpoints
56+
- `GET /health` — API health check
57+
- `GET /verify/{certificate_id}` — Verify a certificate
58+
- `POST /admin/certificates` — Create a certificate (admin, API key required)
59+
- `PATCH /admin/certificates/{certificate_id}/revoke` — Revoke a certificate (admin, API key required)
60+
61+
## Issuing a Certificate
62+
- Use the admin API or run `python scripts/create_certificate.py` interactively.
63+
64+
## Revoking a Certificate
65+
- Use the PATCH admin endpoint with a revocation reason.
66+
67+
## Frontend Integration
68+
- The verification page (`verify/index.html`) calls the backend API at `https://api.mathcodelab.de/verify/{certificate_id}`.
69+
- Supports both `/verify/?id=...` and `/verify/ID` URL formats.
70+
71+
## Environment Variables
72+
- `DATABASE_URL` — e.g. `sqlite:///./certificates.db`
73+
- `ADMIN_API_KEY` — API key for admin endpoints
74+
- `ALLOWED_ORIGINS` — e.g. `https://mathcodelab.de,https://www.mathcodelab.de`
75+
76+
## Deployment
77+
- Deploy backend to Railway, Render, Fly.io, etc.
78+
- Use the production command:
79+
```bash
80+
uvicorn app.main:app --host 0.0.0.0 --port $PORT
81+
```
82+
- Point `api.mathcodelab.de` to your backend host (CNAME or A record).
83+
- Set CORS origins via `ALLOWED_ORIGINS`.
84+
85+
## Certificate PDF Integration
86+
- Each certificate should display:
87+
- Certificate ID (e.g. MCL-2026-XXXXXX)
88+
- Verification URL: `https://mathcodelab.de/verify/?id={certificate_id}`
89+
- (Optional) QR code to the same URL
90+
- The verification page confirms MathCodeLab as the issuer, not external accreditation.
91+
92+
## Wording Policy
93+
- Use “Certificate of Completion” or “Certificate of Participation”.
94+
- Do **not** use “accredited”, “state-recognized”, “official degree”, or “diploma”.
95+
96+
---
97+
98+
For questions, contact Mohammad Orabe.

backend/app/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# MathCodeLab Backend App Package

backend/app/crud.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from sqlalchemy.orm import Session
2+
from . import models, schemas
3+
from datetime import datetime
4+
import random, string
5+
6+
def generate_certificate_id(year: int) -> str:
7+
random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))
8+
return f"MCL-{year}-{random_part}"
9+
10+
def get_certificate_by_public_id(db: Session, certificate_id: str):
11+
return db.query(models.Certificate).filter(models.Certificate.certificate_id == certificate_id).first()
12+
13+
def create_certificate(db: Session, cert_in: schemas.CertificateCreate):
14+
year = datetime.now().year
15+
cert_id = generate_certificate_id(year)
16+
cert = models.Certificate(
17+
certificate_id=cert_id,
18+
student_name=cert_in.student_name,
19+
course_title=cert_in.course_title,
20+
completion_date=cert_in.completion_date,
21+
duration_hours=cert_in.duration_hours,
22+
issuer=cert_in.issuer,
23+
instructor=cert_in.instructor,
24+
status=models.CertificateStatus.valid
25+
)
26+
db.add(cert)
27+
db.commit()
28+
db.refresh(cert)
29+
return cert
30+
31+
def revoke_certificate(db: Session, certificate_id: str, reason: str = None):
32+
cert = get_certificate_by_public_id(db, certificate_id)
33+
if not cert:
34+
return None
35+
cert.status = models.CertificateStatus.revoked
36+
cert.revocation_reason = reason
37+
cert.updated_at = datetime.utcnow()
38+
db.commit()
39+
db.refresh(cert)
40+
return cert

backend/app/database.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
from sqlalchemy import create_engine
3+
from sqlalchemy.orm import sessionmaker, declarative_base
4+
5+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./certificates.db")
6+
7+
# Adjust connection arguments for SQLite only
8+
connect_args = {"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else None
9+
10+
engine = create_engine(DATABASE_URL, connect_args=connect_args)
11+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
12+
Base = declarative_base()
13+
14+
def get_db():
15+
db = SessionLocal()
16+
try:
17+
yield db
18+
finally:
19+
db.close()

backend/app/main.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
from fastapi import FastAPI, HTTPException, Depends, Header
3+
from fastapi.middleware.cors import CORSMiddleware
4+
from sqlalchemy.orm import Session
5+
from starlette.responses import JSONResponse
6+
from datetime import datetime
7+
from . import models, schemas, crud, database, security
8+
9+
app = FastAPI(title="MathCodeLab Certificate Verification API")
10+
11+
# CORS
12+
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",")
13+
app.add_middleware(
14+
CORSMiddleware,
15+
allow_origins=[origin.strip() for origin in ALLOWED_ORIGINS],
16+
allow_credentials=True,
17+
allow_methods=["*"],
18+
allow_headers=["*"],
19+
)
20+
21+
# Dependency
22+
get_db = database.get_db
23+
24+
@app.get("/health")
25+
def health():
26+
return {"status": "ok"}
27+
28+
@app.get("/verify/{certificate_id}", response_model=schemas.CertificateVerificationResponse)
29+
def verify_certificate(certificate_id: str, db: Session = Depends(get_db)):
30+
cert = crud.get_certificate_by_public_id(db, certificate_id)
31+
if not cert:
32+
return JSONResponse(status_code=404, content={
33+
"status": "invalid",
34+
"certificate_id": certificate_id,
35+
"message": "Certificate not found"
36+
})
37+
if cert.status == "revoked":
38+
return {
39+
"status": "revoked",
40+
"certificate_id": cert.certificate_id,
41+
"revocation_reason": cert.revocation_reason or ""
42+
}
43+
return {
44+
"status": "valid",
45+
"certificate_id": cert.certificate_id,
46+
"student_name": cert.student_name,
47+
"course_title": cert.course_title,
48+
"completion_date": cert.completion_date,
49+
"duration_hours": cert.duration_hours,
50+
"issuer": cert.issuer,
51+
"instructor": cert.instructor,
52+
"verified_at": datetime.utcnow().isoformat() + "Z"
53+
}
54+
55+
@app.post("/admin/certificates", response_model=schemas.CertificateOut)
56+
def create_certificate(
57+
cert_in: schemas.CertificateCreate,
58+
db: Session = Depends(get_db),
59+
api_key: str = Depends(security.verify_api_key)
60+
):
61+
return crud.create_certificate(db, cert_in)
62+
63+
@app.patch("/admin/certificates/{certificate_id}/revoke", response_model=schemas.CertificateOut)
64+
def revoke_certificate(
65+
certificate_id: str,
66+
body: schemas.CertificateRevoke,
67+
db: Session = Depends(get_db),
68+
api_key: str = Depends(security.verify_api_key)
69+
):
70+
cert = crud.revoke_certificate(db, certificate_id, body.revocation_reason)
71+
if not cert:
72+
raise HTTPException(status_code=404, detail="Certificate not found")
73+
return cert

backend/app/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from sqlalchemy import Column, Integer, String, DateTime, Enum
2+
from sqlalchemy.sql import func
3+
from .database import Base
4+
import enum
5+
6+
class CertificateStatus(str, enum.Enum):
7+
valid = "valid"
8+
revoked = "revoked"
9+
10+
class Certificate(Base):
11+
__tablename__ = "certificates"
12+
id = Column(Integer, primary_key=True, index=True)
13+
certificate_id = Column(String, unique=True, index=True, nullable=False)
14+
student_name = Column(String, nullable=False)
15+
course_title = Column(String, nullable=False)
16+
completion_date = Column(String, nullable=False)
17+
duration_hours = Column(Integer, nullable=False)
18+
issuer = Column(String, default="MathCodeLab", nullable=False)
19+
instructor = Column(String, default="Mohammad Orabe", nullable=False)
20+
status = Column(Enum(CertificateStatus), default=CertificateStatus.valid, nullable=False)
21+
revocation_reason = Column(String, nullable=True)
22+
created_at = Column(DateTime(timezone=True), server_default=func.now())
23+
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())

backend/app/schemas.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from pydantic import BaseModel, Field
2+
from typing import Optional
3+
4+
class CertificateBase(BaseModel):
5+
student_name: str
6+
course_title: str
7+
completion_date: str
8+
duration_hours: int
9+
issuer: Optional[str] = "MathCodeLab"
10+
instructor: Optional[str] = "Mohammad Orabe"
11+
12+
class CertificateCreate(CertificateBase):
13+
pass
14+
15+
class CertificateOut(CertificateBase):
16+
certificate_id: str
17+
status: str
18+
revocation_reason: Optional[str] = None
19+
class Config:
20+
from_attributes = True
21+
22+
class CertificateVerificationResponse(BaseModel):
23+
status: str
24+
certificate_id: str
25+
student_name: Optional[str] = None
26+
course_title: Optional[str] = None
27+
completion_date: Optional[str] = None
28+
duration_hours: Optional[int] = None
29+
issuer: Optional[str] = None
30+
instructor: Optional[str] = None
31+
verified_at: Optional[str] = None
32+
revocation_reason: Optional[str] = None
33+
message: Optional[str] = None
34+
35+
class CertificateRevoke(BaseModel):
36+
revocation_reason: Optional[str] = None

backend/app/security.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import os
2+
from fastapi import Header, HTTPException, status, Depends
3+
4+
def verify_api_key(authorization: str = Header(...)):
5+
api_key = os.getenv("ADMIN_API_KEY")
6+
if not api_key:
7+
raise HTTPException(status_code=500, detail="Admin API key not set")
8+
if not authorization or not authorization.startswith("Bearer "):
9+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing or invalid Authorization header")
10+
token = authorization.split(" ", 1)[1]
11+
if token != api_key:
12+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
13+
return token

backend/render.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
- type: web
3+
name: mathcodelab.github.io
4+
env: python
5+
buildCommand: "pip install -r requirements.txt"
6+
startCommand: "uvicorn app.main:app --host 0.0.0.0 --port 10000"
7+
plan: free
8+
envVars:
9+
- key: DATABASE_URL
10+
fromSecret: DATABASE_URL
11+
- key: ADMIN_API_KEY
12+
fromSecret: ADMIN_API_KEY

0 commit comments

Comments
 (0)