diff --git a/app/main.py b/api/main.py similarity index 63% rename from app/main.py rename to api/main.py index 9ef3e03..c8c1296 100644 --- a/app/main.py +++ b/api/main.py @@ -1,19 +1,15 @@ from fastapi import FastAPI, Depends, Request, Path from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import RedirectResponse, JSONResponse -from fastapi.templating import Jinja2Templates -from fastapi.staticfiles import StaticFiles -from app.routes.auth_routes import router as auth_router -from app.routes.links_routes import router as links_router -from app.routes.user_routes import router as user_router -from typing import Annotated, Union +from fastapi.responses import JSONResponse, RedirectResponse +from api.routes.auth_routes import router as auth_router +from api.routes.links_routes import router as links_router +from api.routes.user_routes import router as user_router +from typing import Annotated from fastapi.exceptions import HTTPException from starlette.status import HTTP_404_NOT_FOUND -from app.util.authentication import get_current_user -from app.util.db_dependency import get_db -from app.util.log import log -from app.schemas.auth_schemas import User +from api.util.db_dependency import get_db +from api.util.log import log from models import Link @@ -28,45 +24,28 @@ app = FastAPI( }, ) +origins = [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "localhost:3000", + "127.0.0.1:3000", + # f"{CUSTOM_DOMAIN}" +] + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=origins, + allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - allow_credentials=True, ) -app.mount("/static", StaticFiles(directory="app/static"), name="static") -templates = Jinja2Templates(directory="app/templates") - # Import routes app.include_router(auth_router, prefix="/api") app.include_router(links_router, prefix="/api") app.include_router(user_router, prefix="/api") -@app.get("/login") -async def login(request: Request): - return templates.TemplateResponse("login.html", {"request": request}) - - -@app.get("/signup") -async def signup(request: Request): - return templates.TemplateResponse("signup.html", {"request": request}) - - -@app.get("/dashboard") -async def dashboard( - request: Request, - response: Union[User, RedirectResponse] = Depends(get_current_user), -): - if isinstance(response, RedirectResponse): - return response - return templates.TemplateResponse( - "dashboard.html", {"request": request, "user": response.username} - ) - - @app.get("/c/{link}") async def log_redirect( link: Annotated[str, Path(title="Redirect link")], diff --git a/api/routes/auth_routes.py b/api/routes/auth_routes.py new file mode 100644 index 0000000..c51557f --- /dev/null +++ b/api/routes/auth_routes.py @@ -0,0 +1,46 @@ +from fastapi import Depends, APIRouter, status, HTTPException +from fastapi.security import OAuth2PasswordRequestForm +from fastapi.responses import Response, JSONResponse +from datetime import timedelta +from typing import Annotated + +from api.util.authentication import ( + create_access_token, + authenticate_user, +) +from api.util.db_dependency import get_db + + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/token", summary="Authenticate and get an access token") +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()], + response: Response, + db=Depends(get_db), +): + """ + Return an access token for the user, if the given authentication details are correct + """ + user = authenticate_user(db, form_data.username, form_data.password) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=1) + access_token = create_access_token( + data={"sub": user.id, "username": user.username}, + expires_delta=access_token_expires, + ) + response = JSONResponse(content={"success": True}) + response.set_cookie( + key="access_token", + value=access_token, + httponly=True, # Prevents client-side access + # secure=True, # Cookies are only sent over HTTPS + ) + return response diff --git a/app/routes/links_routes.py b/api/routes/links_routes.py similarity index 96% rename from app/routes/links_routes.py rename to api/routes/links_routes.py index 874f3c2..5ed565b 100644 --- a/app/routes/links_routes.py +++ b/api/routes/links_routes.py @@ -6,11 +6,11 @@ import random import datetime import validators -from app.util.db_dependency import get_db +from api.util.db_dependency import get_db from models import Link, Log -from app.schemas.links_schemas import URLSchema -from app.schemas.auth_schemas import User -from app.util.authentication import get_current_user +from api.schemas.links_schemas import URLSchema +from api.schemas.auth_schemas import User +from api.util.authentication import get_current_user router = APIRouter(prefix="/links", tags=["links"]) diff --git a/app/routes/user_routes.py b/api/routes/user_routes.py similarity index 94% rename from app/routes/user_routes.py rename to api/routes/user_routes.py index 12b2828..cf9be52 100644 --- a/app/routes/user_routes.py +++ b/api/routes/user_routes.py @@ -6,12 +6,12 @@ import string import bcrypt import random -from app.util.db_dependency import get_db -from app.util.check_password_reqs import check_password_reqs -from app.schemas.auth_schemas import User -from app.schemas.user_schemas import * +from api.util.db_dependency import get_db +from api.util.check_password_reqs import check_password_reqs +from api.schemas.auth_schemas import User +from api.schemas.user_schemas import * from models import User as UserModel -from app.util.authentication import ( +from api.util.authentication import ( verify_password, get_current_user, ) diff --git a/app/schemas/auth_schemas.py b/api/schemas/auth_schemas.py similarity index 84% rename from app/schemas/auth_schemas.py rename to api/schemas/auth_schemas.py index 86e0ee8..48745ee 100644 --- a/app/schemas/auth_schemas.py +++ b/api/schemas/auth_schemas.py @@ -3,7 +3,6 @@ from pydantic import BaseModel class Token(BaseModel): access_token: str - refresh_token: str | None = None token_type: str diff --git a/app/schemas/links_schemas.py b/api/schemas/links_schemas.py similarity index 100% rename from app/schemas/links_schemas.py rename to api/schemas/links_schemas.py diff --git a/app/schemas/user_schemas.py b/api/schemas/user_schemas.py similarity index 100% rename from app/schemas/user_schemas.py rename to api/schemas/user_schemas.py diff --git a/api/static/js/api.js b/api/static/js/api.js new file mode 100644 index 0000000..243edf7 --- /dev/null +++ b/api/static/js/api.js @@ -0,0 +1,26 @@ +// Description: This file contains functions to access the API with JWT authentication. + +/** + * Accept an API endpoint, method, and body to send to the API. + * - If successful, return the response + * - If not, return false + * @param {*} endpoint API endpoint + * @param {*} method String (GET, POST, PUT, DELETE) + * @param {*} body Data to send to the API + * @returns response.json or false + */ +async function accessAPI(endpoint, method, body) { + let response = await fetch(`/api${endpoint}`, { + method: method, + body: body, + }); + + if (response.ok) { + let data = await response.json(); + data = await data; + return data; + + } + + return false; +} \ No newline at end of file diff --git a/app/templates/dashboard.html b/api/templates/dashboard.html similarity index 79% rename from app/templates/dashboard.html rename to api/templates/dashboard.html index fa7458a..25372bd 100644 --- a/app/templates/dashboard.html +++ b/api/templates/dashboard.html @@ -5,6 +5,7 @@