Uploading blog endpoint
This commit is contained in:
parent
770b17c4d1
commit
faa718c7d5
|
|
@ -1,3 +1,4 @@
|
||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
postgres-data
|
postgres-data
|
||||||
|
blogs
|
||||||
|
|
@ -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
|
||||||
24
dbHandler.py
24
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)
|
# 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:
|
||||||
debugPrint("Commit query executing...")
|
try:
|
||||||
dbCursor = dbConnection.cursor()
|
debugPrint("Commit query executing...")
|
||||||
dbCursor.execute(query)
|
dbCursor = dbConnection.cursor()
|
||||||
dbConnection.commit()
|
dbCursor.execute(query)
|
||||||
dbResults = dbCursor.fetchall()
|
dbConnection.commit()
|
||||||
dbCursor.close()
|
dbResults = dbCursor.fetchall()
|
||||||
return dbResults
|
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:
|
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
98
main.py
|
|
@ -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!"}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
@ -15,29 +16,43 @@ def checkAndRetrieveMetadata(fileData, metadataName: str):
|
||||||
else:
|
else:
|
||||||
debugPrint("Could not find " + metadataFullName + " metadata field of document!")
|
debugPrint("Could not find " + metadataFullName + " metadata field of document!")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def orgToHTML(filePath: str):
|
def getOrgTitle(blogID: int) -> 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")
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue