Compare commits

...
10 Commits
Author SHA1 Message Date
JosephFerano 4e05999df8 Fix README 2023-05-20 14:55:14 +07:00
JosephFerano a9c88d68e3 Don't render TOC 2023-05-20 14:41:10 +07:00
JosephFerano ed5ca366fe Added License and WIP Readme 2023-05-20 14:39:55 +07:00
JosephFerano 505c201654 Update TODOs 2023-05-19 19:44:44 +07:00
JosephFerano 88582d9ff3 Bad ChatGPT 2023-05-19 19:34:53 +07:00
JosephFerano f3a858956e Init db and and use dotenv to load database credentials 2023-05-19 19:19:17 +07:00
JosephFerano cd74846a7f Change URL 2023-05-19 17:16:43 +07:00
JosephFerano 52e38adcb7 Change port 2023-05-19 16:28:21 +07:00
JosephFerano 1424fb2f43 Docker-compose with the couchdb backend 2023-05-19 16:20:57 +07:00
JosephFerano de85790ffa Copy to clipboard button flow, and return 404 page if url not found 2023-05-19 16:19:20 +07:00
11 changed files with 142 additions and 27 deletions

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
/target /target
venv venv
__pycache__ __pycache__
/.env
/data/
+13 -6
View File
@@ -1,11 +1,18 @@
FROM python:3.11 # Use an official Python runtime as a parent image
FROM python:3
WORKDIR /code # Set the working directory in the container
WORKDIR /app
COPY ./requirements.txt /code/requirements.txt # Install any needed packages specified in requirements.txt
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt # Copy the current directory contents into the container at /app
COPY . /app/
COPY ./app /code/app # Make port 8000 available to the world outside this container
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] # 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:
+17 -3
View File
@@ -1,14 +1,23 @@
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse, FileResponse from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from dotenv import load_dotenv
import couchdb import couchdb
import os
import uuid import uuid
import validators 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 = FastAPI()
app.mount("/static", StaticFiles(directory="static", html=True), name="static") 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("/") @app.get("/")
async def root(): async def root():
@@ -16,8 +25,13 @@ async def root():
@app.get("/{url_id}", status_code=301, response_class=RedirectResponse) @app.get("/{url_id}", status_code=301, response_class=RedirectResponse)
async def redirect_urls(url_id): async def redirect_urls(url_id):
target_url = couch["urls"][url_id].get('full_url') db = couch["urls"]
if url_id in db:
target_url = db[url_id].get('full_url')
return RedirectResponse(target_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: Get JWT tokens working and use that to return only the user's urls
# TODO: Look into how FastAPI handles the Authentication header # TODO: Look into how FastAPI handles the Authentication header
@@ -43,7 +57,7 @@ async def create_url(body: dict):
# TODO: Calculate how many unique IDs we are actually generating # TODO: Calculate how many unique IDs we are actually generating
url_id = uuid.uuid4().hex[:6] url_id = uuid.uuid4().hex[:6]
db[url_id] = { "full_url": body["url"], "user_id": body["username"] } db[url_id] = { "full_url": body["url"], "user_id": body["username"] }
return { "shortenedUrl": "http://localhost:8000/" + url_id } return { "shortenedUrl": "https://zipmy.link/" + url_id }
else: else:
raise HTTPException(status_code=400, detail="Url provided is invalid") raise HTTPException(status_code=400, detail="Url provided is invalid")
+4
View File
@@ -32,6 +32,10 @@ GET :host/eecd98
# Talk directly to CouchDB # 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 value from key
GET http://admin:password@127.0.0.1:5984/urls/_all_docs?include_docs=true GET http://admin:password@127.0.0.1:5984/urls/_all_docs?include_docs=true
:headers :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
+12 -13
View File
@@ -2,20 +2,19 @@
#+TODO: TODO INPROGRESS | DONE BACKLOG #+TODO: TODO INPROGRESS | DONE BACKLOG
#+STARTUP: overview #+STARTUP: overview
* TODO Move everything into an app directory * DONE Add button to copy shortened URL to clipboard
* TODO Knock out source code TODOs * DONE Add a 404 page in case the URL provided is invalid
* TODO Add button to copy shortened URL to clipboard * DONE RTFM on how to create docker compose files
* TODO Add a 404 page in case the URL provided is invalid * DONE Containerize couchdb
* TODO Get the container running with the Dockerfile * DONE Get the container running with the Dockerfile
* TODO Point zipmy.link to joe-vps * DONE Point zipmy.link to joe-vps
- [ ] Add DNS records - [X] Add DNS records
- [ ] Add to nginx.conf - [X] Add to nginx.conf
- [ ] Generate certbot SSL cert - [X] Generate certbot SSL cert
* TODO Create a README.org * DONE Add MIT license
* TODO Add MIT license * INPROGRESS Create a README.org
* BACKLOG Containerize couchdb
* BACKLOG RTFM on how to create docker compose files
* BACKLOG RTFM on how to create a container network * BACKLOG RTFM on how to create a container network
* BACKLOG Gitea Actions * BACKLOG Gitea Actions
* BACKLOG Unit tests * BACKLOG Unit tests
+1
View File
@@ -3,3 +3,4 @@ uvicorn
couchdb couchdb
aiofiles aiofiles
validators 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>
+18 -5
View File
@@ -1,20 +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() { async function shortenUrl() {
const url = document.getElementById("url-input").value; const urlInput = document.getElementById("url-input");
const btn = document.getElementById("submit-btn").value; const btn = document.getElementById("submit-btn");
// btn.enab // btn.enab
const response = await fetch("/api/v1/urls", { const response = await fetch("/api/v1/urls", {
method: 'PUT', method: 'PUT',
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: url, username: "anonymous" }) body: JSON.stringify({ url: urlInput.value, username: "anonymous" })
}); });
const data = await response.json(); const data = await response.json();
const resultElement = document.getElementById("result"); const resultElement = document.getElementById("result");
if (response.ok) { if (response.ok) {
resultElement.textContent = `Shortened Url: ${data.shortenedUrl}` resultElement.textContent = ''
resultElement.style.color = "black"; urlInput.value = data.shortenedUrl;
btn.innerText = "📋 Copy to Clipboard!";
btn.onclick = copyUrlToClipboard;
} else { } else {
resultElement.textContent = `Error: ${data.detail}` resultElement.textContent = `Error: ${data.detail}`
resultElement.style.color = "red"; resultElement.style.color = "red";
btn.value = "Shorten Url";
} }
} }