From fea2b59549eded9f1f2f1680358b6e951b6ebdf3 Mon Sep 17 00:00:00 2001 From: James Bertelson Date: Fri, 20 Oct 2023 21:46:50 -0400 Subject: [PATCH 1/5] Prep for implementing admin functionality --- app/main.py | 10 +- app/requirements.txt | 3 +- app/{index.html => templates/admin.html} | 0 app/templates/index.html | 123 +++++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) rename app/{index.html => templates/admin.html} (100%) create mode 100644 app/templates/index.html diff --git a/app/main.py b/app/main.py index 7cdd2dd..430aeab 100644 --- a/app/main.py +++ b/app/main.py @@ -111,10 +111,18 @@ async def createlink(data: dict): @app.get("/") async def read_root(): - with open("/app/index.html") as f: + with open("/app/templates/index.html") as f: html = f.read() return HTMLResponse(html) +@app.get("/admin") +def admin(request: Request): + with sqlite3.connect('data/lard.db') as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM links") + entries = cursor.fetchall() + return templates.TemplateResponse("admin.html", {"request": request, "entries": entries}) + @app.get("/{path}") async def redirect(path = None): conn = sqlite3.connect('/data/lard.db') diff --git a/app/requirements.txt b/app/requirements.txt index ccc252b..2c54bf1 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,4 +1,5 @@ fastapi uvicorn configparser -starlette \ No newline at end of file +starlette +jinja2 \ No newline at end of file diff --git a/app/index.html b/app/templates/admin.html similarity index 100% rename from app/index.html rename to app/templates/admin.html diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..5408a82 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,123 @@ + + + + + + Lard: A Redirect Daemon + + + + + + +
+ +
+ +
+

LARD: A Redirect Daemon

+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+
+ +
+ + \ No newline at end of file From 6990e69280358bafb382f120ec6943cd4aec1c34 Mon Sep 17 00:00:00 2001 From: James Bertelson Date: Fri, 20 Oct 2023 21:50:24 -0400 Subject: [PATCH 2/5] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..390edc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data/lard.db From 1017de477ae5680537c1f556cae49aa51b842c23 Mon Sep 17 00:00:00 2001 From: James Bertelson Date: Fri, 20 Oct 2023 21:52:11 -0400 Subject: [PATCH 3/5] initial admin template --- app/templates/admin.html | 78 +++++++++++++--------------------------- 1 file changed, 25 insertions(+), 53 deletions(-) diff --git a/app/templates/admin.html b/app/templates/admin.html index 5408a82..16783da 100644 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -66,58 +66,30 @@ - - -
- -
- -
-

LARD: A Redirect Daemon

- -
- -
- - -
- -
- - -
- -
- -
- -
-
- -
-
- -
+

Admin Dashboard

+ + + + + + + + + + + {% for entry in entries %} + + + + + + + {% endfor %} + +
Short URLLong URLViewsActions
{{ entry.short }}{{ entry.long }}{{ entry.views }} + + +
+ \ No newline at end of file From 0b6deddde99ec7405c5c8aabd55f4b1b5efc3962 Mon Sep 17 00:00:00 2001 From: James Bertelson Date: Sat, 21 Oct 2023 00:18:13 -0400 Subject: [PATCH 4/5] Admin panel with list + delete functionality --- app/main.py | 55 +++++++++++--- app/requirements.txt | 3 +- app/templates/admin.html | 151 ++++++++++++++++++++++++++++++++------- data/config.ini | 3 +- 4 files changed, 177 insertions(+), 35 deletions(-) diff --git a/app/main.py b/app/main.py index 430aeab..532c57b 100644 --- a/app/main.py +++ b/app/main.py @@ -4,12 +4,17 @@ import re import sqlite3 import string +import hashlib +import bcrypt from datetime import datetime -from fastapi import Depends, FastAPI +from fastapi import Depends, FastAPI, Request, HTTPException, BackgroundTasks, status from fastapi.responses import HTMLResponse, JSONResponse from fastapi.security import HTTPBasic, HTTPBasicCredentials from starlette.responses import RedirectResponse +from fastapi.templating import Jinja2Templates + +security = HTTPBasic() def get_config(): @@ -21,6 +26,10 @@ def get_password(): config = get_config() return config.get('Auth', 'password') +def get_admin(): + config = get_config() + return config.get('Auth', 'admin') + def get_baseurl(): config = get_config() return config["General"]["baseurl"] @@ -54,7 +63,22 @@ def generate_shorturl(): return generate_shorturl() return short -def update_link_on_view(conn, short_url): +def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)): + admin_data = get_admin().split(":") + correct_username = admin_data[0] + correct_password_hash = admin_data[1].encode('utf-8') + + if credentials.username == correct_username and bcrypt.checkpw(credentials.password.encode('utf-8'), correct_password_hash): + return credentials.username + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Basic"}, + ) +def update_link_on_view(short_url): + conn = sqlite3.connect('/data/lard.db') + # Connect to the database cur = conn.cursor() @@ -64,7 +88,7 @@ def update_link_on_view(conn, short_url): # Update the views and last fields cur.execute("UPDATE links SET views = views + 1, last = ? WHERE short = ?", (timestamp, short_url)) conn.commit() - + conn.close() # Create schema if not exists if not database_exists('/data/lard.db'): @@ -87,7 +111,7 @@ def update_link_on_view(conn, short_url): print("Error creating database:", e) app = FastAPI() -security = HTTPBasic() + @app.post("/create") @@ -109,22 +133,37 @@ async def createlink(data: dict): conn.commit() return f"{baseurl}/{short}" +@app.delete("/delete/{id}") +async def deletelink(id: int): + try: + with sqlite3.connect('/data/lard.db') as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM links WHERE id=?", (id,)) + conn.commit() + return {"message": "Entry deleted successfully"} + except Exception as e: + print(e) + return {"message": f"Error: {str(e)}"} + + @app.get("/") async def read_root(): with open("/app/templates/index.html") as f: html = f.read() return HTMLResponse(html) +templates = Jinja2Templates(directory="/app/templates") + @app.get("/admin") -def admin(request: Request): - with sqlite3.connect('data/lard.db') as conn: +def admin(request: Request, username: str = Depends(verify_credentials)): + with sqlite3.connect('/data/lard.db') as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM links") entries = cursor.fetchall() return templates.TemplateResponse("admin.html", {"request": request, "entries": entries}) @app.get("/{path}") -async def redirect(path = None): +async def redirect(path = None, background_tasks: BackgroundTasks = None): conn = sqlite3.connect('/data/lard.db') # Create a cursor @@ -134,7 +173,7 @@ async def redirect(path = None): rows = cursor.fetchall() if len(rows) == 1: - update_link_on_view(conn, path) + background_tasks.add_task(update_link_on_view, path) conn.close() return RedirectResponse(url=rows[0][0], status_code=301) else: diff --git a/app/requirements.txt b/app/requirements.txt index 2c54bf1..672d742 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -2,4 +2,5 @@ fastapi uvicorn configparser starlette -jinja2 \ No newline at end of file +jinja2 +bcrypt \ No newline at end of file diff --git a/app/templates/admin.html b/app/templates/admin.html index 16783da..2f14b36 100644 --- a/app/templates/admin.html +++ b/app/templates/admin.html @@ -63,33 +63,134 @@ text-decoration: none; font-style: italic; } + .modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.6); +} + +.modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 20px; + background-color: #ffffff; + width: 80%; + max-width: 500px; +} + +.close-btn { + cursor: pointer; + float: right; + font-size: 28px; +} +#links { + border-collapse: separate; + border-spacing: 0; + width: 100%; + border-radius: 10px; + overflow: hidden; /* For rounded corners */ +} + +#links th, #links td { + border: 1px solid #e0e0e0; /* Adjust this value to change the border color */ + padding: 10px; /* Adjust this value to change the padding inside the cells */ + text-align: left; +} + +#links th { + background-color: #f5f5f5; /* Adjust this value to change the header background color */ +} + +#links tbody tr:hover { + background-color: #f0f0f0; /* Adjust this value to change the row hover background color */ +} -

