Skip to content

Commit

Permalink
Merge pull request #2 from diffra/admin
Browse files Browse the repository at this point in the history
Admin
  • Loading branch information
diffra authored Oct 21, 2023
2 parents d3c3caa + df68e7c commit 8e3b371
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data/lard.db
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -57,11 +60,24 @@ 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:

- `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.
61 changes: 54 additions & 7 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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"]
Expand Down Expand Up @@ -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()

Expand All @@ -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'):
Expand All @@ -87,7 +111,7 @@ def update_link_on_view(conn, short_url):
print("Error creating database:", e)

app = FastAPI()
security = HTTPBasic()



@app.post("/create")
Expand All @@ -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
Expand All @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion app/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
fastapi
uvicorn
configparser
starlette
starlette
jinja2
bcrypt
196 changes: 196 additions & 0 deletions app/templates/admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lard: A Redirect Daemon</title>
<!-- CUSTOM CSS -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400&family=Roboto:wght@300;400;500;700&display=swap');
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,body{
background: #045D5D;
font-family: 'Roboto', sans-serif;
}
.container{
max-width: 600px;
margin: 120px auto;

}
.form-card{
background: #fff;
padding:10px 20px 30px 20px;
box-shadow: 2px 3px 11px 3px #00000069;
}
.form-card .text-center{
text-align: center;
}
.form-card .text-center h1{
margin:20px 0;
}
.input-group{
margin: 30px 0;
}
.input-group label{
padding: 10px 5px;
}
.input-group .form-control{
display: block;
width: 100%;
padding: 10px 5px;
margin-top: 7px;

}
.submit-btn .btn-dark{
padding: 15px 35px;
background: #045D5D;
color: #fff;
border: none;
}
#output {
display: block;
font-size: large;
height: 20px;
margin-top: 5px;
text-align: center;
}
#output a {
color: black;
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 */
}
</style>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
function deleteEntry(buttonElement) {
// Getting the ID from the button's data-id attribute
const entryId = buttonElement.getAttribute("data-id");

// Sending the DELETE request
fetch('/delete/' + entryId, {
method: 'DELETE',
})
.then(response => {
if (response.ok) {
// Reloading the page on successful delete
location.reload();
} else {
throw new Error('Failed to delete entry');
}
})
.catch(error => {
// Popping a JS alert on failure
alert('Failed to delete the entry: ' + error.message);
});
}
</script>
<div id="url-shortner">
<!-- CONTAINER WITH MAX-WIDTH 600 -->
<div class="container">
<!-- FORM CARD WITH WHITE BG -->
<div class="form-card">
<div class="text-center"><h1>LARD: A Redirect Daemon</h1></div>
<!-- FORM -->
<!-- INPUT FOR URL -->
<div class="input-group">
<table id="links">
<thead>
<tr>
<th>ID</th>
<th>Short URL</th>
<th>Long URL</th>
<th>Views</th>
<th>Last Access</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr>
<td data-colname="id">{{ entry[0] }}</td> <!-- ID -->
<td data-colname="short">{{ entry[1] }}</td> <!-- Short URL -->
<td data-colname="long">{{ entry[2] }}</td> <!-- Long URL -->
<td data-colname="views">{{ entry[3] }}</td> <!-- Views -->
<td data-colname="access">{{ entry[4] }}</td> <!-- Last Access -->
<td>
<button data-id="{{ entry[0] }}" onclick="deleteEntry(this)">Delete</button>
</td>
</tr>
{% endfor %}

</tbody>
</table>

</div>
</div> <!-- FORM-CARD CLOSE -->
</div> <!-- CONTAINER CLOSE -->

</div>
</body>
</html>













File renamed without changes.
3 changes: 2 additions & 1 deletion data/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ baseurl = https://l.yourdomain.com
length = 5

[Auth]
password = lard
password = lard
admin = admin:$2y$10$TGVz8YgPBXggJAf.BjOjHeMls59VXI7g7bGLLX9zF4uvHJcM8nKjG

0 comments on commit 8e3b371

Please sign in to comment.