poetry migration and general server cleanup

This commit is contained in:
Lucas Jensen
2024-06-29 11:41:32 -07:00
parent c2fa4b12ea
commit 53850749b8
11 changed files with 1369 additions and 220 deletions

4
server/.gitignore vendored
View File

@@ -173,4 +173,6 @@ poetry.toml
# LSP config files # LSP config files
pyrightconfig.json pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python # End of https://www.toptal.com/developers/gitignore/api/python
.vscode/

View File

@@ -1 +0,0 @@
__version__ = "0.1.22"

View File

@@ -5,24 +5,21 @@ CREATE TABLE `self` (
`email` VARCHAR(255) NOT NULL, `email` VARCHAR(255) NOT NULL,
`bio` TEXT NOT NULL, `bio` TEXT NOT NULL,
`github` VARCHAR(255) NOT NULL, `github` VARCHAR(255) NOT NULL,
`auth0_sub` VARCHAR(255) NOT NULL, `gitea` VARCHAR(255) NOT NULL
`test_sub` VARCHAR(255) NOT NULL
); );
INSERT INTO `self` ( INSERT INTO `self` (
`name`, `name`,
`email`, `email`,
`bio`, `bio`,
`github`, `github`,
`auth0_sub`, `gitea`
`test_sub`
) )
VALUES ( VALUES (
'Lucas Jensen', 'Lucas Jensen',
'lucas.p.jensen10@gmail.com', 'lucas@lucasjensen.me',
"I am a recent graduate from Oregon State University with a Bachelor's degree in Computer Science, driven by a passion for solving complex problems through technology. During my academic journey, I honed my skills and practical knowledge, setting a strong foundation for my career. My enthusiasm led me to a Software Engineering internship at Cvent, where I focused on Service Level Indicators (SLIs) and TypeScript. This experience allowed me to dive deep into the intricacies of backend development, gaining hands-on expertise in Python, FastAPI, Flask, Bash scripting, Linux, Nginx, and Systemd.\nMy commitment to delivering robust solutions is reflected in my proficiency in writing unit tests, ensuring the reliability and stability of the software I develop. I thrive in collaborative environments and have successfully contributed to team projects, understanding the importance of effective communication and cooperation. As I embark on my professional journey, I am excited to leverage my diverse skill set to tackle new challenges and make meaningful contributions to the field of computer science. Explore my portfolio to witness the intersection of my academic background and practical experiences that shape my identity as a dedicated and skilled computer scientist.", "I'm a software engineer working for the University of Oregon and an alum from Oregon State University with a B.S. in Computer Science. I'm passionate about open source software and self hosting anything and everything. Most of my projects, including this gitea instance, are hosted either on a Raspberry Pi or an Ubuntu server with Linode.\nMuch of what you see here is a WIP and will likely remain so for the foreseeable future. All the projects listed here are passion projects which are tended to in my spare time.",
'https://github.com/ljensen505', 'https://github.com/ljensen505',
'google-oauth2|103593642272149633528', 'https://gitea.lucasjensen.me/lucasjensen'
'FZdDeArr7QuX8qVmbKD2ggdLvlJZKEjE@clients'
); );
DROP TABLE IF EXISTS `projects`; DROP TABLE IF EXISTS `projects`;
CREATE TABLE `projects` ( CREATE TABLE `projects` (
@@ -43,22 +40,22 @@ INSERT INTO `projects` (
VALUES ( VALUES (
'The Grapefruits Duo', 'The Grapefruits Duo',
'An artist website for a local chamber music duo. Built with MySQL, FastAPI, and React with TypeScript.', 'An artist website for a local chamber music duo. Built with MySQL, FastAPI, and React with TypeScript.',
'https://github.com/ljensen505/TheGrapefruitsDuo', 'https://gitea.lucasjensen.me/lucasjensen/TheGrapefruitsDuo',
'https://thegrapefruitsduo.com/', 'https://thegrapefruitsduo.com/',
TRUE TRUE
), ),
( (
'Portfolio Backend', 'Portfolio Homepage',
'A RESTful API for my portfolio website. Consumed by the portfolio frontend. Built with FastAPI and MySQL. Hosted on a Raspberry Pi in my living room.', 'This very website!',
'https://github.com/ljensen505/portfolio-back', 'https://gitea.lucasjensen.me/lucasjensen/LucasJensen',
'https://api.lucasjensen.me/', 'https://lucasjensen.me/',
TRUE TRUE
), ),
( (
'Portfolio Frontend', 'Megan Johns Portfilo Website',
'The frontend for my portfolio website (this very site!). Consumes the portfolio backend. Built with React and Typescript. Hosted on a Raspberry Pi in my living room.', 'A comprehensive portfolio for local Eugene musician and artist, Megan Johns.',
'https://github.com/ljensen505/portfolio-front', 'https://gitea.lucasjensen.me/lucasjensen/MeganJohns',
'https://lucasjensen.me/', 'https://mj.lucasjensen.me/',
TRUE TRUE
), ),
( (
@@ -74,18 +71,4 @@ VALUES (
NULL, NULL,
'https://efdl.lucasjensen.me/', 'https://efdl.lucasjensen.me/',
TRUE TRUE
),
(
'Chess API',
'A RESTful API for playing chess online. Consumed by the Chess GUI.',
'https://github.com/ljensen505/chess-back',
'https://api.chess.v2.lucasjensen.me/',
TRUE
),
(
'Chess',
'A webapp for playing chess online against a friend. Consumes the Chess API.',
'https://github.com/ljensen505/chess-front',
'https://chess.lucasjensen.me/',
FALSE
) )

View File

@@ -6,6 +6,7 @@ origins = [
"http://localhost", "http://localhost",
"http://localhost:3000", "http://localhost:3000",
"https://localhost:3000", "https://localhost:3000",
"http://127.0.0.1:3000",
"https://lucasjensen.me/", "https://lucasjensen.me/",
"https://lucasjensen.me", "https://lucasjensen.me",
"https://www.lucasjensen.me/", "https://www.lucasjensen.me/",

View File

@@ -6,14 +6,11 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
import queries import queries
from __version__ import __version__
from helpers import origins from helpers import origins
from models import About, Project from models import About, Project, Lucas
from utils import VerifyToken
load_dotenv() load_dotenv()
app = FastAPI() app = FastAPI()
auth = VerifyToken()
app.add_middleware( app.add_middleware(
@@ -28,19 +25,9 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", status_code=status.HTTP_200_OK) @app.get("/", status_code=status.HTTP_200_OK)
async def root(): async def root() -> Lucas:
available_routes = [ lucas = Lucas(about=queries.get_about(), projects=queries.get_projects())
"/", return lucas
"/about",
"/projects",
"/static/resume.pdf",
"/static/favicon.png",
]
return {
"welcome": "backend api for lucasjensen.me",
"version": __version__,
"routes": available_routes,
}
@app.get("/about", status_code=status.HTTP_200_OK) @app.get("/about", status_code=status.HTTP_200_OK)
@@ -69,11 +56,9 @@ async def projects() -> list[Project]:
@app.get("/projects/{project_id}", status_code=status.HTTP_200_OK) @app.get("/projects/{project_id}", status_code=status.HTTP_200_OK)
async def project(project_id: int) -> Project: async def project(project_id: int) -> Project:
project = queries.get_project(project_id) project = None
try: try:
project = queries.get_project(project_id) project = queries.get_project(project_id)
except Exception as e: except Exception as e:
print(f"err getting projects: {e}") print(f"err getting projects: {e}")
raise HTTPException( raise HTTPException(
@@ -86,48 +71,3 @@ async def project(project_id: int) -> Project:
detail=f"project with id {project_id} not found", detail=f"project with id {project_id} not found",
) )
return project return project
@app.post("/projects", status_code=status.HTTP_201_CREATED)
async def post_project(project: Project, auth_result=Security(auth.verify)) -> Project:
user_sub, test_sub = queries.get_subs().values()
jwt_sub = auth_result.get("sub")
if jwt_sub not in [user_sub, test_sub]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="unauthorized",
)
try:
return queries.create_project(project)
except Exception as e:
print(f"err creating project: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"database error: {e}",
)
@app.delete("/projects/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_project(project_id: int, auth_result=Security(auth.verify)):
user_sub, test_sub = queries.get_subs().values()
jwt_sub = auth_result.get("sub")
if jwt_sub not in [user_sub, test_sub]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="unauthorized",
)
project = queries.get_project(project_id)
if project is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"project with id {project_id} not found",
)
try:
return queries.delete_project(project_id)
except Exception as e:
print(f"err deleting project: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"database error: {e}",
)

View File

@@ -1,4 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional
class About(BaseModel): class About(BaseModel):
@@ -6,12 +7,18 @@ class About(BaseModel):
email: str email: str
bio: str bio: str
github: str github: str
gitea: str
class Project(BaseModel): class Project(BaseModel):
id: int | None = None id: Optional[int] = None
name: str name: str
description: str description: str
source: str | None = None source: Optional[str] = None
live: str | None = None live: Optional[str] = None
is_self_hosted: bool = False is_self_hosted: Optional[bool] = False
class Lucas(BaseModel):
projects: list[Project]
about: About

1312
server/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
server/pyproject.toml Normal file
View File

@@ -0,0 +1,22 @@
[tool.poetry]
name = "lucasjensen-fastapi"
version = "0.1.0"
description = "A RESTful API for lucasjensen.me"
authors = ["Lucas Jensen <lucas@lucasjensen.me>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.10"
black = "^24.4.2"
pytest = "^8.2.2"
fastapi = "^0.111.0"
mysql-connector-python = "^8.4.0"
pyjwt = "^2.8.0"
pydantic = "^2.7.4"
pydantic-settings = "^2.3.4"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -52,7 +52,7 @@ def delete_project(project_id: int) -> None:
def get_about() -> About: def get_about() -> About:
db = connect_db() db = connect_db()
cursor = db.cursor(dictionary=True) cursor = db.cursor(dictionary=True)
cursor.execute("SELECT name, email, bio, github FROM self") cursor.execute("SELECT name, email, bio, github, gitea FROM self")
data = {key: val for key, val in cursor.fetchone().items()} # type: ignore data = {key: val for key, val in cursor.fetchone().items()} # type: ignore
db.close() db.close()
return About(**data) return About(**data)

View File

@@ -1,47 +0,0 @@
annotated-types==0.6.0
anyio==4.2.0
black==23.12.1
certifi==2023.11.17
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
cryptography==41.0.7
dotted-notation==0.11.0
fastapi==0.108.0
h11==0.14.0
httpcore==1.0.2
httptools==0.6.1
httpx==0.26.0
idna==3.6
iniconfig==2.0.0
markdown-it-py==3.0.0
mdurl==0.1.2
mypy-extensions==1.0.0
mysql-connector-python==8.2.0
packaging==23.2
pathspec==0.12.1
platformdirs==4.1.0
pluggy==1.3.0
protobuf==4.21.12
pycparser==2.21
pydantic==2.5.3
pydantic-settings==2.1.0
pydantic_core==2.14.6
Pygments==2.17.2
PyJWT==2.8.0
pyparsing==3.1.1
pytest==7.4.4
python-dotenv==1.0.0
PyYAML==6.0.1
requests==2.31.0
rich==13.7.0
rich-click==1.7.2
sniffio==1.3.0
starlette==0.32.0.post1
tomlkit==0.12.3
typing_extensions==4.9.0
urllib3==2.1.0
uvicorn==0.25.0
uvloop==0.19.0
watchfiles==0.21.0
websockets==12.0

View File

@@ -1,70 +0,0 @@
from typing import Optional
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, SecurityScopes
from config import get_settings
class UnauthorizedException(HTTPException):
def __init__(self, detail: str, **kwargs):
"""Returns HTTP 403"""
super().__init__(status.HTTP_403_FORBIDDEN, detail=detail)
class UnauthenticatedException(HTTPException):
def __init__(self):
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Requires authentication",
)
class UnauthenticatedException(HTTPException):
def __init__(self):
super().__init__(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Requires authentication"
)
class VerifyToken:
"""Does all the token verification using PyJWT"""
def __init__(self):
self.config = get_settings()
# This gets the JWKS from a given URL and does processing so you can
# use any of the keys available
jwks_url = f"https://{self.config.auth0_domain}/.well-known/jwks.json"
self.jwks_client = jwt.PyJWKClient(jwks_url)
async def verify(
self,
security_scopes: SecurityScopes,
token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer()),
):
if token is None:
raise UnauthenticatedException
# This gets the 'kid' from the passed token
try:
signing_key = self.jwks_client.get_signing_key_from_jwt(
token.credentials
).key
except jwt.exceptions.PyJWKClientError as error:
raise UnauthorizedException(str(error))
except jwt.exceptions.DecodeError as error:
raise UnauthorizedException(str(error))
try:
payload = jwt.decode(
token.credentials,
signing_key,
algorithms=self.config.auth0_algorithms, # type: ignore
audience=self.config.auth0_api_audience,
issuer=self.config.auth0_issuer,
)
except Exception as error:
raise UnauthorizedException(str(error))
return payload