diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..390edc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data/lard.db 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 7cdd2dd..d50b0d4 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,14 +133,37 @@ async def createlink(data: dict): conn.commit() return f"{baseurl}/{short}" +@app.delete("/delete/{id}") +async def deletelink(id: int, username: str = Depends(verify_credentials)): + 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/index.html") as f: + 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, 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 @@ -126,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 ccc252b..672d742 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,4 +1,6 @@ fastapi uvicorn configparser -starlette \ No newline at end of file +starlette +jinja2 +bcrypt \ No newline at end of file diff --git a/app/templates/admin.html b/app/templates/admin.html new file mode 100644 index 0000000..2f14b36 --- /dev/null +++ b/app/templates/admin.html @@ -0,0 +1,196 @@ + + + + + + Lard: A Redirect Daemon + + + + + + +
+ +
+ +
+

LARD: A Redirect Daemon

+ + +
+ + + + + + + + + + + + + {% for entry in entries %} + + + + + + + + + {% endfor %} + + + + +
+
+
+ +
+ + + + + + + + + + + + + + + diff --git a/app/index.html b/app/templates/index.html similarity index 100% rename from app/index.html rename to app/templates/index.html 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