Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c6a959b5 | ||
|
|
b77b0eaf22 | ||
|
|
602da7a45a | ||
|
|
35e771e2b6 |
+2
-66
@@ -10,20 +10,14 @@ run:
|
|||||||
|
|
||||||
linters:
|
linters:
|
||||||
default: all
|
default: all
|
||||||
enable:
|
|
||||||
# Successor to the deprecated gomodguard. Named explicitly, rather than
|
|
||||||
# left to `default: all`, because it carries the module policy below.
|
|
||||||
- gomodguard_v2
|
|
||||||
disable:
|
disable:
|
||||||
# Genuinely incompatible with project patterns
|
# Genuinely incompatible with project patterns
|
||||||
- exhaustruct # Requires all struct fields
|
- exhaustruct # Requires all struct fields
|
||||||
|
- depguard # Dependency allow/block lists
|
||||||
- godot # Requires comments to end with periods
|
- godot # Requires comments to end with periods
|
||||||
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
- wrapcheck # Too verbose for internal packages
|
- wrapcheck # Too verbose for internal packages
|
||||||
- varnamelen # Short names like db, id are idiomatic Go
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
# Deprecated: the warning is attached to the old name, so it is
|
|
||||||
# silenced by disabling that name, not by enabling the successor.
|
|
||||||
- wsl # Deprecated, replaced by wsl_v5
|
|
||||||
- gomodguard # Deprecated, replaced by gomodguard_v2
|
|
||||||
settings:
|
settings:
|
||||||
lll:
|
lll:
|
||||||
line-length: 88
|
line-length: 88
|
||||||
@@ -34,64 +28,6 @@ linters:
|
|||||||
max-complexity: 15
|
max-complexity: 15
|
||||||
dupl:
|
dupl:
|
||||||
threshold: 100
|
threshold: 100
|
||||||
depguard:
|
|
||||||
# Test-support code must not be compiled into the shipped binary. A
|
|
||||||
# test-support package exists to hand a test privileges the program
|
|
||||||
# itself must never have, so a file that is not a test must not import
|
|
||||||
# one. Test files, and the files inside a package whose directory name
|
|
||||||
# ends in `test`, are where that code belongs, and are exempt.
|
|
||||||
#
|
|
||||||
# The deny list below is the one part of this file a repository is
|
|
||||||
# expected to extend, and the only part it may. depguard matches an
|
|
||||||
# import path against a list of prefixes, so it cannot be told "any path
|
|
||||||
# whose last segment ends in test"; a repository's own test-support
|
|
||||||
# packages have to be named here one at a time, by full import path,
|
|
||||||
# under a module path that differs from repository to repository. Add
|
|
||||||
# them; change nothing else.
|
|
||||||
rules:
|
|
||||||
test-support:
|
|
||||||
list-mode: lax
|
|
||||||
files:
|
|
||||||
- "$all"
|
|
||||||
- "!$test"
|
|
||||||
- "!**/*test/**"
|
|
||||||
deny:
|
|
||||||
- pkg: net/http/httptest
|
|
||||||
desc: >-
|
|
||||||
Test-support code belongs in test files and in packages whose
|
|
||||||
directory name ends in test, not in the shipped binary.
|
|
||||||
# Only decisions already recorded in the Go package defaults are
|
|
||||||
# listed here. Every entry matches the module path exactly.
|
|
||||||
gomodguard_v2:
|
|
||||||
blocked:
|
|
||||||
- module: github.com/rs/zerolog
|
|
||||||
recommendations:
|
|
||||||
- log/slog
|
|
||||||
reason: "Structured logging is stdlib log/slog."
|
|
||||||
# One entry per pre-fork module path, because the later releases
|
|
||||||
# are separate paths. A prefix match would be shorter but would
|
|
||||||
# also reach github.com/go-redis/redismock, the test double for
|
|
||||||
# the successor these entries recommend.
|
|
||||||
- module: github.com/go-redis/redis
|
|
||||||
recommendations:
|
|
||||||
- github.com/redis/go-redis/v9
|
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
|
||||||
- module: github.com/go-redis/redis/v7
|
|
||||||
recommendations:
|
|
||||||
- github.com/redis/go-redis/v9
|
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
|
||||||
- module: github.com/go-redis/redis/v8
|
|
||||||
recommendations:
|
|
||||||
- github.com/redis/go-redis/v9
|
|
||||||
reason: "Pre-fork module; use the maintained go-redis v9."
|
|
||||||
- module: github.com/sergi/go-diff
|
|
||||||
recommendations:
|
|
||||||
- github.com/aymanbagabas/go-udiff
|
|
||||||
reason: "No unified diff output; use go-udiff."
|
|
||||||
- module: github.com/hexops/gotextdiff
|
|
||||||
recommendations:
|
|
||||||
- github.com/aymanbagabas/go-udiff
|
|
||||||
reason: "Unmaintained fork; use go-udiff."
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
max-issues-per-linter: 0
|
max-issues-per-linter: 0
|
||||||
|
|||||||
@@ -192,9 +192,8 @@ path under `/v1/` answers 200, in maintenance mode too.
|
|||||||
- `GET /.well-known/healthcheck.json` — JSON with `status` (`ok`), `now`,
|
- `GET /.well-known/healthcheck.json` — JSON with `status` (`ok`), `now`,
|
||||||
`uptime_seconds`, `uptime_human`, `version`, `appname` and
|
`uptime_seconds`, `uptime_human`, `version`, `appname` and
|
||||||
`maintenance_mode`. Needs: nothing. Answers: 200, always.
|
`maintenance_mode`. Needs: nothing. Answers: 200, always.
|
||||||
- `GET /static/<file>` — the stylesheet and script the login and generator
|
- `GET /static/<file>` — the script the login and generator pages load. Needs:
|
||||||
pages load. Needs: nothing. Answers: 200, or 404 for a file that does not
|
nothing. Answers: 200, or 404 for a file that does not exist.
|
||||||
exist.
|
|
||||||
- `GET /metrics` — Prometheus metrics (see Architecture). Needs: HTTP basic
|
- `GET /metrics` — Prometheus metrics (see Architecture). Needs: HTTP basic
|
||||||
authentication with `metrics.username` and `metrics.password`. Answers: 200;
|
authentication with `metrics.username` and `metrics.password`. Answers: 200;
|
||||||
401 without them; 404 when they are not set, as the route then does not exist.
|
401 without them; 404 when they are not set, as the route then does not exist.
|
||||||
|
|||||||
@@ -61,20 +61,6 @@ P2: security: referer blacklist
|
|||||||
not exist is passed over; any other error, such as a directory on the path
|
not exist is passed over; any other error, such as a directory on the path
|
||||||
that pixa may not enter, aborts startup naming the file, as a file that does
|
that pixa may not enter, aborts startup naming the file, as a file that does
|
||||||
not parse already did.
|
not parse already did.
|
||||||
- 2026-10-04 `.golangci.yml` re-vendored from the canonical copy (closes #57):
|
|
||||||
the deprecated `gomodguard` is switched off, so lint runs print no
|
|
||||||
deprecation warning; its successor `gomodguard_v2` runs with the shared
|
|
||||||
module block list, and `depguard` keeps `net/http/httptest` out of files that
|
|
||||||
are not tests. The tree needed no code changes.
|
|
||||||
- 2026-10-04 the Content-Security-Policy allows no inline script or style
|
|
||||||
(closes #125): `script-src` and `style-src` are `'self'` only. The generator
|
|
||||||
page's two inline `onclick` handlers moved into
|
|
||||||
`internal/static/generator.js`, attached with `addEventListener`; the bundled
|
|
||||||
Tailwind script, which built styles in the browser, is replaced by a small
|
|
||||||
hand-written `internal/static/style.css` with only the rules the login and
|
|
||||||
generator pages use, the templates carrying a few plain class names in place
|
|
||||||
of Tailwind's. No build step. The pages keep their layout, not every pixel of
|
|
||||||
it.
|
|
||||||
- 2026-10-04 deployment guide and example Caddy config (closes #89):
|
- 2026-10-04 deployment guide and example Caddy config (closes #89):
|
||||||
"Deployment" in `README.md` says what the reverse proxy in front of pixa must
|
"Deployment" in `README.md` says what the reverse proxy in front of pixa must
|
||||||
do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set
|
do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set
|
||||||
|
|||||||
@@ -35,10 +35,13 @@ const HSTSValue = "max-age=31536000; includeSubDomains"
|
|||||||
|
|
||||||
// ContentSecurityPolicyValue is the Content-Security-Policy header value.
|
// ContentSecurityPolicyValue is the Content-Security-Policy header value.
|
||||||
// default-src 'self' is the baseline and frame-ancestors 'none' is the primary
|
// default-src 'self' is the baseline and frame-ancestors 'none' is the primary
|
||||||
// clickjacking control.
|
// clickjacking control. 'unsafe-inline' is required in script-src and style-src
|
||||||
|
// because the served templates carry inline onclick handlers (generator page)
|
||||||
|
// and the bundled Tailwind asset injects a runtime <style> element; dropping it
|
||||||
|
// needs template changes outside this issue's scope.
|
||||||
const ContentSecurityPolicyValue = "default-src 'self'; " +
|
const ContentSecurityPolicyValue = "default-src 'self'; " +
|
||||||
"script-src 'self'; " +
|
"script-src 'self' 'unsafe-inline'; " +
|
||||||
"style-src 'self'; " +
|
"style-src 'self' 'unsafe-inline'; " +
|
||||||
"object-src 'none'; " +
|
"object-src 'none'; " +
|
||||||
"base-uri 'self'; " +
|
"base-uri 'self'; " +
|
||||||
"form-action 'self'; " +
|
"form-action 'self'; " +
|
||||||
|
|||||||
@@ -325,13 +325,6 @@ func TestSecurityHeaders_PolicyHeaders(t *testing.T) {
|
|||||||
|
|
||||||
handler.ServeHTTP(rec, req)
|
handler.ServeHTTP(rec, req)
|
||||||
|
|
||||||
// The login and generator pages load their script and stylesheet from
|
|
||||||
// /static, so the policy allows no inline script or style.
|
|
||||||
csp := rec.Header().Get("Content-Security-Policy")
|
|
||||||
if strings.Contains(csp, "unsafe-inline") {
|
|
||||||
t.Errorf("Content-Security-Policy allows unsafe-inline: %q", csp)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
header string
|
header string
|
||||||
want string
|
want string
|
||||||
@@ -340,8 +333,8 @@ func TestSecurityHeaders_PolicyHeaders(t *testing.T) {
|
|||||||
{
|
{
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
"default-src 'self'; " +
|
"default-src 'self'; " +
|
||||||
"script-src 'self'; " +
|
"script-src 'self' 'unsafe-inline'; " +
|
||||||
"style-src 'self'; " +
|
"style-src 'self' 'unsafe-inline'; " +
|
||||||
"object-src 'none'; " +
|
"object-src 'none'; " +
|
||||||
"base-uri 'self'; " +
|
"base-uri 'self'; " +
|
||||||
"form-action 'self'; " +
|
"form-action 'self'; " +
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (s *Server) SetupRoutes() {
|
|||||||
// Robots.txt
|
// Robots.txt
|
||||||
s.router.Get("/robots.txt", s.h.HandleRobotsTxt())
|
s.router.Get("/robots.txt", s.h.HandleRobotsTxt())
|
||||||
|
|
||||||
// The login and generator pages' stylesheet and script
|
// Static files (Tailwind CSS, etc.)
|
||||||
s.router.Handle("/static/*", http.StripPrefix("/static/", static.Handler()))
|
s.router.Handle("/static/*", http.StripPrefix("/static/", static.Handler()))
|
||||||
|
|
||||||
// Login/generator UI. The form routes carry CSRF protection; the
|
// Login/generator UI. The form routes carry CSRF protection; the
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// Generator page: a click on the generated URL selects it, and the Copy
|
|
||||||
// button copies it. Both are on the page only once a URL has been generated.
|
|
||||||
const generatedURL = document.getElementById("generated-url");
|
|
||||||
|
|
||||||
if (generatedURL) {
|
|
||||||
generatedURL.addEventListener("click", () => generatedURL.select());
|
|
||||||
document.getElementById("copy-url").addEventListener("click", () => {
|
|
||||||
navigator.clipboard.writeText(generatedURL.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *.css *.js
|
//go:embed *.js
|
||||||
var files embed.FS
|
var files embed.FS
|
||||||
|
|
||||||
// FS returns the embedded filesystem containing static files.
|
// FS returns the embedded filesystem containing static files.
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
/* The login and generator pages. */
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f3f4f6;
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
box-shadow: 0 1px 2px rgb(0 0 0 / 5%);
|
|
||||||
font: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus,
|
|
||||||
select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 2px #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
background: #2563eb;
|
|
||||||
color: #fff;
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: 2px solid #3b82f6;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
form > * + * {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgb(0 0 0 / 10%),
|
|
||||||
0 2px 4px -2px rgb(0 0 0 / 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border: 1px solid #f87171;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
background: #fee2e2;
|
|
||||||
color: #b91c1c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login page: the card centred on the screen. */
|
|
||||||
|
|
||||||
.login {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .card {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 28rem;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login h1 {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Generator page. */
|
|
||||||
|
|
||||||
.page {
|
|
||||||
max-width: 42rem;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
header a {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
header a:hover {
|
|
||||||
color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid #bbf7d0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background: #f0fdf4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result h2 {
|
|
||||||
margin: 0 0 0.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result div {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result input {
|
|
||||||
flex: 1;
|
|
||||||
border-color: #86efac;
|
|
||||||
box-shadow: none;
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result button {
|
|
||||||
width: auto;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
background: #16a34a;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result button:hover {
|
|
||||||
background: #15803d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result p {
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #16a34a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.columns {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
margin-top: 1rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #6b7280;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -4,47 +4,52 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Pixa - URL Generator</title>
|
<title>Pixa - URL Generator</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<script src="/static/tailwind.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-gray-100 min-h-screen">
|
||||||
<div class="page">
|
<div class="max-w-2xl mx-auto py-8 px-4">
|
||||||
<header>
|
<div class="flex justify-between items-center mb-8">
|
||||||
<h1>Pixa URL Generator</h1>
|
<h1 class="text-2xl font-bold text-gray-800">Pixa URL Generator</h1>
|
||||||
<a href="/logout">
|
<a href="/logout" class="text-sm text-gray-600 hover:text-gray-800 underline">
|
||||||
Logout
|
Logout
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</div>
|
||||||
|
|
||||||
{{if .GeneratedURL}}
|
{{if .GeneratedURL}}
|
||||||
<div class="result">
|
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
||||||
<h2>Generated URL</h2>
|
<h2 class="text-sm font-medium text-green-800 mb-2">Generated URL</h2>
|
||||||
<div>
|
<div class="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
readonly
|
readonly
|
||||||
value="{{.GeneratedURL}}"
|
value="{{.GeneratedURL}}"
|
||||||
id="generated-url"
|
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"
|
||||||
>
|
>
|
||||||
<button id="copy-url">
|
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p class="text-xs text-green-600 mt-2">
|
||||||
Expires: {{.ExpiresAt}}
|
Expires: {{.ExpiresAt}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Error}}
|
{{if .Error}}
|
||||||
<div class="error">
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
|
||||||
{{.Error}}
|
{{.Error}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form method="POST" action="/generate" class="card">
|
<form method="POST" action="/generate" class="bg-white rounded-lg shadow-md p-6 space-y-4">
|
||||||
{{ .CSRFField }}
|
{{ .CSRFField }}
|
||||||
<div>
|
<div>
|
||||||
<label for="url">
|
<label for="url" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Source URL
|
Source URL
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -54,12 +59,13 @@
|
|||||||
required
|
required
|
||||||
placeholder="https://example.com/image.jpg"
|
placeholder="https://example.com/image.jpg"
|
||||||
value="{{.FormURL}}"
|
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>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="width">
|
<label for="width" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Width
|
Width
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -70,10 +76,11 @@
|
|||||||
max="8192"
|
max="8192"
|
||||||
value="{{if .FormWidth}}{{.FormWidth}}{{else}}0{{end}}"
|
value="{{if .FormWidth}}{{.FormWidth}}{{else}}0{{end}}"
|
||||||
placeholder="0 = original"
|
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>
|
<div>
|
||||||
<label for="height">
|
<label for="height" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Height
|
Height
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -84,16 +91,21 @@
|
|||||||
max="8192"
|
max="8192"
|
||||||
value="{{if .FormHeight}}{{.FormHeight}}{{else}}0{{end}}"
|
value="{{if .FormHeight}}{{.FormHeight}}{{else}}0{{end}}"
|
||||||
placeholder="0 = original"
|
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>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="format">
|
<label for="format" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Format
|
Format
|
||||||
</label>
|
</label>
|
||||||
<select id="format" name="format">
|
<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="orig" {{if eq .FormFormat "orig"}}selected{{end}}>Original</option>
|
||||||
<option value="jpeg" {{if eq .FormFormat "jpeg"}}selected{{end}}>JPEG</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="png" {{if eq .FormFormat "png"}}selected{{end}}>PNG</option>
|
||||||
@@ -103,10 +115,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="quality">
|
<label for="quality" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Quality
|
Quality
|
||||||
</label>
|
</label>
|
||||||
<select id="quality" name="quality">
|
<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="25" {{if eq .FormQuality "25"}}selected{{end}}>Potato</option>
|
||||||
<option value="50" {{if eq .FormQuality "50"}}selected{{end}}>Low</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="70" {{if eq .FormQuality "70"}}selected{{end}}>Medium</option>
|
||||||
@@ -116,12 +132,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="fit">
|
<label for="fit" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Fit Mode
|
Fit Mode
|
||||||
</label>
|
</label>
|
||||||
<select id="fit" name="fit">
|
<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="cover" {{if eq .FormFit "cover"}}selected{{end}}>Cover</option>
|
||||||
<option value="contain" {{if eq .FormFit "contain"}}selected{{end}}>Contain</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="fill" {{if eq .FormFit "fill"}}selected{{end}}>Fill</option>
|
||||||
@@ -130,10 +150,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="ttl">
|
<label for="ttl" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Expires In
|
Expires In
|
||||||
</label>
|
</label>
|
||||||
<select id="ttl" name="ttl">
|
<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="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="60" {{if eq .FormTTL "60"}}selected{{end}}>1 minute</option>
|
||||||
<option value="3600" {{if eq .FormTTL "3600"}}selected{{end}}>1 hour</option>
|
<option value="3600" {{if eq .FormTTL "3600"}}selected{{end}}>1 hour</option>
|
||||||
@@ -145,15 +169,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit">
|
<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
|
Generate Encrypted URL
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="note">
|
<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.
|
Generated URLs are encrypted and cannot be modified. They will expire at the specified time.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/generator.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Pixa - Login</title>
|
<title>Pixa - Login</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<script src="/static/tailwind.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="login">
|
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
|
||||||
<div class="card">
|
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
|
||||||
<h1>Pixa Image Proxy</h1>
|
<h1 class="text-2xl font-bold text-gray-800 mb-6 text-center">Pixa Image Proxy</h1>
|
||||||
|
|
||||||
{{if .Error}}
|
{{if .Error}}
|
||||||
<div class="error">
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
||||||
{{.Error}}
|
{{.Error}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<form method="POST" action="/">
|
<form method="POST" action="/" class="space-y-4">
|
||||||
{{ .CSRFField }}
|
{{ .CSRFField }}
|
||||||
<div>
|
<div>
|
||||||
<label for="key">
|
<label for="key" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
Signing Key
|
Signing Key
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -28,11 +28,15 @@
|
|||||||
name="key"
|
name="key"
|
||||||
required
|
required
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
|
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"
|
||||||
placeholder="Enter your signing key"
|
placeholder="Enter your signing key"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit">
|
<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"
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user