check / check (push) Successful in 3m6s
The encrypted /v1/e/ route used the decrypted payload unchecked, so a token could request an over-limit size or an unknown fit mode; the generator turned unparseable numbers into 0. imgcache.ValidateDimension alone holds the MaxDimension bound and is used by the path parser, by the new ValidateImageRequest (which adds ValidateFitMode) and by the generator. Both image routes call ValidateImageRequest, so each answers 400. The generator answers 400 naming the field for a width or height that is not a number or fails that check, a quality that is not a number from 1 to 100, a ttl that is not a number from 0 to the largest the expiry calculation can hold, or an unknown fit. Empty quality is 85; empty ttl never expires. The form's size inputs stop at 8192. Model: opus-4-8 (implementation); opus-5-5 (rework)
186 lines
9.0 KiB
HTML
186 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Pixa - URL Generator</title>
|
|
<script src="/static/tailwind.js"></script>
|
|
</head>
|
|
<body class="bg-gray-100 min-h-screen">
|
|
<div class="max-w-2xl mx-auto py-8 px-4">
|
|
<div class="flex justify-between items-center mb-8">
|
|
<h1 class="text-2xl font-bold text-gray-800">Pixa URL Generator</h1>
|
|
<a href="/logout" class="text-sm text-gray-600 hover:text-gray-800 underline">
|
|
Logout
|
|
</a>
|
|
</div>
|
|
|
|
{{if .GeneratedURL}}
|
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
|
<h2 class="text-sm font-medium text-green-800 mb-2">Generated URL</h2>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
readonly
|
|
value="{{.GeneratedURL}}"
|
|
id="generated-url"
|
|
class="flex-1 px-3 py-2 bg-white border border-green-300 rounded-md text-sm font-mono"
|
|
onclick="this.select()"
|
|
>
|
|
<button
|
|
onclick="navigator.clipboard.writeText(document.getElementById('generated-url').value)"
|
|
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
|
>
|
|
Copy
|
|
</button>
|
|
</div>
|
|
<p class="text-xs text-green-600 mt-2">
|
|
Expires: {{.ExpiresAt}}
|
|
</p>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Error}}
|
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
|
|
{{.Error}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<form method="POST" action="/generate" class="bg-white rounded-lg shadow-md p-6 space-y-4">
|
|
{{ .CSRFField }}
|
|
<div>
|
|
<label for="url" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Source URL
|
|
</label>
|
|
<input
|
|
type="url"
|
|
id="url"
|
|
name="url"
|
|
required
|
|
placeholder="https://example.com/image.jpg"
|
|
value="{{.FormURL}}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="width" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Width
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="width"
|
|
name="width"
|
|
min="0"
|
|
max="8192"
|
|
value="{{if .FormWidth}}{{.FormWidth}}{{else}}0{{end}}"
|
|
placeholder="0 = original"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
</div>
|
|
<div>
|
|
<label for="height" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Height
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="height"
|
|
name="height"
|
|
min="0"
|
|
max="8192"
|
|
value="{{if .FormHeight}}{{.FormHeight}}{{else}}0{{end}}"
|
|
placeholder="0 = original"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="format" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Format
|
|
</label>
|
|
<select
|
|
id="format"
|
|
name="format"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="orig" {{if eq .FormFormat "orig"}}selected{{end}}>Original</option>
|
|
<option value="jpeg" {{if eq .FormFormat "jpeg"}}selected{{end}}>JPEG</option>
|
|
<option value="png" {{if eq .FormFormat "png"}}selected{{end}}>PNG</option>
|
|
<option value="webp" {{if eq .FormFormat "webp"}}selected{{end}}>WebP</option>
|
|
<option value="avif" {{if eq .FormFormat "avif"}}selected{{end}}>AVIF</option>
|
|
<option value="gif" {{if eq .FormFormat "gif"}}selected{{end}}>GIF</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="quality" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Quality
|
|
</label>
|
|
<select
|
|
id="quality"
|
|
name="quality"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="25" {{if eq .FormQuality "25"}}selected{{end}}>Potato</option>
|
|
<option value="50" {{if eq .FormQuality "50"}}selected{{end}}>Low</option>
|
|
<option value="70" {{if eq .FormQuality "70"}}selected{{end}}>Medium</option>
|
|
<option value="85" {{if or (eq .FormQuality "85") (eq .FormQuality "")}}selected{{end}}>High</option>
|
|
<option value="100" {{if eq .FormQuality "100"}}selected{{end}}>Ultra</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="fit" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Fit Mode
|
|
</label>
|
|
<select
|
|
id="fit"
|
|
name="fit"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="cover" {{if eq .FormFit "cover"}}selected{{end}}>Cover</option>
|
|
<option value="contain" {{if eq .FormFit "contain"}}selected{{end}}>Contain</option>
|
|
<option value="fill" {{if eq .FormFit "fill"}}selected{{end}}>Fill</option>
|
|
<option value="inside" {{if eq .FormFit "inside"}}selected{{end}}>Inside</option>
|
|
<option value="outside" {{if eq .FormFit "outside"}}selected{{end}}>Outside</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="ttl" class="block text-sm font-medium text-gray-700 mb-1">
|
|
Expires In
|
|
</label>
|
|
<select
|
|
id="ttl"
|
|
name="ttl"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="0" {{if or (eq .FormTTL "0") (eq .FormTTL "")}}selected{{end}}>Never</option>
|
|
<option value="60" {{if eq .FormTTL "60"}}selected{{end}}>1 minute</option>
|
|
<option value="3600" {{if eq .FormTTL "3600"}}selected{{end}}>1 hour</option>
|
|
<option value="86400" {{if eq .FormTTL "86400"}}selected{{end}}>1 day</option>
|
|
<option value="604800" {{if eq .FormTTL "604800"}}selected{{end}}>1 week</option>
|
|
<option value="2592000" {{if eq .FormTTL "2592000"}}selected{{end}}>30 days</option>
|
|
<option value="31536000" {{if eq .FormTTL "31536000"}}selected{{end}}>1 year</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
|
>
|
|
Generate Encrypted URL
|
|
</button>
|
|
</form>
|
|
|
|
<p class="text-xs text-gray-500 mt-4 text-center">
|
|
Generated URLs are encrypted and cannot be modified. They will expire at the specified time.
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>
|