Admin Dashboard

- - - - - - - - - - - {% for entry in entries %} - - - - - - - {% endfor %} - -
Short URLLong URLViewsActions
{{ entry.short }}{{ entry.long }}{{ entry.views }} - - -
+ + +
+ +
+ +
+

LARD: A Redirect Daemon

+ + +
+ + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + {% endfor %} + + + + +
+
+
+ +
+ + + + + + + + + + + + + - \ No newline at end of file diff --git a/data/config.ini b/data/config.ini index 8c5fcb2..cd78383 100644 --- a/data/config.ini +++ b/data/config.ini @@ -3,4 +3,5 @@ baseurl = https://l.yourdomain.com length = 5 [Auth] -password = lard +password = lard +admin = admin:$2y$10$TGVz8YgPBXggJAf.BjOjHeMls59VXI7g7bGLLX9zF4uvHJcM8nKjG \ No newline at end of file From df68e7c5169b329125f7b60edd8f57ac5e6c4d60 Mon Sep 17 00:00:00 2001 From: James Bertelson Date: Sat, 21 Oct 2023 00:26:39 -0400 Subject: [PATCH 5/5] Readme and security updates --- README.md | 16 ++++++++++++++++ app/main.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cee2fa4..a43ee9b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ The configuration for the application is stored in the file `./data/config.ini`. [Auth] password = lard + #bcrypted admin:password + #To generate a username:password string, see: https://hostingcanada.org/htpasswd-generator/ or run `htpasswd -nBC 10 admin` + admin = admin:$2y$10$TGVz8YgPBXggJAf.BjOjHeMls59VXI7g7bGLLX9zF4uvHJcM8nKjG ``` @@ -57,6 +60,10 @@ The following sections and options are available: This endpoint returns a simple HTML page with a form. +### `GET /admin` + +This password-protected HTML endpoint returns a list of links within the system with delete capability. + ### `POST /create` This endpoint allows you to create a new redirect. The following parameters are supported: @@ -64,4 +71,13 @@ This endpoint allows you to create a new redirect. The following parameters are - `url`: The URL to redirect to. - `key`: The password +### `DELETE /delete/{id}` + +This endpoint deletes an existing link: + +{id} is the database id of the link to be deleted. + +Requires same auth as admin + + HTML/CSS layout thanks to Smart Developers. diff --git a/app/main.py b/app/main.py index 532c57b..d50b0d4 100644 --- a/app/main.py +++ b/app/main.py @@ -134,7 +134,7 @@ async def createlink(data: dict): return f"{baseurl}/{short}" @app.delete("/delete/{id}") -async def deletelink(id: int): +async def deletelink(id: int, username: str = Depends(verify_credentials)): try: with sqlite3.connect('/data/lard.db') as conn: cursor = conn.cursor()