Merge remote-tracking branch 'origin/master'
All checks were successful
Build and Publish / build_and_publish (push) Successful in 1m30s
All checks were successful
Build and Publish / build_and_publish (push) Successful in 1m30s
# Conflicts: # src/static/index.html
This commit is contained in:
commit
7af49cbb84
4 changed files with 72 additions and 20 deletions
|
@ -5,21 +5,33 @@ on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
build_and_publish:
|
build_and_publish:
|
||||||
runs-on: [ubuntu-latest]
|
runs-on: [ubuntu-latest]
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: git.i-am.nexus/nex/link-elongater
|
||||||
|
|
||||||
- name: Log into forgejo CR
|
- name: Log into forgejo CR
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: git.i-am.nexus
|
registry: git.i-am.nexus
|
||||||
username: nex
|
username: nex
|
||||||
password: ${{ secrets.CR_TOKEN }}
|
password: ${{ secrets.CR_PASSWORD }}
|
||||||
- name: Build Docker image
|
|
||||||
run: |
|
- name: Build and push
|
||||||
docker build -t git.i-am.nexus/nex/link-elongater:latest .
|
uses: docker/build-push-action@v5
|
||||||
- name: Push to forgejo CR
|
with:
|
||||||
run: |
|
context: .
|
||||||
docker push -a git.i-am.nexus/nex/link-elongater
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=git.i-am.nexus/nex/link-elongater:master
|
||||||
|
cache-to: type=inline
|
|
@ -7,7 +7,9 @@ from tortoise.contrib.pydantic import pydantic_model_creator
|
||||||
|
|
||||||
class Redirect(Model):
|
class Redirect(Model):
|
||||||
uuid = fields.UUIDField(primary_key=True, default=uuid.uuid4)
|
uuid = fields.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||||
slug = fields.CharField(max_length=32779, index=False, default=lambda: secrets.token_urlsafe(1024))
|
slug = fields.CharField(
|
||||||
|
max_length=32779, index=False, default=lambda: secrets.token_urlsafe(1024)
|
||||||
|
)
|
||||||
destination = fields.CharField(max_length=8192)
|
destination = fields.CharField(max_length=8192)
|
||||||
created_at = fields.DatetimeField(auto_now_add=True)
|
created_at = fields.DatetimeField(auto_now_add=True)
|
||||||
expires = fields.DatetimeField(null=True)
|
expires = fields.DatetimeField(null=True)
|
||||||
|
|
13
src/main.py
13
src/main.py
|
@ -8,7 +8,7 @@ import db
|
||||||
import requests
|
import requests
|
||||||
from fastapi import FastAPI, Request, HTTPException, status, Form
|
from fastapi import FastAPI, Request, HTTPException, status, Form
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse, JSONResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from tortoise.contrib.fastapi import RegisterTortoise
|
from tortoise.contrib.fastapi import RegisterTortoise
|
||||||
|
@ -94,6 +94,7 @@ async def list_redirects():
|
||||||
|
|
||||||
@app.post("/api/create", response_model=db.RedirectPydantic)
|
@app.post("/api/create", response_model=db.RedirectPydantic)
|
||||||
async def create_redirect(
|
async def create_redirect(
|
||||||
|
response: JSONResponse,
|
||||||
destination: str = Form(...),
|
destination: str = Form(...),
|
||||||
expires: typing.Optional[datetime.datetime] = Form(None),
|
expires: typing.Optional[datetime.datetime] = Form(None),
|
||||||
max_visits: typing.Optional[int] = Form(None),
|
max_visits: typing.Optional[int] = Form(None),
|
||||||
|
@ -106,6 +107,11 @@ async def create_redirect(
|
||||||
elif parsed.scheme not in ["http", "https"]:
|
elif parsed.scheme not in ["http", "https"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid URL - must be HTTP/HTTPS")
|
raise HTTPException(status_code=400, detail="Invalid URL - must be HTTP/HTTPS")
|
||||||
|
|
||||||
|
existing = await db.Redirect.get_or_none(destination=destination)
|
||||||
|
if existing is not None:
|
||||||
|
response.status_code = 200
|
||||||
|
return await db.RedirectPydantic.from_tortoise_orm(existing)
|
||||||
|
|
||||||
match slug_type:
|
match slug_type:
|
||||||
case "urlsafe":
|
case "urlsafe":
|
||||||
slug = secrets.token_urlsafe(slug_length // 2)
|
slug = secrets.token_urlsafe(slug_length // 2)
|
||||||
|
@ -130,7 +136,10 @@ async def create_redirect(
|
||||||
|
|
||||||
while await db.Redirect.get_or_none(slug=slug) is not None:
|
while await db.Redirect.get_or_none(slug=slug) is not None:
|
||||||
slug = secrets.token_urlsafe(slug_length // 2)
|
slug = secrets.token_urlsafe(slug_length // 2)
|
||||||
redirect = await db.Redirect.create(destination=destination, expires=expires, max_visits=max_visits, slug=slug)
|
redirect = await db.Redirect.create(
|
||||||
|
destination=destination, expires=expires, max_visits=max_visits, slug=slug
|
||||||
|
)
|
||||||
|
response.status_code = 201
|
||||||
return await db.RedirectPydantic.from_tortoise_orm(redirect)
|
return await db.RedirectPydantic.from_tortoise_orm(redirect)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,12 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Link Elongater - Create Redirect</title>
|
<title>Link Elongater - Create Redirect</title>
|
||||||
<script>
|
<script>
|
||||||
|
function onChange(e) {
|
||||||
|
let slugLength = parseInt(document.getElementById("slug_length").value);
|
||||||
|
let netloc = window.location.origin;
|
||||||
|
const totalLength = netloc.length + 3 + slugLength;
|
||||||
|
document.getElementById("urllen").textContent = totalLength;
|
||||||
|
}
|
||||||
function onSubmit(e) {
|
function onSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let formData = new FormData(e.target);
|
let formData = new FormData(e.target);
|
||||||
|
@ -44,9 +50,31 @@
|
||||||
"DOMContentLoaded",
|
"DOMContentLoaded",
|
||||||
() => {
|
() => {
|
||||||
document.querySelector("form").addEventListener("submit", onSubmit);
|
document.querySelector("form").addEventListener("submit", onSubmit);
|
||||||
|
document.getElementById("slug_length").addEventListener("input", onChange);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Create redirect</h1>
|
<h1>Create redirect</h1>
|
||||||
|
@ -60,10 +88,11 @@
|
||||||
<input type="url" id="destination" name="destination" required/><br/>
|
<input type="url" id="destination" name="destination" required/><br/>
|
||||||
<label for="expires">Expires:</label>
|
<label for="expires">Expires:</label>
|
||||||
<input type="date" id="expires" name="expires"/><br/>
|
<input type="date" id="expires" name="expires"/><br/>
|
||||||
<label for="max_visits">Max visits:</label>
|
<label for="max_visits">Max visits (default: unlimited):</label>
|
||||||
<input type="number" id="max_visits" name="max_visits"/><br/>
|
<input type="number" id="max_visits" name="max_visits"/><br/>
|
||||||
<label for="slug_length">Slug length:</label>
|
<label for="slug_length">Slug length:</label>
|
||||||
<input type="number" id="slug_length" name="slug_length" value="1000" min="2" max="16389"/><br/>
|
<input type="number" id="slug_length" name="slug_length" value="1000" min="2" max="16389"/>
|
||||||
|
<p>URL length will total to <span id="urllen">a lot of</span> characters.</p><br/>
|
||||||
<label for="slug_type">Slug type:</label>
|
<label for="slug_type">Slug type:</label>
|
||||||
<select id="slug_type" name="slug_type" value="urlsafe">
|
<select id="slug_type" name="slug_type" value="urlsafe">
|
||||||
<option value="urlsafe">Base64 (urlsafe)</option>
|
<option value="urlsafe">Base64 (urlsafe)</option>
|
||||||
|
|
Loading…
Reference in a new issue