feat: implement Phase 1 (auth) and Phase 2 (faves CRUD) foundation
Go backend with server-rendered HTML/HTMX frontend, SQLite database, and filesystem image storage. Self-hostable single-binary architecture. Phase 1 — Authentication & project foundation: - Argon2id password hashing with timing-attack prevention - Session management with cookie-based auth and periodic cleanup - Login, signup (open/requests/closed modes), logout, forced password reset - CSRF double-submit cookie pattern with HTMX auto-inclusion - Proxy-aware real IP extraction (WireGuard/Tailscale support) - Configurable base path for subdomain and subpath deployment - Rate limiting on auth endpoints with background cleanup - Security headers (CSP, X-Frame-Options, Referrer-Policy) - Structured logging with slog, graceful shutdown - Pico CSS + HTMX vendored and embedded via go:embed Phase 2 — Faves CRUD with tags and images: - Full CRUD for favorites with ownership checks - Image upload with EXIF stripping, resize to 1920px, UUID filenames - Tag system with HTMX autocomplete (prefix search, popularity-sorted) - Privacy controls (public/private per fave, user-configurable default) - Tag browsing, pagination, batch tag loading (avoids N+1) - OpenGraph meta tags on public fave detail pages Includes code quality pass: extracted shared helpers, fixed signup request persistence bug, plugged rate limiter memory leak, removed dead code, and logged previously-swallowed errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
fc1f7259c5
52 changed files with 5459 additions and 0 deletions
92
web/templates/pages/fave_form.html
Normal file
92
web/templates/pages/fave_form.html
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
{{define "content"}}
|
||||
<article>
|
||||
{{with .Data}}
|
||||
{{if .IsNew}}
|
||||
<h1>Ny favoritt</h1>
|
||||
{{else}}
|
||||
<h1>Rediger favoritt</h1>
|
||||
{{end}}
|
||||
|
||||
<form method="POST"
|
||||
enctype="multipart/form-data"
|
||||
{{if .IsNew}}
|
||||
action="{{basePath}}/faves"
|
||||
{{else}}
|
||||
action="{{basePath}}/faves/{{.Fave.ID}}"
|
||||
{{end}}>
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
|
||||
|
||||
<label for="description">
|
||||
Beskrivelse *
|
||||
<input type="text" id="description" name="description"
|
||||
value="{{.Description}}" required autofocus
|
||||
placeholder="F.eks. «Blade Runner 2049» eller «Rød tulipan»">
|
||||
</label>
|
||||
|
||||
<label for="url">
|
||||
Lenke (valgfri)
|
||||
<input type="url" id="url" name="url"
|
||||
value="{{.URL}}"
|
||||
placeholder="https://...">
|
||||
</label>
|
||||
|
||||
<label for="image">
|
||||
Bilde (valgfri)
|
||||
<input type="file" id="image" name="image"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp">
|
||||
</label>
|
||||
{{if not .IsNew}}
|
||||
{{if .Fave.ImagePath}}
|
||||
<div class="current-image">
|
||||
<img src="{{basePath}}/uploads/{{.Fave.ImagePath}}"
|
||||
alt="Nåværende bilde"
|
||||
style="max-width: 200px; max-height: 200px;">
|
||||
<label>
|
||||
<input type="checkbox" name="remove_image" value="1">
|
||||
Fjern bilde
|
||||
</label>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<label for="tags">
|
||||
Merkelapper (kommaseparert)
|
||||
<input type="text" id="tags" name="tags"
|
||||
value="{{.Tags}}"
|
||||
placeholder="film, sci-fi, favoritt"
|
||||
autocomplete="off"
|
||||
hx-get="{{basePath}}/tags/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#tag-suggestions"
|
||||
hx-params="*"
|
||||
hx-vals='{"q": ""}'
|
||||
aria-describedby="tags-help"
|
||||
aria-autocomplete="list"
|
||||
aria-controls="tag-suggestions">
|
||||
<small id="tags-help">Skriv for å søke i eksisterende merkelapper. Maks {{maxTags}} stk.</small>
|
||||
</label>
|
||||
<ul id="tag-suggestions" role="listbox" class="tag-suggestions" aria-label="Merkelappforslag"></ul>
|
||||
|
||||
<fieldset>
|
||||
<legend>Synlighet</legend>
|
||||
<label>
|
||||
<input type="radio" name="privacy" value="public"
|
||||
{{if eq .Privacy "public"}}checked{{end}}
|
||||
{{if .IsNew}}{{if eq .DefaultPrivacy "public"}}checked{{end}}{{end}}>
|
||||
Offentlig
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="privacy" value="private"
|
||||
{{if eq .Privacy "private"}}checked{{end}}
|
||||
{{if .IsNew}}{{if eq .DefaultPrivacy "private"}}checked{{end}}{{end}}>
|
||||
Privat
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">
|
||||
{{if .IsNew}}Legg til{{else}}Lagre{{end}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue