Fix delete color and contrast
This commit is contained in:
@@ -61,12 +61,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.handler.S
|
||||
func (s *Server) routes() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /{$}", s.handleIndex)
|
||||
mux.HandleFunc("POST /api/upload", s.handleUpload)
|
||||
mux.HandleFunc("POST /upload", s.handleUpload)
|
||||
mux.HandleFunc("GET /api/limits", s.handleLimits)
|
||||
mux.HandleFunc("GET /admin", s.handleAdmin)
|
||||
mux.HandleFunc("GET /d/{id}", s.handleDownload)
|
||||
mux.HandleFunc("GET /i/{id}", s.handleInfo)
|
||||
mux.HandleFunc("POST /api/d/{id}/delete", s.handleDelete)
|
||||
mux.HandleFunc("POST /d/{id}/delete", s.handleDelete)
|
||||
mux.HandleFunc("GET /login", s.handleLoginPage)
|
||||
mux.HandleFunc("POST /login", s.handleLogin)
|
||||
mux.HandleFunc("POST /logout", s.handleLogout)
|
||||
@@ -104,10 +104,10 @@ func (s *Server) staticHandler() http.Handler {
|
||||
// appCSP locks the application pages down to their own origin. The frontend has
|
||||
// no inline script and no third-party anything, so this can be strict.
|
||||
//
|
||||
// connect-src is not optional here: the upload page talks to /api/upload and
|
||||
// /api/limits over XMLHttpRequest, and every fetch-directive left unlisted
|
||||
// falls back to default-src, so omitting it makes the browser block every
|
||||
// upload before it reaches the network. See TestAppCSPAllowsWhatThePageDoes.
|
||||
// connect-src is not optional here: the upload page posts to /upload over
|
||||
// XMLHttpRequest to draw a progress bar, and every fetch-directive left
|
||||
// unlisted falls back to default-src, so omitting it makes the browser block
|
||||
// every upload before it reaches the network. See TestAppCSPAllowsWhatThePageDoes.
|
||||
const appCSP = "default-src 'none'; script-src 'self'; style-src 'self'; " +
|
||||
"img-src 'self' data:; connect-src 'self'; form-action 'self'; " +
|
||||
"base-uri 'none'; frame-ancestors 'none'"
|
||||
|
||||
@@ -110,7 +110,7 @@ func (h *harness) upload(t *testing.T, body []byte, headers map[string]string) *
|
||||
|
||||
func (h *harness) uploadReader(t *testing.T, body io.Reader, headers map[string]string) *http.Response {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest("POST", h.ts.URL+"/api/upload", body)
|
||||
req, err := http.NewRequest("POST", h.ts.URL+"/upload", body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -416,7 +416,7 @@ func TestPathTraversalIsRejected(t *testing.T) {
|
||||
|
||||
func TestReservedNamesAreRejected(t *testing.T) {
|
||||
h := newHarness(t, nil)
|
||||
for _, name := range []string{"api", "static", "d", "i", "robots.txt"} {
|
||||
for _, name := range []string{"api", "static", "d", "i", "upload", "robots.txt"} {
|
||||
resp := h.upload(t, []byte("x"), map[string]string{
|
||||
"Vanity": name,
|
||||
"Authorization": "Bearer " + h.token,
|
||||
@@ -434,7 +434,7 @@ func TestDeleteRequiresTheRightToken(t *testing.T) {
|
||||
|
||||
del := func(token string) int {
|
||||
form := strings.NewReader("token=" + token)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := h.ts.Client().Do(req)
|
||||
@@ -465,7 +465,7 @@ func TestAdminMayDeleteAnything(t *testing.T) {
|
||||
h := newHarness(t, nil)
|
||||
res := decode[uploadResult](t, h.upload(t, []byte("someone else's"), nil))
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", nil)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+h.admin)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := h.ts.Client().Do(req)
|
||||
@@ -494,7 +494,7 @@ func TestMultipartUpload(t *testing.T) {
|
||||
fw.Write([]byte("hello from a browser"))
|
||||
mw.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := h.ts.Client().Do(req)
|
||||
@@ -523,7 +523,7 @@ func TestFormPostRendersHTML(t *testing.T) {
|
||||
fw.Write([]byte("data"))
|
||||
mw.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml")
|
||||
resp, err := h.ts.Client().Do(req)
|
||||
@@ -693,7 +693,7 @@ func TestLoginStoresTheToken(t *testing.T) {
|
||||
}
|
||||
|
||||
// The session alone is now enough to claim a custom name.
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", strings.NewReader("two"))
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", strings.NewReader("two"))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Vanity", "session-upload")
|
||||
req.AddCookie(cookie)
|
||||
@@ -885,7 +885,7 @@ func TestCookieDoesNotShadowTheDeleteToken(t *testing.T) {
|
||||
res := decode[uploadResult](t, h.upload(t, []byte("x"), nil))
|
||||
|
||||
form := strings.NewReader("token=" + res.DeleteToken)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: h.token})
|
||||
@@ -929,7 +929,7 @@ func (h *harness) formUploadWith(t *testing.T, cookie *http.Cookie, fields map[s
|
||||
fw.Write([]byte(content))
|
||||
mw.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if cookie != nil {
|
||||
@@ -1050,7 +1050,7 @@ func TestResultPageOffersBothLinksAndAWayBack(t *testing.T) {
|
||||
fw.Write([]byte("data"))
|
||||
mw.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Accept", "text/html")
|
||||
resp, err := h.ts.Client().Do(req)
|
||||
@@ -1223,7 +1223,7 @@ func TestAdminDeleteReturnsToTheTable(t *testing.T) {
|
||||
client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }
|
||||
|
||||
form := strings.NewReader("from=admin")
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "text/html")
|
||||
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: h.admin})
|
||||
@@ -1250,7 +1250,7 @@ func TestAdminDeleteStillRequiresAdmin(t *testing.T) {
|
||||
res := decode[uploadResult](t, h.upload(t, []byte("not yours"), nil))
|
||||
|
||||
form := strings.NewReader("from=admin")
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/d/"+res.ID+"/delete", form)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/d/"+res.ID+"/delete", form)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.AddCookie(&http.Cookie{Name: tokenCookie, Value: h.token})
|
||||
@@ -1305,7 +1305,7 @@ func TestMultipartHonoursTheHeaderForm(t *testing.T) {
|
||||
fw.Write([]byte("payload"))
|
||||
mw.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+h.token)
|
||||
@@ -1339,7 +1339,7 @@ func TestFormFieldsOverrideTheHeaders(t *testing.T) {
|
||||
fw.Write([]byte("payload"))
|
||||
mw.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", &body)
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", &body)
|
||||
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+h.token)
|
||||
@@ -1399,7 +1399,7 @@ func TestInfoPageAcceptsTheDeleteToken(t *testing.T) {
|
||||
t.Fatal("the info page leaks the delete token to anyone holding the link")
|
||||
}
|
||||
|
||||
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
|
||||
resp := h.postForm(t, "/d/"+res.ID+"/delete",
|
||||
url.Values{"from": {"info"}, "token": {res.DeleteToken}}, nil)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -1417,7 +1417,7 @@ func TestWrongDeleteTokenReturnsToTheInfoPage(t *testing.T) {
|
||||
res := decode[uploadResult](t, h.upload(t, []byte("x"), map[string]string{
|
||||
"Content-Disposition": `attachment; filename="keepme.bin"`}))
|
||||
|
||||
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
|
||||
resp := h.postForm(t, "/d/"+res.ID+"/delete",
|
||||
url.Values{"from": {"info"}, "token": {"wrong"}}, nil)
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
@@ -1473,7 +1473,7 @@ func TestInfoPageOffersADirectButtonToAnOwner(t *testing.T) {
|
||||
}
|
||||
|
||||
// And that button actually works with no token field at all.
|
||||
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
|
||||
resp := h.postForm(t, "/d/"+res.ID+"/delete",
|
||||
url.Values{"from": {"info"}}, &http.Cookie{Name: tokenCookie, Value: h.token})
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -1494,7 +1494,7 @@ func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
|
||||
if last != nil {
|
||||
last.Body.Close()
|
||||
}
|
||||
last = h.postForm(t, "/api/d/"+res.ID+"/delete",
|
||||
last = h.postForm(t, "/d/"+res.ID+"/delete",
|
||||
url.Values{"from": {"info"}, "token": {"guess"}}, nil)
|
||||
}
|
||||
if last.StatusCode != http.StatusTooManyRequests {
|
||||
@@ -1503,7 +1503,7 @@ func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
|
||||
last.Body.Close()
|
||||
|
||||
// The real token still works, having consumed nothing from the bucket.
|
||||
resp := h.postForm(t, "/api/d/"+res.ID+"/delete",
|
||||
resp := h.postForm(t, "/d/"+res.ID+"/delete",
|
||||
url.Values{"from": {"info"}, "token": {res.DeleteToken}}, nil)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -1520,7 +1520,7 @@ func TestFailedDeletesAreThrottledAndSuccessIsNot(t *testing.T) {
|
||||
func TestCrossOriginPostsAreRejected(t *testing.T) {
|
||||
h := newHarness(t, nil)
|
||||
|
||||
paths := []string{"/login", "/logout", "/api/upload", "/api/d/anything/delete"}
|
||||
paths := []string{"/login", "/logout", "/upload", "/d/anything/delete"}
|
||||
hostile := []map[string]string{
|
||||
{"Origin": "https://evil.example.com"},
|
||||
{"Sec-Fetch-Site": "cross-site"},
|
||||
@@ -1746,7 +1746,7 @@ func TestChosenPassphraseWorksEndToEnd(t *testing.T) {
|
||||
t.Fatal("no session was started")
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/api/upload", strings.NewReader("x"))
|
||||
req, _ := http.NewRequest("POST", h.ts.URL+"/upload", strings.NewReader("x"))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Vanity", "chosen-upload")
|
||||
req.AddCookie(cookie)
|
||||
|
||||
@@ -405,7 +405,7 @@ func (s *Server) respondUploaded(w http.ResponseWriter, r *http.Request, m *stor
|
||||
URL: url,
|
||||
InfoURL: s.absBase(r) + "i/" + m.ID,
|
||||
DeleteToken: secret,
|
||||
DeleteURL: s.absBase(r) + "api/d/" + m.ID + "/delete",
|
||||
DeleteURL: s.absBase(r) + "d/" + m.ID + "/delete",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ var vanityRe = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{1,63}$`)
|
||||
// allowed into the object namespace.
|
||||
var reserved = map[string]bool{
|
||||
"d": true, "i": true, "api": true, "static": true, "admin": true,
|
||||
"login": true, "logout": true,
|
||||
"login": true, "logout": true, "upload": true,
|
||||
"favicon.ico": true, "robots.txt": true, "index.html": true,
|
||||
"sitemap.xml": true, "tokens.json": true, "objects": true,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
--line: #dcdcd5;
|
||||
--accent: #2f6f4f;
|
||||
--accent-fg: #ffffff;
|
||||
/* Red reads as text and as a button surface, and one value cannot do both:
|
||||
see the pair below, kept in step per theme. */
|
||||
--danger: #9b2c2c;
|
||||
--danger-bg: #991b1b;
|
||||
--danger-fg: #ffffff;
|
||||
--warn-bg: #fdf6e3;
|
||||
--radius: 10px;
|
||||
}
|
||||
@@ -23,6 +27,8 @@
|
||||
--accent: #5fae86;
|
||||
--accent-fg: #10241a;
|
||||
--danger: #e07070;
|
||||
--danger-bg: #d32f2f;
|
||||
--danger-fg: #ffffff;
|
||||
--warn-bg: #2a2418;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +118,7 @@ button, .button {
|
||||
text-decoration: none;
|
||||
}
|
||||
button:disabled { opacity: .55; cursor: default; }
|
||||
button.danger { background: var(--danger); color: #fff; }
|
||||
button.danger { background: var(--danger-bg); color: var(--danger-fg); }
|
||||
|
||||
.progress .bar {
|
||||
height: .5rem;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<td data-label="Uploaded" class="nowrap" title="{{.Created}}">{{.CreatedAgo}}</td>
|
||||
<td data-label="Expires" class="nowrap" title="{{.Expires}}">{{.ExpiresIn}}</td>
|
||||
<td class="actions-cell">
|
||||
<form method="post" action="{{$.Base}}api/d/{{.ID}}/delete">
|
||||
<form method="post" action="{{$.Base}}d/{{.ID}}/delete">
|
||||
<input type="hidden" name="from" value="admin">
|
||||
<button type="submit" class="danger small"
|
||||
data-confirm="Delete {{.Filename}}? This cannot be undone.">Delete</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<p class="notice">Your login is no longer valid, that token has been removed. You have been logged out.</p>
|
||||
{{end}}
|
||||
|
||||
<form id="upload" class="card" method="post" action="{{.Base}}api/upload"
|
||||
<form id="upload" class="card" method="post" action="{{.Base}}upload"
|
||||
enctype="multipart/form-data" data-max-size="{{.MaxSizeBytes}}">
|
||||
|
||||
<!-- Field order is load-bearing: the server streams this body rather than
|
||||
@@ -68,6 +68,6 @@
|
||||
-H 'Authorization: Bearer <token>' \
|
||||
-H 'Vanity: my-file' \
|
||||
-H 'Expiry: 3d' \
|
||||
{{.AbsBase}}api/upload</code></pre>
|
||||
{{.AbsBase}}upload</code></pre>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
{{if .CanDelete}}
|
||||
<p class="hint">Your token can remove this file.</p>
|
||||
<form method="post" action="{{.Base}}api/d/{{.Meta.ID}}/delete">
|
||||
<form method="post" action="{{.Base}}d/{{.Meta.ID}}/delete">
|
||||
<input type="hidden" name="from" value="info">
|
||||
<button type="submit" class="danger"
|
||||
data-confirm="Delete {{.Meta.Filename}}? This cannot be undone.">Delete this file</button>
|
||||
@@ -36,7 +36,7 @@
|
||||
Paste the delete token you were given when this file was uploaded.
|
||||
It is the only way to remove a file early without an owning token.
|
||||
</p>
|
||||
<form method="post" action="{{.Base}}api/d/{{.Meta.ID}}/delete">
|
||||
<form method="post" action="{{.Base}}d/{{.Meta.ID}}/delete">
|
||||
<input type="hidden" name="from" value="info">
|
||||
<label class="field">
|
||||
<span>Delete token</span>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
expires.
|
||||
</p>
|
||||
<p class="mono wrap">{{.DeleteToken}}</p>
|
||||
<form method="post" action="{{.Base}}api/d/{{.Meta.ID}}/delete">
|
||||
<form method="post" action="{{.Base}}d/{{.Meta.ID}}/delete">
|
||||
<input type="hidden" name="token" value="{{.DeleteToken}}">
|
||||
<button type="submit" class="danger"
|
||||
data-confirm="Delete {{.Meta.Filename}}? This cannot be undone.">Delete it now</button>
|
||||
|
||||
Reference in New Issue
Block a user