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 .venv
__pycache__ __pycache__
postgres-data 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) # 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: def _commitQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list:
try:
debugPrint("Commit query executing...") debugPrint("Commit query executing...")
dbCursor = dbConnection.cursor() dbCursor = dbConnection.cursor()
dbCursor.execute(query) dbCursor.execute(query)
@ -72,6 +73,9 @@ def _commitQuery(dbConnection: psycopg2.extensions.connection, query: sql.Compos
dbResults = dbCursor.fetchall() dbResults = dbCursor.fetchall()
dbCursor.close() dbCursor.close()
return dbResults return dbResults
except Exception as error:
errorPrint("Commit query failed! " + repr(error))
return None
def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list: def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composable) -> list:
try: try:
debugPrint("Exec query executing...") debugPrint("Exec query executing...")
@ -85,7 +89,7 @@ def _execQuery(dbConnection: psycopg2.extensions.connection, query: sql.Composab
return None return None
# Callable helper functions # 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 + "...") debugPrint("Attempting to insert new row (" + str(tableFormat) + ") VALUES (" + str(tableValues) + ") into table name " + tableName + "...")
sanitisedQuery = sql.SQL(""" sanitisedQuery = sql.SQL("""
INSERT INTO {table} ({format}) INSERT INTO {table} ({format})
@ -94,14 +98,14 @@ def insertRow(dbConnection: psycopg2.extensions.connection, tableName: str, tabl
""").format( """).format(
table=sql.Identifier(tableName), table=sql.Identifier(tableName),
format=sql.SQL(", ").join( format=sql.SQL(", ").join(
sql.Identifier(value) for value in tableFormat sql.Identifier(value.lower()) for value in tableFormat
), ),
values=sql.SQL(", ").join( values=sql.SQL(", ").join(
sql.Literal(value) for value in tableValues sql.Literal(value) for value in tableValues
) )
) )
debugPrint(sanitisedQuery.as_string(dbConnection)) 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: 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 + "...") 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 sys
import atexit import atexit
import signal import signal
@ -14,6 +15,19 @@ import dbHandler
import userHandler import userHandler
import securityHandler import securityHandler
import tokenHandler 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", dbConnection = dbHandler.connect("blorgdb",
"172.20.0.10", "172.20.0.10",
@ -52,12 +66,17 @@ def apiInit():
ID SERIAL PRIMARY KEY, ID SERIAL PRIMARY KEY,
AuthorID INTEGER, AuthorID INTEGER,
CategoryID INTEGER, CategoryID INTEGER,
DatePosted TIMESTAMP, DatePosted TIMESTAMP
""") """)
dbHandler.initTable(dbConnection, "Categories", """ dbHandler.initTable(dbConnection, "Categories", """
ID SERIAL PRIMARY KEY, ID SERIAL PRIMARY KEY,
Name VARCHAR(255) 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") # userHandler.createUser(dbConnection, "testuser", "Test", "User", "A test user", "TestCountry", "TestTheme", "TestColor", "testuser")
def apiCleanup(): def apiCleanup():
@ -100,12 +119,12 @@ def postapi(body: ApiBody):
print(body.password) print(body.password)
return body return body
class loginBody(BaseModel): class postuserLoginBody(BaseModel):
username: str username: str
password: str password: str
rememberMe: bool rememberMe: bool
@app.post("/api/login") @app.post("/api/user/login")
def postlogin(body: loginBody, request: Request): def postuserLogin(body: postuserLoginBody, request: Request):
try: try:
if userHandler.checkUserExistence(dbConnection, body.username): if userHandler.checkUserExistence(dbConnection, body.username):
userID = userHandler.getIDByUsername(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." "message": "Get public info failed! UserID provided does not exist."
} }
except Exception as error: except Exception as error:
msg = "Get public info failed! Unexpected server error. " + repr(error)
print(msg)
return { return {
"success": False, "success": False,
"username": None, "username": None,
"firstName": None, "firstName": None,
"lastName": None, "lastName": None,
"message": "Get public info failed! Unexpected server error. " + repr(error) "message": msg
} }
@app.get("/api/user/settings/account") @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." "message": "Get user settings failed! authToken provided is not valid."
} }
except Exception as error: except Exception as error:
msg = "Get user settings failed! Unexpected server error. " + repr(error)
print(msg)
return { return {
"success": False, "success": False,
"username": None, "username": None,
"firstName": None, "firstName": None,
"lastName": 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 # GET
# /api/user/ByAuthToken # /api/user/IDByAuthToken
# - userID # - userID
# Could be merged into a single endpoint which you can optionally query what you need from
# /api/user/publicInfo/{userID} # /api/user/publicInfo/{userID}
# - username # - username
# - firstname # - firstname
@ -201,19 +263,29 @@ def getuserSettingsAccount(authToken: Annotated[str | None, Header()] = None):
# - profile picture # - profile picture
# - location # - location
# - public email (For contact) # - public email (For contact)
# Could be merged into a single endpoint which you can optionally query what you need from
# /api/user/privateInfo/{userID} # /api/user/privateInfo/{userID}
# - private email (For authentication/login) # - private email (For authentication/login)
# /api/blog/title # Could be merged into a single endpoint which you can optionally query what you need from
# /api/blog/authorID # /api/blog/title/{blogID}
# /api/blog/categoryID # /api/blog/authorID/{blogID}
# /api/blog/pictureLocation # /api/blog/categoryID/{blogID}
# /api/blog/description # /api/blog/pictureURL/{blogID}
# /api/blog/datePosted # /api/blog/description/{blogID}
# /api/blog/datePosted/{blogID}
# POST # POST
# /api/user/changeInfo/{infotype} # /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") @app.get("/api")
def getapi(): def getapi():
return {"Hello": "API!"} return {"Hello": "API!"}

View File

@ -1,12 +1,13 @@
from orgparse import load from orgparse import loads
blogDir: str = "./blogs"
debug: bool = True debug: bool = True
def debugPrint(msg: str) -> None: def debugPrint(msg: str) -> None:
if debug: if debug:
print("(ORG HANDLER) PRINT: " + msg) print("(ORG HANDLER) PRINT: " + msg)
def checkAndRetrieveMetadata(fileData, metadataName: str): def checkAndRetrieveMetadata(fileData: str, metadataName: str):
line = fileData.readline() line = fileData.readline()
metadataFullName = "#+" + metadataName + ":" metadataFullName = "#+" + metadataName + ":"
if metadataFullName in line: if metadataFullName in line:
@ -16,28 +17,42 @@ def checkAndRetrieveMetadata(fileData, metadataName: str):
debugPrint("Could not find " + metadataFullName + " metadata field of document!") debugPrint("Could not find " + metadataFullName + " metadata field of document!")
return False return False
def getOrgTitle(blogID: int) -> str:
def orgToHTML(filePath: str):
try: try:
fileData = open(filePath, 'r') fileData = open(blogDir + "/" + str(blogID) + "/" + str(blogID) + ".org", 'r')
orgRoot = load(filePath) title = checkAndRetrieveMetadata(fileData, "TITLE")
if title:
return title
except Exception as error:
debugPrint("Error getting blog title! " + repr(error))
Title = checkAndRetrieveMetadata(fileData, "TITLE") def getOrgDescription(blogID: str) -> str:
if not Title: try:
raise Exception("A valid #+TITLE: field is required as the first line of the org page.") fileData = open(blogDir + "/" + str(blogID) + "/" + str(blogID) + ".org", 'r')
orgRoot = loads(fileData.read())
fileData.readline()
shortDescription = checkAndRetrieveMetadata(fileData, "DESCRIPTION") shortDescription = checkAndRetrieveMetadata(fileData, "DESCRIPTION")
if not shortDescription: 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 firstText = orgRoot[1].body
shortDescription = (firstText[:60] + "...") if len(firstText) > 60 else firstText 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:]: for node in orgRoot[1:]:
if node.heading: if node.heading:
headingLevel = str(node.level) headingLevel = str(node.level)
print("<h" + headingLevel + ">" + node.heading + "</h" + headingLevel + ">") parsedHTML += "<h" + headingLevel + ">" + node.heading + "</h" + headingLevel + ">" + "\n"
if node.body: if node.body:
print(node.body) parsedHTML += node.body + "\n"
return parsedHTML
except Exception as error: except Exception as error:
debugPrint("Error parsing org! " + repr(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) return dbHandler.checkFieldValueExistence(dbConnection, "authtokens", "token", authToken)
def createToken(dbConnection: psycopg2.extensions.connection, userID: int, rememberMe: bool, locationIP: str) -> str: 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) randToken = secrets.token_hex(1023)
dateCreated = datetime.datetime.now() dateCreated = datetime.datetime.now()
if rememberMe: if rememberMe:
dateExpiry = dateCreated + datetime.timedelta(days=30) dateExpiry = dateCreated + datetime.timedelta(days=30)
else: else:
dateExpiry = dateCreated + datetime.timedelta(days=1) 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, dbHandler.insertRow(dbConnection,
'authtokens', 'authtokens',
['token', 'ownerid', 'datecreated', 'dateexpiry', 'iplocationcreated'], ['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 return randToken