tuxedo notes: photo upload, username/email display + confirm-email, favicon; verified owner-only edit

This commit is contained in:
2026-08-07 19:46:08 +10:00
parent f7e018cf64
commit 0ee211d156
10 changed files with 105 additions and 4 deletions

View File

@@ -26,6 +26,10 @@ func (a *App) Register(w http.ResponseWriter, r *http.Request) {
a.render(w, r, "register", "Create account", nil, "Email and password are required.") a.render(w, r, "register", "Create account", nil, "Email and password are required.")
return return
} }
if confirm := strings.ToLower(strings.TrimSpace(r.FormValue("confirm_email"))); confirm != "" && confirm != email {
a.render(w, r, "register", "Create account", nil, "Emails do not match.")
return
}
if len(password) < 8 { if len(password) < 8 {
a.render(w, r, "register", "Create account", nil, "Password must be at least 8 characters.") a.render(w, r, "register", "Create account", nil, "Password must be at least 8 characters.")
return return

View File

@@ -2,7 +2,11 @@ package handlers
import ( import (
"errors" "errors"
"fmt"
"io"
"net/http" "net/http"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -169,6 +173,73 @@ func (a *App) loadOwnedTag(w http.ResponseWriter, r *http.Request, uid int64) (d
return tag, true return tag, true
} }
// UploadPhoto saves an uploaded image for a tag and sets its photo_url (HTMX).
func (a *App) UploadPhoto(w http.ResponseWriter, r *http.Request) {
uid, _ := auth.GetUserID(r)
tag, ok := a.loadOwnedTag(w, r, uid)
if !ok {
return
}
if err := r.ParseMultipartForm(5 << 20); err != nil { // 5 MB
writeUploadMsg(w, "Upload too large (max 5 MB).")
return
}
file, header, err := r.FormFile("photo")
if err != nil {
writeUploadMsg(w, "Please choose an image file.")
return
}
defer file.Close()
if header.Size > 5<<20 {
writeUploadMsg(w, "Upload too large (max 5 MB).")
return
}
if !strings.HasPrefix(header.Header.Get("Content-Type"), "image/") {
writeUploadMsg(w, "Only image files are allowed.")
return
}
// Sanitise the filename and store under uploads/.
ext := strings.ToLower(filepath.Ext(header.Filename))
if ext == "" {
ext = ".jpg"
}
dst := filepath.Join("static", "uploads", fmt.Sprintf("tag%d%s", tag.ID, ext))
out, err := os.Create(dst)
if err != nil {
writeUploadMsg(w, "Could not save the photo.")
return
}
defer out.Close()
if _, err := io.Copy(out, file); err != nil {
writeUploadMsg(w, "Could not save the photo.")
return
}
photoURL := "/static/uploads/" + filepath.Base(dst)
if _, err := a.Queries.UpdateTagDetails(r.Context(), db.UpdateTagDetailsParams{
ID: tag.ID,
PhotoUrl: pgtype.Text{String: photoURL, Valid: true},
// Preserve existing values: re-send them via the form is not possible here,
// so use the stored tag fields for everything else.
ItemType: tag.ItemType, Description: tag.Description, Phone: tag.Phone,
Address: tag.Address, Notes: tag.Notes, SmsEnabled: tag.SmsEnabled,
}); err != nil {
writeUploadMsg(w, "Could not update the tag.")
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<p class="mt-2 text-xs text-green-700">Photo uploaded ✓ <a href="%s" class="underline">view</a> — refresh to see it on the tag page.</p>`, photoURL)
}
func writeUploadMsg(w http.ResponseWriter, msg string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<p class="mt-2 text-xs text-red-600">%s</p>`, msg)
}
func textOrNil(s string) pgtype.Text { func textOrNil(s string) pgtype.Text {
return pgtype.Text{String: s, Valid: s != ""} return pgtype.Text{String: s, Valid: s != ""}
} }

View File

@@ -73,6 +73,7 @@ func main() {
mux.Handle("GET /account/tags/{id}/edit/inline", app.RequireAuth(app.EditInlinePage)) mux.Handle("GET /account/tags/{id}/edit/inline", app.RequireAuth(app.EditInlinePage))
mux.Handle("POST /account/tags/{id}/edit", app.RequireAuth(app.EditTag)) mux.Handle("POST /account/tags/{id}/edit", app.RequireAuth(app.EditTag))
mux.Handle("POST /account/tags/{id}/delete", app.RequireAuth(app.DeleteTag)) mux.Handle("POST /account/tags/{id}/delete", app.RequireAuth(app.DeleteTag))
mux.Handle("POST /account/tags/{id}/photo", app.RequireAuth(app.UploadPhoto))
srv := &http.Server{ srv := &http.Server{
Addr: addr, Addr: addr,

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#1c1917"/>
<text x="32" y="44" font-size="36" text-anchor="middle">🐕</text>
</svg>

After

Width:  |  Height:  |  Size: 194 B

View File

View File

@@ -0,0 +1,2 @@
‰PNG


After

Width:  |  Height:  |  Size: 8 B

View File

@@ -4,6 +4,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<title>{{.Title}} · Where Woof !</title> <title>{{.Title}} · Where Woof !</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <script src="https://unpkg.com/htmx.org@1.9.12"></script>
@@ -24,7 +25,10 @@
<a href="/" class="text-lg font-bold tracking-tight">🐕 Where Woof</a> <a href="/" class="text-lg font-bold tracking-tight">🐕 Where Woof</a>
<div class="flex items-center gap-4 text-sm"> <div class="flex items-center gap-4 text-sm">
{{if .CurrentUser}} {{if .CurrentUser}}
<span class="text-stone-300">Hi, {{.CurrentUser.Name}}</span> <div class="text-right leading-tight">
<span class="text-stone-300">Hi, {{.CurrentUser.Name}}</span>
<span class="block text-xs text-stone-500" title="Your username / email">{{.CurrentUser.Email}}</span>
</div>
<a href="/account" class="hover:underline">My Tags</a> <a href="/account" class="hover:underline">My Tags</a>
<form method="post" action="/logout" class="inline"> <form method="post" action="/logout" class="inline">
<button class="hover:underline">Log out</button> <button class="hover:underline">Log out</button>

View File

@@ -11,9 +11,13 @@
<input id="name" name="name" type="text" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required> <input id="name" name="name" type="text" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
</div> </div>
<div> <div>
<label class="block text-sm font-medium" for="email">Email</label> <label class="block text-sm font-medium" for="email">Email (your username)</label>
<input id="email" name="email" type="email" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required> <input id="email" name="email" type="email" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
</div> </div>
<div>
<label class="block text-sm font-medium" for="confirm_email">Confirm email</label>
<input id="confirm_email" name="confirm_email" type="email" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" required>
</div>
<div> <div>
<label class="block text-sm font-medium" for="password">Password</label> <label class="block text-sm font-medium" for="password">Password</label>
<input id="password" name="password" type="password" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" minlength="8" required> <input id="password" name="password" type="password" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" minlength="8" required>

View File

@@ -25,8 +25,15 @@
<textarea name="description" rows="3" class="mt-1 w-full rounded border border-stone-300 px-3 py-2">{{if .Data.Description.Valid}}{{.Data.Description.String}}{{end}}</textarea> <textarea name="description" rows="3" class="mt-1 w-full rounded border border-stone-300 px-3 py-2">{{if .Data.Description.Valid}}{{.Data.Description.String}}{{end}}</textarea>
</div> </div>
<div> <div>
<label class="block text-sm font-medium">Photo URL</label> <label class="block text-sm font-medium">Photo</label>
<input name="photo_url" type="url" value="{{if .Data.PhotoUrl.Valid}}{{.Data.PhotoUrl.String}}{{end}}" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" placeholder="https://…"> {{if .Data.PhotoUrl.Valid}}<img src="{{.Data.PhotoUrl.String}}" alt="Current photo" class="mb-2 h-32 w-32 rounded-lg object-cover">{{end}}
<input name="photo_url" type="url" value="{{if .Data.PhotoUrl.Valid}}{{.Data.PhotoUrl.String}}{{end}}" class="mt-1 w-full rounded border border-stone-300 px-3 py-2" placeholder="https://… or upload below">
<form hx-encoding="multipart/form-data" hx-post="/account/tags/{{.Data.ID}}/photo" hx-target="#upload-area" hx-swap="innerHTML" class="mt-2 flex gap-2" id="upload-area">
<input type="file" name="photo" accept="image/*" class="text-sm">
<button type="submit" class="rounded bg-stone-200 px-3 py-1 text-xs font-medium hover:bg-stone-300">Upload</button>
<span class="htmx-indicator self-center text-xs text-stone-400">Uploading…</span>
</form>
<p class="mt-1 text-xs text-stone-500">Upload a photo, or paste a URL above.</p>
</div> </div>
<div> <div>
<label class="block text-sm font-medium">Phone number (shown to finders)</label> <label class="block text-sm font-medium">Phone number (shown to finders)</label>

View File

@@ -9,3 +9,7 @@ x 2026-08-05 Set up Gitea for where_woof +project:where-woof +agent:where_woof
x 2026-08-07 (B) Scan-flow: re-alert when different finder phone within 250 m window +project:where-woof +agent:right-monitor-pi-coding x 2026-08-07 (B) Scan-flow: re-alert when different finder phone within 250 m window +project:where-woof +agent:right-monitor-pi-coding
x 2026-08-07 (B) Scan-flow: browser fingerprint + 24 h block, store in DB +project:where-woof +agent:right-monitor-pi-coding x 2026-08-07 (B) Scan-flow: browser fingerprint + 24 h block, store in DB +project:where-woof +agent:right-monitor-pi-coding
x 2026-08-07 (B) Preset tag-ID registry: seed real manufactured IDs, enforce registry-only (anti-scam) +project:where-woof +agent:right-monitor-pi-coding x 2026-08-07 (B) Preset tag-ID registry: seed real manufactured IDs, enforce registry-only (anti-scam) +project:where-woof +agent:right-monitor-pi-coding
(A) Photo +project:where-woof +issues:htmx
(A) Confirm Username and See username +project:where-woof +issues:htmx
(A) Favicon +project:where-woof +issues:htmx
(A) Edit this tags details allowed me to edit +project:where-woof +issues:htmx