diff --git a/.gitignore b/.gitignore index 1b86f22..3f2acea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .venv __pycache__ -postgres-data \ No newline at end of file +postgres-data +blogs \ No newline at end of file diff --git a/blogHandler.py b/blogHandler.py new file mode 100644 index 0000000..ded565c --- /dev/null +++ b/blogHandler.py @@ -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 diff --git a/dbHandler.py b/dbHandler.py index faab479..7c894a4 100644 --- a/dbHandler.py +++ b/dbHandler.py @@ -65,13 +65,17 @@ 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: - debugPrint("Commit query executing...") - dbCursor = dbConnection.cursor() - dbCursor.execute(query) - dbConnection.commit() - dbResults = dbCursor.fetchall() - dbCursor.close() - return dbResults + try: + debugPrint("Commit query executing...") + dbCursor = dbConnection.cursor() + dbCursor.execute(query) + dbConnection.commit() + 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 + "...") diff --git a/main.py b/main.py index 99b2e37..4d22bf6 100644 --- a/main.py +++ b/main.py @@ -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!"} diff --git a/orgHandler.py b/orgHandler.py index 894b69b..051e502 100644 --- a/orgHandler.py +++ b/orgHandler.py @@ -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: @@ -15,29 +16,43 @@ def checkAndRetrieveMetadata(fileData, metadataName: str): else: 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("" + node.heading + "") + parsedHTML += "" + node.heading + "" + "\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") diff --git a/tokenHandler.py b/tokenHandler.py index 1594bf4..c827afa 100644 --- a/tokenHandler.py +++ b/tokenHandler.py @@ -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]) + ['token', 'ownerid', 'datecreated', 'dateexpiry', 'iplocationcreated'], + [randToken, userID, dateCreatedStr, dateCreatedStr, locationIP ]) return randToken