Uploading blog endpoint

This commit is contained in:
Curt Spark 2024-04-30 18:24:20 +01:00
parent 770b17c4d1
commit faa718c7d5
6 changed files with 186 additions and 41 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.venv
__pycache__
postgres-data
blogs

51
blogHandler.py Normal file
View File

@ -0,0 +1,51 @@
import os
import datetime
import psycopg2
import orgHandler
import dbHandler
blogDir: str = "./blogs"
debug: bool = True
def debugPrint(msg: str) -> None:
if debug:
print("(BLOG HANDLER) PRINT: " + msg)
# Returns new Blog ID
def createBlogEntry(dbConnection: psycopg2.extensions.connection, userID: int, categoryID: int) -> int:
datePosted = datetime.datetime.now()
datePostedStr = datePosted.strftime("%G-%m-%d %X")
debugPrint("Now creating new user with following attributes : userID = " + str(userID) + ", categoryID = " + str(categoryID) + ", datePosted = " + datePostedStr)
newRow = dbHandler.insertRow(dbConnection,
'blogs',
['authorID', 'categoryID', 'datePosted' ],
[userID, categoryID, datePostedStr])
return newRow[0]
def uploadBlog(dbConnection: psycopg2.extensions.connection, userID: int, orgRawIn: str):
try:
orgParsedOut = orgHandler.orgToHTML(orgRawIn)
if orgParsedOut:
blogID = createBlogEntry(dbConnection, userID, 1)
newBlogDir = blogDir + "/" + str(blogID)
debugPrint("Attempting to create new blog directory " + newBlogDir + "...")
os.mkdir(newBlogDir)
debugPrint("Attempting to write new blog file " + newBlogDir + ".org...")
orgFileData = open(newBlogDir + "/" + str(blogID) + ".org", 'w')
orgFileData.write(orgRawIn)
orgFileData.close()
debugPrint("Attempting to write new blog file " + blogDir + "/" + str(blogID) + "/" + str(blogID) + ".html...")
HTMLFileData = open(newBlogDir + "/" + str(blogID) + ".html", 'w')
HTMLFileData.write(orgParsedOut)
HTMLFileData.close()
return blogID
else:
return False
except Exception as error:
debugPrint("Create blog failed! " + repr(error))
return False

View File

@ -65,6 +65,7 @@ def initTable(dbConnection: psycopg2.extensions.connection, tableName: str, tabl
# These base functions should not be called directly as they perform no string query sanitisation (Therefore vulnerable to SQL injection attacks)
def _commitQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list:
try:
debugPrint("Commit query executing...")
dbCursor = dbConnection.cursor()
dbCursor.execute(query)
@ -72,6 +73,9 @@ def _commitQuery(dbConnection: psycopg2.extensions.connection, query: sql.Compos
dbResults = dbCursor.fetchall()
dbCursor.close()
return dbResults
except Exception as error:
errorPrint("Commit query failed! " + repr(error))
return None
def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list:
try:
debugPrint("Exec query executing...")
@ -85,7 +89,7 @@ def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composab
return None
# Callable helper functions
def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tableFormat: list[str], tableValues: list):
def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tableFormat: list[str], tableValues: list) -> list:
debugPrint("Attempting to insert new row (" + str(tableFormat) + ") VALUES (" + str(tableValues) + ") into table name " + tableName + "...")
sanitisedQuery = sql.SQL("""
INSERT INTO {table} ({format})
@ -94,14 +98,14 @@ def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tabl
""").format(
table=sql.Identifier(tableName),
format=sql.SQL(", ").join(
sql.Identifier(value) for value in tableFormat
sql.Identifier(value.lower()) for value in tableFormat
),
values=sql.SQL(", ").join(
sql.Literal(value) for value in tableValues
)
)
debugPrint(sanitisedQuery.as_string(dbConnection))
_commitQuery(dbConnection, sanitisedQuery)
return _commitQuery(dbConnection, sanitisedQuery)[0]
def getFieldValueByID(dbConnection: psycopg2.extensions.connection, tableName: str, RowID: int, tableField: str) -> str:
debugPrint("Attempting to get values of field name " + tableField + " in ID row " + str(RowID) + " in table name " + tableName + "...")

98
main.py
View File

