mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:17:39 +03:00
url shortener addons
This commit is contained in:
41
backend/url-shortener/DEPLOY.md
Normal file
41
backend/url-shortener/DEPLOY.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# URL shortener — deploy (FE → BE)
|
||||||
|
|
||||||
|
**Topology:** same as [INFRA.md](../../docs/INFRA.md) — TLS on Gentoo FE, app on Alpine BE `:80`.
|
||||||
|
|
||||||
|
## Prerequisites (confirmed)
|
||||||
|
|
||||||
|
| Item | Status |
|
||||||
|
|------|--------|
|
||||||
|
| DNS `s.f0xx.org` → FE | PO |
|
||||||
|
| FE TLS cert | `/etc/letsencrypt/live/s.f0xx.org/` |
|
||||||
|
| Git remote | `git://f0xx.org/androicast_project/url-shortener` |
|
||||||
|
| MariaDB schema `url_shortener` | `sql/schema.mariadb.sql` |
|
||||||
|
|
||||||
|
## FE (Gentoo)
|
||||||
|
|
||||||
|
Add `deploy/nginx.fe-s.f0xx.org.snippet` inside a new `server { listen 443 ssl; server_name s.f0xx.org; }` block
|
||||||
|
(mirror `apps.f0xx.org` TLS paths; cert paths above).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nginx -t && rc-service nginx reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## BE (Alpine)
|
||||||
|
|
||||||
|
1. Sync this tree to docroot (suggested): `/var/www/localhost/htdocs/apps/s/`
|
||||||
|
2. Include `deploy/nginx.be-url-shortener.fragment` in `apps.conf` **or** dedicated `server_name` on BE
|
||||||
|
3. Apply SQL migration
|
||||||
|
4. `rc-service php-fpm81 restart` if needed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nginx -t && rc-service nginx reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sSI https://s.f0xx.org/api/v1/health
|
||||||
|
./scripts/smoke_shorten.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Not added to `validate_be_services.sh` until first production deploy (SPEC).
|
||||||
42
backend/url-shortener/README.md
Normal file
42
backend/url-shortener/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# URL shortener (`s.f0xx.org`)
|
||||||
|
|
||||||
|
Standalone opt-in service — **not** part of `examples/crash_reporter/backend/`.
|
||||||
|
|
||||||
|
| Doc | Path |
|
||||||
|
|-----|------|
|
||||||
|
| SPEC | [docs/specs/20100611_3_url_shortener.md](../../docs/specs/20100611_3_url_shortener.md) |
|
||||||
|
| DR | [docs/DRs/20100611_3_url_shortener.md](../../docs/DRs/20100611_3_url_shortener.md) |
|
||||||
|
| Deploy | [DEPLOY.md](DEPLOY.md) |
|
||||||
|
|
||||||
|
**Status:** scaffold (OpenAPI + SQL + nginx fragments + smoke scripts). PHP handlers TODO.
|
||||||
|
|
||||||
|
**Git remote (PO confirmed):** `git://f0xx.org/androicast_project/url-shortener`
|
||||||
|
**Public URL:** `https://s.f0xx.org`
|
||||||
|
**FE TLS (PO confirmed):** `/etc/letsencrypt/live/s.f0xx.org/{fullchain.pem,privkey.pem}`
|
||||||
|
|
||||||
|
## Quick smoke (when deployed)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BASE=https://s.f0xx.org
|
||||||
|
export BEARER=demo-bearer-replace-me
|
||||||
|
./scripts/demo_curl.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local dev (future)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MariaDB: apply sql/schema.mariadb.sql
|
||||||
|
# php -S 127.0.0.1:8091 -t public
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
backend/url-shortener/
|
||||||
|
openapi.yaml # API contract (source of truth with SPEC §6)
|
||||||
|
deploy/ # FE + BE nginx fragments
|
||||||
|
sql/schema.mariadb.sql
|
||||||
|
scripts/demo_curl.sh
|
||||||
|
scripts/smoke_shorten.sh
|
||||||
|
public/index.php # stub router (health only until impl)
|
||||||
|
```
|
||||||
29
backend/url-shortener/deploy/nginx.be-url-shortener.fragment
Normal file
29
backend/url-shortener/deploy/nginx.be-url-shortener.fragment
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# BE Alpine — url-shortener vhost fragment
|
||||||
|
# Include from /etc/nginx/conf.d/apps.conf or separate file.
|
||||||
|
#
|
||||||
|
# Docroot: /var/www/localhost/htdocs/apps/s/public
|
||||||
|
# PHP-FPM: same socket as crash reporter unless load warrants a pool.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name s.f0xx.org;
|
||||||
|
|
||||||
|
root /var/www/localhost/htdocs/apps/s/public;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
# API + QR
|
||||||
|
location ^~ /api/ {
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Short slug redirect (12–16 hex chars)
|
||||||
|
location ~ ^/[a-f0-9]{6,16}$ {
|
||||||
|
try_files $uri /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_pass unix:/run/php-fpm81.sock;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet
Normal file
30
backend/url-shortener/deploy/nginx.fe-s.f0xx.org.snippet
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# FE Gentoo — server block for s.f0xx.org (URL shortener)
|
||||||
|
# File: tmp/FE_gentoo/etc/nginx/nginx.conf OR conf.d/s.f0xx.org.conf
|
||||||
|
#
|
||||||
|
# TLS cert (confirmed on FE):
|
||||||
|
# ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem;
|
||||||
|
# ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name s.f0xx.org;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name s.f0xx.org;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/s.f0xx.org/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/s.f0xx.org/privkey.pem;
|
||||||
|
|
||||||
|
# All paths → BE :80 (PHP-FPM url-shortener vhost)
|
||||||
|
location / {
|
||||||
|
proxy_pass http://artc0.intra.raptor.org:80;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
backend/url-shortener/openapi.yaml
Normal file
253
backend/url-shortener/openapi.yaml
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Android Cast URL Shortener
|
||||||
|
description: |
|
||||||
|
Standalone service at https://s.f0xx.org — SPEC docs/specs/20100611_3_url_shortener.md
|
||||||
|
version: 0.1.0
|
||||||
|
servers:
|
||||||
|
- url: https://s.f0xx.org
|
||||||
|
description: Production (FE TLS → BE :80)
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/api/v1/health:
|
||||||
|
get:
|
||||||
|
summary: Service health
|
||||||
|
operationId: health
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
service:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/api/v1/shorten:
|
||||||
|
get:
|
||||||
|
summary: Shorten URL (query params)
|
||||||
|
description: Prefer POST for long URLs or sensitive query strings.
|
||||||
|
operationId: shortenGet
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/url'
|
||||||
|
- $ref: '#/components/parameters/bearer'
|
||||||
|
- $ref: '#/components/parameters/signer'
|
||||||
|
- $ref: '#/components/parameters/ttl'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/components/responses/ShortenSuccess'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'409':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
post:
|
||||||
|
summary: Shorten URL (JSON body) — preferred
|
||||||
|
operationId: shortenPost
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ShortenRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
$ref: '#/components/responses/ShortenSuccess'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'401':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'409':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
'429':
|
||||||
|
$ref: '#/components/responses/ShortenError'
|
||||||
|
|
||||||
|
/api/v1/qr/{slug}.png:
|
||||||
|
get:
|
||||||
|
summary: QR code PNG for short URL
|
||||||
|
operationId: qrPng
|
||||||
|
parameters:
|
||||||
|
- name: slug
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^[a-f0-9]{6,16}$'
|
||||||
|
- name: size
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 256
|
||||||
|
minimum: 128
|
||||||
|
maximum: 512
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: PNG image
|
||||||
|
content:
|
||||||
|
image/png:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
'404':
|
||||||
|
description: Unknown slug
|
||||||
|
'410':
|
||||||
|
description: Expired slug
|
||||||
|
|
||||||
|
/{slug}:
|
||||||
|
get:
|
||||||
|
summary: Redirect to original URL
|
||||||
|
operationId: redirect
|
||||||
|
parameters:
|
||||||
|
- name: slug
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
pattern: '^[a-f0-9]{6,16}$'
|
||||||
|
responses:
|
||||||
|
'302':
|
||||||
|
description: Found — Location is original URL
|
||||||
|
headers:
|
||||||
|
Location:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
'404':
|
||||||
|
description: Unknown slug
|
||||||
|
'410':
|
||||||
|
description: Expired slug
|
||||||
|
head:
|
||||||
|
summary: Redirect probe (no body)
|
||||||
|
operationId: redirectHead
|
||||||
|
parameters:
|
||||||
|
- name: slug
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'302':
|
||||||
|
description: Found
|
||||||
|
'404':
|
||||||
|
description: Unknown
|
||||||
|
'410':
|
||||||
|
description: Expired
|
||||||
|
|
||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
url:
|
||||||
|
name: url
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
maxLength: 2048
|
||||||
|
bearer:
|
||||||
|
name: bearer
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
minLength: 8
|
||||||
|
signer:
|
||||||
|
name: signer
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
name: ttl
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: Seconds until expiry; 0 = permanent (signer required)
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 86400
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
ShortenRequest:
|
||||||
|
type: object
|
||||||
|
required: [url, bearer]
|
||||||
|
properties:
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
maxLength: 2048
|
||||||
|
bearer:
|
||||||
|
type: string
|
||||||
|
signer:
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
type: integer
|
||||||
|
minimum: 0
|
||||||
|
default: 86400
|
||||||
|
ShortenSuccess:
|
||||||
|
type: object
|
||||||
|
required: [code, url, u, bearer, ttl]
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
enum: ['0']
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
u:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: Short URL
|
||||||
|
bearer:
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
type: string
|
||||||
|
description: Remaining seconds as string; "0" if permanent
|
||||||
|
qr:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
description: Optional v1.1 — URL to QR PNG
|
||||||
|
ShortenError:
|
||||||
|
type: object
|
||||||
|
required: [code, desc]
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- ACCESS_DENIED
|
||||||
|
- MISSING_BEARER
|
||||||
|
- INVALID_URL
|
||||||
|
- TTL_EXPIRED
|
||||||
|
- SIGNER_REQUIRED
|
||||||
|
- RATE_LIMITED
|
||||||
|
- INTERNAL
|
||||||
|
- NOT_IMPLEMENTED
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
bearer:
|
||||||
|
type: string
|
||||||
|
desc:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
responses:
|
||||||
|
ShortenSuccess:
|
||||||
|
description: Short URL created or cached
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ShortenSuccess'
|
||||||
|
ShortenError:
|
||||||
|
description: Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ShortenError'
|
||||||
27
backend/url-shortener/public/index.php
Normal file
27
backend/url-shortener/public/index.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL shortener front controller (stub).
|
||||||
|
* Routes: /api/v1/health, /api/v1/shorten, /api/v1/qr/{slug}.png, /{slug}
|
||||||
|
* Full implementation tracked in docs/specs/20100611_3_url_shortener.md
|
||||||
|
*/
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||||
|
|
||||||
|
if ($uri === '/api/v1/health') {
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'stub',
|
||||||
|
'service' => 'url-shortener',
|
||||||
|
'message' => 'PHP handlers not deployed yet — see backend/url-shortener/README.md',
|
||||||
|
], JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
http_response_code(501);
|
||||||
|
echo json_encode([
|
||||||
|
'code' => 'NOT_IMPLEMENTED',
|
||||||
|
'desc' => 'url-shortener API stub — deploy implementation from git://f0xx.org/androicast_project/url-shortener',
|
||||||
|
], JSON_UNESCAPED_SLASHES);
|
||||||
55
backend/url-shortener/scripts/demo_curl.sh
Executable file
55
backend/url-shortener/scripts/demo_curl.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# End-to-end curl cookbook — SPEC appendix A.
|
||||||
|
# Usage:
|
||||||
|
# export BASE=https://s.f0xx.org
|
||||||
|
# export BEARER=your-trusted-token
|
||||||
|
# export SIGNER=optional-for-ttl0
|
||||||
|
# ./scripts/demo_curl.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="${BASE:-https://s.f0xx.org}"
|
||||||
|
BEARER="${BEARER:?set BEARER to a trusted API token}"
|
||||||
|
SIGNER="${SIGNER:-}"
|
||||||
|
TARGET="${TARGET:-https://defender.net/very/long/link?demo=1}"
|
||||||
|
TTL="${TTL:-3600}"
|
||||||
|
|
||||||
|
echo "==> 1) Health"
|
||||||
|
curl -fsS "$BASE/api/v1/health" | head -c 200
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "==> 2) Shorten (POST preferred)"
|
||||||
|
SHORT_JSON="$(curl -fsS -X POST "$BASE/api/v1/shorten" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -nc --arg url "$TARGET" --arg bearer "$BEARER" --argjson ttl "$TTL" \
|
||||||
|
'{url:$url,bearer:$bearer,ttl:$ttl}')")"
|
||||||
|
echo "$SHORT_JSON" | jq .
|
||||||
|
|
||||||
|
SHORT="$(echo "$SHORT_JSON" | jq -r '.u // empty')"
|
||||||
|
if [[ -z "$SHORT" ]]; then
|
||||||
|
echo "shorten failed (no .u in response)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
SLUG="${SHORT##*/}"
|
||||||
|
|
||||||
|
echo "==> 3) Shorten cache hit (same bearer + url)"
|
||||||
|
curl -fsS -X POST "$BASE/api/v1/shorten" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -nc --arg url "$TARGET" --arg bearer "$BEARER" --argjson ttl "$TTL" \
|
||||||
|
'{url:$url,bearer:$bearer,ttl:$ttl}')" | jq .
|
||||||
|
|
||||||
|
echo "==> 4) Redirect (HEAD — no body)"
|
||||||
|
curl -sSI "$SHORT" | grep -E '^(HTTP|Location:)'
|
||||||
|
|
||||||
|
echo "==> 5) QR PNG"
|
||||||
|
curl -fsS -o "/tmp/url-shortener-qr-${SLUG}.png" "$BASE/api/v1/qr/${SLUG}.png?size=256"
|
||||||
|
file "/tmp/url-shortener-qr-${SLUG}.png"
|
||||||
|
|
||||||
|
if [[ -n "$SIGNER" ]]; then
|
||||||
|
echo "==> 6) Permanent link (ttl=0, signer required)"
|
||||||
|
curl -fsS -X POST "$BASE/api/v1/shorten" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -nc --arg url "$TARGET&perm=1" --arg bearer "$BEARER" --arg signer "$SIGNER" \
|
||||||
|
'{url:$url,bearer:$bearer,signer:$signer,ttl:0}')" | jq .
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Done"
|
||||||
29
backend/url-shortener/scripts/smoke_shorten.sh
Executable file
29
backend/url-shortener/scripts/smoke_shorten.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Minimal deploy smoke — exits non-zero on failure.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE="${BASE:-https://s.f0xx.org}"
|
||||||
|
BEARER="${BEARER:-}"
|
||||||
|
|
||||||
|
echo "==> health $BASE/api/v1/health"
|
||||||
|
code="$(curl -sS -o /tmp/url-shortener-health.json -w '%{http_code}' "$BASE/api/v1/health" || true)"
|
||||||
|
if [[ "$code" != "200" ]]; then
|
||||||
|
echo "FAIL health HTTP $code" >&2
|
||||||
|
cat /tmp/url-shortener-health.json 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cat /tmp/url-shortener-health.json
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ -z "$BEARER" ]]; then
|
||||||
|
echo "SKIP shorten (set BEARER to run full smoke)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> shorten POST"
|
||||||
|
curl -fsS -X POST "$BASE/api/v1/shorten" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"url":"https://example.com/smoke","bearer":"'"$BEARER"'","ttl":300}' \
|
||||||
|
| jq -e '.code == "0" and (.u | length) > 0' >/dev/null
|
||||||
|
|
||||||
|
echo "OK"
|
||||||
62
backend/url-shortener/sql/schema.mariadb.sql
Normal file
62
backend/url-shortener/sql/schema.mariadb.sql
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
-- URL shortener — MariaDB schema (SPEC docs/specs/20100611_3_url_shortener.md §9)
|
||||||
|
CREATE DATABASE IF NOT EXISTS url_shortener
|
||||||
|
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE url_shortener;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bearer_tokens (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
token_hash CHAR(64) NOT NULL,
|
||||||
|
label VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
trusted TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
can_temporary TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
can_permanent TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
rate_limit_per_hour INT UNSIGNED NOT NULL DEFAULT 1000,
|
||||||
|
revoked_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_bearer_token_hash (token_hash)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS signer_keys (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
key_hash CHAR(64) NOT NULL,
|
||||||
|
label VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
can_permanent TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
revoked_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_signer_key_hash (key_hash)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS links (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
bearer_id INT UNSIGNED NOT NULL,
|
||||||
|
signer_id INT UNSIGNED NULL,
|
||||||
|
slug VARCHAR(16) NOT NULL,
|
||||||
|
origin TEXT NOT NULL,
|
||||||
|
origin_hash CHAR(64) NOT NULL,
|
||||||
|
ttl_seconds INT UNSIGNED NOT NULL DEFAULT 86400,
|
||||||
|
expires_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
accessed_at DATETIME NULL,
|
||||||
|
access_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_slug (slug),
|
||||||
|
KEY idx_bearer_origin (bearer_id, origin_hash),
|
||||||
|
CONSTRAINT fk_links_bearer FOREIGN KEY (bearer_id) REFERENCES bearer_tokens (id),
|
||||||
|
CONSTRAINT fk_links_signer FOREIGN KEY (signer_id) REFERENCES signer_keys (id)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
event VARCHAR(32) NOT NULL,
|
||||||
|
bearer_id INT UNSIGNED NULL,
|
||||||
|
slug VARCHAR(16) NULL,
|
||||||
|
ip VARCHAR(45) NOT NULL DEFAULT '',
|
||||||
|
detail TEXT,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_audit_created (created_at)
|
||||||
|
) ENGINE=InnoDB;
|
||||||
@@ -92,7 +92,7 @@ endobj
|
|||||||
endobj
|
endobj
|
||||||
13 0 obj
|
13 0 obj
|
||||||
<<
|
<<
|
||||||
/Author (Android Cast project) /CreationDate (D:20260521190501+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521190501+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
/Author (Android Cast project) /CreationDate (D:20260611185343+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260611185343+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||||
/Subject (\(unspecified\)) /Title (Android Cast \204 Next-gen egress review) /Trapped /False
|
/Subject (\(unspecified\)) /Title (Android Cast \204 Next-gen egress review) /Trapped /False
|
||||||
>>
|
>>
|
||||||
endobj
|
endobj
|
||||||
@@ -153,7 +153,7 @@ xref
|
|||||||
trailer
|
trailer
|
||||||
<<
|
<<
|
||||||
/ID
|
/ID
|
||||||
[<69103a44ba79ad224f4c4d9612cf06ea><69103a44ba79ad224f4c4d9612cf06ea>]
|
[<7980b3b39b2662662624213f08f5e4c7><7980b3b39b2662662624213f08f5e4c7>]
|
||||||
% ReportLab generated PDF document -- digest (opensource)
|
% ReportLab generated PDF document -- digest (opensource)
|
||||||
|
|
||||||
/Info 13 0 R
|
/Info 13 0 R
|
||||||
|
|||||||
Reference in New Issue
Block a user