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

2
server/.gitignore vendored
View File

@@ -174,3 +174,5 @@ poetry.toml
pyrightconfig.json
# 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,
`bio` TEXT NOT NULL,
`github` VARCHAR(255) NOT NULL,
`auth0_sub` VARCHAR(255) NOT NULL,
`test_sub` VARCHAR(255) NOT NULL
`gitea` VARCHAR(255) NOT NULL
);
INSERT INTO `self` (
`name`,
`email`,
`bio`,
`github`,
`auth0_sub`,
`test_sub`
`gitea`
)
VALUES (
'Lucas Jensen',
'lucas.p.jensen10@gmail.com',
"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.",
'lucas@lucasjensen.me',
"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',
'google-oauth2|103593642272149633528',
'FZdDeArr7QuX8qVmbKD2ggdLvlJZKEjE@clients'
'https://gitea.lucasjensen.me/lucasjensen'
);
DROP TABLE IF EXISTS `projects`;
CREATE TABLE `projects` (
@@ -43,22 +40,22 @@ INSERT INTO `projects` (
VALUES (
'The Grapefruits Duo',
'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/',
TRUE
),
(
'Portfolio Backend',
'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.',
'https://github.com/ljensen505/portfolio-back',
'https://api.lucasjensen.me/',
'Portfolio Homepage',
'This very website!',
'https://gitea.lucasjensen.me/lucasjensen/LucasJensen',
'https://lucasjensen.me/',
TRUE
),
(
'Portfolio Frontend',
'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.',
'https://github.com/ljensen505/portfolio-front',
'https://lucasjensen.me/',
'Megan Johns Portfilo Website',
'A comprehensive portfolio for local Eugene musician and artist, Megan Johns.',
'https://gitea.lucasjensen.me/lucasjensen/MeganJohns',
'https://mj.lucasjensen.me/',
TRUE
),
(
@@ -74,18 +71,4 @@ VALUES (
NULL,
'https://efdl.lucasjensen.me/',
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:3000",
"https://localhost:3000",
"http://127.0.0.1:3000",
"https://lucasjensen.me/",
"https://lucasjensen.me",
"https://www.lucasjensen.me/",

View File

@@ -6,14 +6,11 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
import queries
from __version__ import __version__
from helpers import origins
from models import About, Project
from utils import VerifyToken
from models import About, Project, Lucas
load_dotenv()
app = FastAPI()
auth = VerifyToken()
app.add_middleware(
@@ -28,19 +25,9 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/", status_code=status.HTTP_200_OK)
async def root():
available_routes = [
"/",
"/about",
"/projects",
"/static/resume.pdf",
"/static/favicon.png",
]
return {
"welcome": "backend api for lucasjensen.me",
"version": __version__,
"routes": available_routes,
}
async def root() -> Lucas:
lucas = Lucas(about=queries.get_about(), projects=queries.get_projects())
return lucas
@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)
async def project(project_id: int) -> Project:
project = queries.get_project(project_id)
project = None
try:
project = queries.get_project(project_id)
except Exception as e:
print(f"err getting projects: {e}")
raise HTTPException(
@@ -86,48 +71,3 @@ async def project(project_id: int) -> Project:
detail=f"project with id {project_id} not found",
)
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 typing import Optional
class About(BaseModel):
@@ -6,12 +7,18 @@ class About(BaseModel):
email: str
bio: str
github: str
gitea: str
class Project(BaseModel):
id: int | None = None
id: Optional[int] = None
name: str
description: str
source: str | None = None
live: str | None = None
is_self_hosted: bool = False
source: Optional[str] = None
live: Optional[str] = None
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:
db = connect_db()
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
db.close()
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