Compare commits

...
13 Commits
12 changed files with 188 additions and 15 deletions

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
/target
venv
__pycache__
/.env
/data/
+18
View File
@@ -0,0 +1,18 @@
# Use an official Python runtime as a parent image
FROM python:3
# Set the working directory in the container
WORKDIR /app
# Install any needed packages specified in requirements.txt
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy the current directory contents into the container at /app
COPY . /app/
# Make port 8000 available to the world outside this container
EXPOSE 8000
# Run api.py when the container launches
CMD ["uvicorn", "api:app", "--host", "0.0.0.0", "--port", "3003"]
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2023 Joseph Ferano - joseph@ferano.io
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
#+TITLE: Zip My Link
#+AUTHOR: Joseph Ferano
#+OPTIONS: toc:nil
* About
This is a standard URL Shortening service built to practice web development.
Currently it works at providing the most basic feature which is to give the user
a shorter version of a URL they provide and then redirecting the user to the
site when consuming the link.
** What I'm using
- Python 3.11
- FastAPI
- CouchDB
- ~podman~ and ~podman-compose~
* COMMENT Local variables
;; Local Variables:
;; eval: (olivetti-mode t)
;; End:
+31 -14
View File
@@ -1,13 +1,23 @@
from fastapi import FastAPI, Response
from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv
import couchdb
import os
import uuid
import validators
load_dotenv()
COUCHDB_USER = os.getenv("COUCHDB_USER")
COUCHDB_PASSWORD = os.getenv("COUCHDB_PASSWORD")
COUCHDB_HOST = os.getenv("COUCHDB_HOST")
COUCHDB_PORT = os.getenv("COUCHDB_PORT")
app = FastAPI()
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
couch = couchdb.Server("http://admin:password@127.0.0.1:5984")
couch = couchdb.Server(f"http://{COUCHDB_USER}:{COUCHDB_PASSWORD}@{COUCHDB_HOST}:{COUCHDB_PORT}")
@app.get("/")
async def root():
@@ -15,8 +25,13 @@ async def root():
@app.get("/{url_id}", status_code=301, response_class=RedirectResponse)
async def redirect_urls(url_id):
target_url = couch["urls"][url_id].get('full_url')
return RedirectResponse(target_url)
db = couch["urls"]
if url_id in db:
target_url = db[url_id].get('full_url')
return RedirectResponse(target_url)
else:
return RedirectResponse('static/404.html')
# TODO: Get JWT tokens working and use that to return only the user's urls
# TODO: Look into how FastAPI handles the Authentication header
@@ -26,29 +41,31 @@ async def read_urls():
return [ { id: db[id].get('full_url') } for id in db ]
@app.get("/api/v1/urls/{url_id}")
async def read_url(url_id, response: Response):
async def read_url(url_id):
db = couch["urls"]
if url_id in db:
return db[url_id]
else:
response.status_code = 404
return { "error": "Url not found" }
raise HTTPException(status_code=404, detail="Url not found")
# TODO: Throttle
# TODO: If user not found, generate a new JWT
@app.put("/api/v1/urls")
async def create_url(body: dict):
db = couch["urls"]
url_id = uuid.uuid4().hex[:6]
db[url_id] = { "full_url": body["url"], "user_id": body["username"] }
return
if validators.url(body["url"]):
db = couch["urls"]
# TODO: Calculate how many unique IDs we are actually generating
url_id = uuid.uuid4().hex[:6]
db[url_id] = { "full_url": body["url"], "user_id": body["username"] }
return { "shortenedUrl": "https://zipmy.link/" + url_id }
else:
raise HTTPException(status_code=400, detail="Url provided is invalid")
@app.delete("/api/v1/urls/{url_id}")
async def delete_url(url_id, response: Response):
async def delete_url(url_id):
db = couch["urls"]
if url_id in db:
del db[url_id]
return { "message": "Url deleted" }
else:
response.status_code = 404
return { "error": "Url not found" }
raise HTTPException(status_code=404, detail="Url not found")
+4
View File
@@ -32,6 +32,10 @@ GET :host/eecd98
# Talk directly to CouchDB
###
# Get value from key
GET http://admin:password@127.0.0.1:5984/_all_dbs
:headers
# Get value from key
GET http://admin:password@127.0.0.1:5984/urls/_all_docs?include_docs=true
:headers
+19
View File
@@ -0,0 +1,19 @@
version: '3'
services:
couchdb:
image: couchdb:latest
network_mode: host
volumes:
- ./data:/opt/couchdb/data:Z
env_file:
- .env
fastapi_app:
build:
context: .
dockerfile: Dockerfile
network_mode: host
depends_on:
- couchdb
env_file:
- .env
+21
View File
@@ -0,0 +1,21 @@
#-*- mode: org -*-
#+TODO: TODO INPROGRESS | DONE BACKLOG
#+STARTUP: overview
* DONE Add button to copy shortened URL to clipboard
* DONE Add a 404 page in case the URL provided is invalid
* DONE RTFM on how to create docker compose files
* DONE Containerize couchdb
* DONE Get the container running with the Dockerfile
* DONE Point zipmy.link to joe-vps
- [X] Add DNS records
- [X] Add to nginx.conf
- [X] Generate certbot SSL cert
* DONE Add MIT license
* INPROGRESS Create a README.org
* BACKLOG RTFM on how to create a container network
* BACKLOG Gitea Actions
* BACKLOG Unit tests
* BACKLOG Switch webserver to Axum and Rust
+2
View File
@@ -2,3 +2,5 @@ fastapi
uvicorn
couchdb
aiofiles
validators
python-dotenv
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zip My Link</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<h1>Zip My Link</h1>
<h1>404</h1>
<p>The shortened URL you provided was not found. Sorry about that!</p>
<a href="index.html">zipmy.link</a>
</body>
</html>
+2 -1
View File
@@ -10,7 +10,8 @@
<h1>Zip My Link</h1>
<p>Type your long URL into the box and get a shorter URL back!</p>
<input type="text" id="url-input" placeholder="Enter your URL here">
<button onclick="submitUrl()">Shorten Url</button>
<button onclick="shortenUrl()" id="submit-btn">Shorten Url</button>
<p id="result"></p>
<script src="static/main.js"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
function copyUrlToClipboard() {
const urlInput = document.getElementById("url-input");
urlInput.select();
urlInput.setSelectionRange(0, 99999);
navigator.clipboard.writeText(urlInput.value);
}
function resetLinkGeneration() {
}
async function shortenUrl() {
const urlInput = document.getElementById("url-input");
const btn = document.getElementById("submit-btn");
// btn.enab
const response = await fetch("/api/v1/urls", {
method: 'PUT',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: urlInput.value, username: "anonymous" })
});
const data = await response.json();
const resultElement = document.getElementById("result");
if (response.ok) {
resultElement.textContent = ''
urlInput.value = data.shortenedUrl;
btn.innerText = "📋 Copy to Clipboard!";
btn.onclick = copyUrlToClipboard;
} else {
resultElement.textContent = `Error: ${data.detail}`
resultElement.style.color = "red";
btn.value = "Shorten Url";
}
}