@ -1,3 +1,4 @@
import os
import sys
import atexit
import signal
@ -14,6 +15,19 @@ import dbHandler
import userHandler
import securityHandler
import tokenHandler
import blogHandler
blogDir = "./blogs"
debug: bool = True
def debugPrint(msg: str) -> None:
if debug:
print("(MAIN) PRINT: " + msg)
def debugInitPrint(msg: str) -> None:
if debug:
print("(MAIN INIT) PRINT: " + msg)
dbConnection = dbHandler.connect("blorgdb",
"172.20.0.10",
@ -52,12 +66,17 @@ def apiInit():
ID SERIAL PRIMARY KEY,
AuthorID INTEGER,
CategoryID INTEGER,
DatePosted TIMESTAMP,
DatePosted TIMESTAMP
""")
dbHandler.initTable(dbConnection, "Categories", """
ID SERIAL PRIMARY KEY,
Name VARCHAR(255)
""")
debugInitPrint("Creating blog data directory at " + blogDir + "...")
if not os.path.exists(blogDir):
os.mkdir(blogDir)
else:
debugInitPrint("Blog data directory already exists! Skipping...")
# userHandler.createUser(dbConnection, "testuser", "Test", "User", "A test user", "TestCountry", "TestTheme", "TestColor", "testuser")
def apiCleanup():
@ -100,12 +119,12 @@ def postapi(body: ApiBody):
print(body.password)
return body
class loginBody(BaseModel):
class postuserLoginBody(BaseModel):
username: str
password: str
rememberMe: bool
@app.post("/api/login")
def postlogin(body: loginBody, request: Request):
@app.post("/api/user/login")
def postuserLogin(body: postuserLoginBody, request: Request):
try:
if userHandler.checkUserExistence(dbConnection, body.username):
userID = userHandler.getIDByUsername(dbConnection, body.username)
@ -153,12 +172,14 @@ def getuserPublicInfo(userID: int):
"message": "Get public info failed! UserID provided does not exist."
}
except Exception as error:
msg = "Get public info failed! Unexpected server error. " + repr(error)
print(msg)
return {
"success": False,
"username": None,
"firstName": None,
"lastName": None,
"message": "Get public info failed! Unexpected server error. " + repr(error)
"message": msg
}
@app.get("/api/user/settings/account")
@ -182,18 +203,59 @@ def getuserSettingsAccount(authToken: Annotated[str | None, Header()] = None):
"message": "Get user settings failed! authToken provided is not valid."
}
except Exception as error:
msg = "Get user settings failed! Unexpected server error. " + repr(error)
print(msg)
return {
"success": False,
"username": None,
"firstName": None,
"lastName": None,
"message": "Get user settings failed! Unexpected server error. " + repr(error)
"message": msg
}
class postblogCreateBody(BaseModel):
authToken: str
orgContents: str
@app.post("/api/blog/create")
def postblogCreate(body: postblogCreateBody):
try:
if tokenHandler.validateTokenExistence(dbConnection, body.authToken):
userID = userHandler.getIDByAuthToken(dbConnection, body.authToken)
newBlog = blogHandler.uploadBlog(dbConnection, userID, body.orgContents)
if newBlog:
return {
"success": True,
"blogID": newBlog,
"message": "Create blog succeeded!"
}
else:
return {
"success": False,
"blogID": newBlog,
"message": "Create blog failed! Unknown error parsing org data."
}
else:
return {
"success": False,
"blogID": None,
"message": "Create blog failed! authToken provided is not valid."
}
except Exception as error:
msg = "Create blog failed! Unexpected server error. " + repr(error)
print(msg)
return {
"success": False,
"blogID": None,
"message": msg
}
# GET
# /api/user/ByAuthToken
# /api/user/IDByAuthToken
# - userID
# Could be merged into a single endpoint which you can optionally query what you need from
# /api/user/publicInfo/{userID}
# - username
# - firstname
@ -201,19 +263,29 @@ def getuserSettingsAccount(authToken: Annotated[str | None, Header()] = None):
# - profile picture
# - location
# - public email (For contact)
# Could be merged into a single endpoint which you can optionally query what you need from
# /api/user/privateInfo/{userID}
# - private email (For authentication/login)
# /api/blog/title
# /api/blog/authorID
# /api/blog/categoryID
# /api/blog/pictureLocation
# /api/blog/description
# /api/blog/datePosted
# Could be merged into a single endpoint which you can optionally query what you need from
# /api/blog/title/{blogID}
# /api/blog/authorID/{blogID}
# /api/blog/categoryID/{blogID}
# /api/blog/pictureURL/{blogID}
# /api/blog/description/{blogID}
# /api/blog/datePosted/{blogID}
# POST
# /api/user/changeInfo/{infotype}
# TODO: Need to overhaul this to accept an input file which we can read in chunks to ensure a too big file isn't sent, frontend will convert textbox/form data appropriately
# https://github.com/steinnes/content-size-limit-asgi
# https://github.com/tiangolo/fastapi/issues/362
# /api/blog/create
# /api/blog/edit
@app.get("/api")
def getapi():
return {"Hello": "API!"}

View File

@ -1,12 +1,13 @@
from orgparse import load
from orgparse import loads
blogDir: str = "./blogs"
debug: bool = True
def debugPrint(msg: str) -> None:
if debug:
print("(ORG HANDLER) PRINT: " + msg)
def checkAndRetrieveMetadata(fileData, metadataName: str):
def checkAndRetrieveMetadata(fileData: str, metadataName: str):
line = fileData.readline()
metadataFullName = "#+" + metadataName + ":"
if metadataFullName in line:
@ -16,28 +17,42 @@ def checkAndRetrieveMetadata(fileData, metadataName: str):
debugPrint("Could not find " + metadataFullName + " metadata field of document!")
return False
def orgToHTML(filePath: str):
def getOrgTitle(blogID: int) -> str:
try:
fileData = open(filePath, 'r')
orgRoot = load(filePath)
fileData = open(blogDir + "/" + str(blogID) + "/" + str(blogID) + ".org", 'r')
title = checkAndRetrieveMetadata(fileData, "TITLE")
if title:
return title
except Exception as error:
debugPrint("Error getting blog title! " + repr(error))
Title = checkAndRetrieveMetadata(fileData, "TITLE")
if not Title:
raise Exception("A valid #+TITLE: field is required as the first line of the org page.")
def getOrgDescription(blogID: str) -> str:
try:
fileData = open(blogDir + "/" + str(blogID) + "/" + str(blogID) + ".org", 'r')
orgRoot = loads(fileData.read())
fileData.readline()
shortDescription = checkAndRetrieveMetadata(fileData, "DESCRIPTION")
if not shortDescription:
debugPrint("No valid description found, will generate a placeholder from the blog itself...")
debugPrint("No valid description found, will generate a placeholder from the text itself...")
firstText = orgRoot[1].body
shortDescription = (firstText[:60] + "...") if len(firstText) > 60 else firstText
return shortDescription
except Exception as error:
debugPrint("Error getting org title! " + repr(error))
def orgToHTML(orgData: str) -> str:
try:
orgRoot = loads(orgData)
parsedHTML = ""
for node in orgRoot[1:]:
if node.heading:
headingLevel = str(node.level)
print("<h" + headingLevel + ">" + node.heading + "</h" + headingLevel + ">")
parsedHTML += "<h" + headingLevel + ">" + node.heading + "</h" + headingLevel + ">" + "\n"
if node.body:
print(node.body)
parsedHTML += node.body + "\n"
return parsedHTML
except Exception as error:
debugPrint("Error parsing org! " + repr(error))
orgToHTML("./test.org")

View File

@ -15,15 +15,17 @@ def validateTokenExistence(dbConnection: psycopg2.extensions.connection, authTok
return dbHandler.checkFieldValueExistence(dbConnection, "authtokens", "token", authToken)
def createToken(dbConnection: psycopg2.extensions.connection, userID: int, rememberMe: bool, locationIP: str) -> str:
debugPrint("Now initialising new token with following attributes : userID = " + str(userID) + ", rememberMe = " + str(rememberMe) + ", locationIP = " + locationIP + "...")
randToken = secrets.token_hex(1023)
dateCreated = datetime.datetime.now()
if rememberMe:
dateExpiry = dateCreated + datetime.timedelta(days=30)
else:
dateExpiry = dateCreated + datetime.timedelta(days=1)
dateExpiryStr = dateExpiry.strftime("%G-%m-%d %X")
dateCreatedStr = dateCreated.strftime("%G-%m-%d %X")
debugPrint("Now initialising new token with following attributes : ownerID = " + str(userID) + ", rememberMe = " + str(rememberMe) + ", locationIP = " + locationIP + "...")
dbHandler.insertRow(dbConnection,
'authtokens',
['token', 'ownerid', 'datecreated', 'dateexpiry', 'iplocationcreated'],
[randToken, userID, dateCreated.strftime("%G-%m-%d %X"), dateExpiry.strftime("%G-%m-%d %X"), locationIP])
[randToken, userID, dateCreatedStr, dateCreatedStr, locationIP ])
return randToken