mirror of
https://github.com/acaloiaro/garbagespeak.com
synced 2026-07-21 10:12:19 +00:00
Allow htmx to send cookies/switch to chai routing
This commit is contained in:
parent
d219961391
commit
00174d260c
5 changed files with 128 additions and 44 deletions
20
env.sample
Normal file
20
env.sample
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# The domain/hostname:port where the Hugo site is available
|
||||
SITE_DOMAN=<SITE_DOMAN>
|
||||
|
||||
# The domain/hostname:port where the PAI server is available
|
||||
API_HOST=<API_HOST>
|
||||
|
||||
# The Postgres URL, e.g. postgresql://postgres:postgres@localhost:5432/garbage_speak?sslomode=disable
|
||||
POSTGRES_URL=<POSTGRES_URL>
|
||||
|
||||
# The hostname from which to send email
|
||||
SMTP_HOST=<SMTP_HOST>
|
||||
|
||||
# The username with which to connect to SMTP_HOST
|
||||
SMTP_USERNAME=<SMTP_USERNAME>
|
||||
|
||||
# The password with which to connect to SMTP_HOST
|
||||
SMTP_PASSWORD=<SMTP_PASSWORD>
|
||||
|
||||
# The email address from which emails are sent
|
||||
SMTP_FROM_ADDRESS=<SMTP_FROM_ADDRESS>
|
||||
2
go.mod
2
go.mod
|
|
@ -17,6 +17,8 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.0.10 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.0.0 // indirect
|
||||
github.com/guregu/null v4.0.0+incompatible // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -19,6 +19,10 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
|||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
|
||||
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
|
||||
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
<script type="text/javascript" src="/htmx.js"></script>
|
||||
<meta name="htmx-config" content='{"withCredentials": "true"}'>
|
||||
|
|
|
|||
145
server.go
145
server.go
|
|
@ -6,7 +6,6 @@ import (
|
|||
"crypto/tls"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
|
@ -24,6 +23,9 @@ import (
|
|||
"github.com/alexedwards/scs/pgxstore"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/georgysavva/scany/v2/pgxscan"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
|
|
@ -69,11 +71,6 @@ type UserEmailVerification struct {
|
|||
//go:embed migrations/*.sql
|
||||
var migrations embed.FS
|
||||
|
||||
// Embed all hugo output as 'public'
|
||||
//
|
||||
//go:embed all:public
|
||||
var content embed.FS
|
||||
|
||||
var db *pgxpool.Pool
|
||||
var sessions *scs.SessionManager
|
||||
var sessionStore *pgxstore.PostgresStore
|
||||
|
|
@ -113,7 +110,14 @@ func init() {
|
|||
}
|
||||
|
||||
sessions = scs.New()
|
||||
sessionStore = pgxstore.NewWithCleanupInterval(db, 10*time.Second)
|
||||
sessionStore = pgxstore.New(db)
|
||||
|
||||
sessions.Cookie.Name = "session_id"
|
||||
//sessions.Cookie.Domain = siteDomain()
|
||||
//sessions.Cookie.HttpOnly = true
|
||||
//sessions.Cookie.Persist = true
|
||||
sessions.Cookie.SameSite = http.SameSiteNoneMode
|
||||
//sessions.Cookie.Secure = true
|
||||
sessions.Store = sessionStore
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -139,22 +143,72 @@ func main() {
|
|||
defer sessionStore.StopCleanup()
|
||||
defer NQ.Shutdown(context.Background())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
serverRoot, _ := fs.Sub(content, "public")
|
||||
|
||||
// Serve all hugo content (the 'public' directory) at the root url
|
||||
mux.Handle("/", http.FileServer(http.FS(serverRoot)))
|
||||
r := chi.NewRouter()
|
||||
//r.Use(sessions.LoadAndSave)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
AllowCredentials: true,
|
||||
AllowedOrigins: []string{"http://localhost:1313", fmt.Sprintf("https://%s", os.Getenv("SITE_DOMAIN"))},
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "hx-target", "hx-current-url", "hx-request"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}))
|
||||
|
||||
// Add any number of handlers for custom endpoints here
|
||||
mux.HandleFunc("/garbage_bin", cors(http.HandlerFunc(garbageBin)))
|
||||
mux.HandleFunc("/users/create", cors(http.HandlerFunc(createAccount)))
|
||||
r.Get("/garbage_bin", garbageBin)
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.Post("/create", createAccount)
|
||||
r.Get("/email_verification/{uev_id}", emailVerification)
|
||||
})
|
||||
|
||||
fmt.Printf("Starting API server on port 1314\n")
|
||||
if err := http.ListenAndServe("0.0.0.0:1314", sessions.LoadAndSave(mux)); err != nil {
|
||||
if err := http.ListenAndServe("0.0.0.0:1314", sessions.LoadAndSave(r)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// emailVerification takes a UserEmailVerification ID, and if it exists, verifies the associated User account by
|
||||
// deleting the UEV record (user login's check if a UEV exists for a User before permitting login)
|
||||
func emailVerification(w http.ResponseWriter, r *http.Request) {
|
||||
// user email verification id
|
||||
uevID := chi.URLParam(r, "uev_id")
|
||||
|
||||
tx, err := db.Begin(r.Context())
|
||||
if err != nil {
|
||||
ise(err, w)
|
||||
return
|
||||
}
|
||||
// Rollback is safe to call even if the tx is already closed, so if
|
||||
// the tx commits successfully, this is a no-op
|
||||
defer tx.Rollback(r.Context())
|
||||
|
||||
var userID string
|
||||
err = tx.QueryRow(r.Context(), "DELETE FROM user_email_verifications WHERE id = $1 RETURNING user_id", uevID).Scan(&userID)
|
||||
if err != nil {
|
||||
log.Fatalf("can't verify email address: %v", err)
|
||||
}
|
||||
|
||||
err = sessions.RenewToken(r.Context())
|
||||
if err != nil {
|
||||
ise(err, w)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("User ID:", userID)
|
||||
// create the user's session
|
||||
sessions.Put(r.Context(), "userID", userID)
|
||||
tx.Commit(r.Context())
|
||||
|
||||
userIDFromSession := sessions.GetString(r.Context(), "userID")
|
||||
log.Println("id from session before redir", userIDFromSession)
|
||||
|
||||
http.Redirect(w, r, baseURL(), http.StatusFound)
|
||||
}
|
||||
|
||||
// createAccount creates new accounts
|
||||
func createAccount(w http.ResponseWriter, r *http.Request) {
|
||||
// The name is not in the query param, let's see if it was submitted as a form
|
||||
|
|
@ -222,7 +276,7 @@ func createAccount(w http.ResponseWriter, r *http.Request) {
|
|||
Queue: "welcome_email",
|
||||
Payload: map[string]interface{}{
|
||||
"recipient": email,
|
||||
"verification_url": fmt.Sprintf("%s/users/email_verification/%s", baseURL(), uevID),
|
||||
"verification_url": fmt.Sprintf("%s/users/email_verification/%s", apiURL(), uevID),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -238,7 +292,7 @@ func garbageBin(w http.ResponseWriter, r *http.Request) {
|
|||
if name == "null" || name == "" {
|
||||
name = "World"
|
||||
}
|
||||
sessions.Put(r.Context(), "message", "Hello from a session!")
|
||||
|
||||
tmpl := template.Must(template.ParseFiles("partials/posts.html"))
|
||||
var buff = bytes.NewBufferString("")
|
||||
ctx := context.Background()
|
||||
|
|
@ -253,36 +307,15 @@ func garbageBin(w http.ResponseWriter, r *http.Request) {
|
|||
ise(err, w)
|
||||
return
|
||||
}
|
||||
msg := sessions.GetString(r.Context(), "message")
|
||||
w.Header().Add("X-Message", msg)
|
||||
|
||||
userIDFromSession := sessions.GetString(r.Context(), "userID")
|
||||
log.Println("id from session after redir", userIDFromSession)
|
||||
|
||||
log.Println("foobar key contains", sessions.GetString(r.Context(), "foobar"))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(buff.Bytes())
|
||||
}
|
||||
|
||||
func ise(err error, w http.ResponseWriter) {
|
||||
fmt.Fprintf(w, "error: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// cors middleware
|
||||
func cors(h http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// in development, the Origin is the the Hugo server, i.e. http://localhost:1313
|
||||
// but in production, it is the domain name where one's site is deployed
|
||||
//
|
||||
// CHANGE THIS: You likely do not want to allow any origin (*) in production. The value should be the base URL of
|
||||
// where your static content is served
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, hx-target, hx-current-url, hx-request")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// sendWelcomeEmail sends an email to recipient containing a special URL that only that can know, for the purpose of
|
||||
// email address verification
|
||||
func sendWelcomeEmail(recipient, verificationURL, siteName string) error {
|
||||
|
|
@ -389,7 +422,7 @@ func env() string {
|
|||
return "development"
|
||||
}
|
||||
|
||||
// baseURL returns the server's base URL, e.g. http://localhost:1313 in development, https://<SITE_DOMAIN> in production
|
||||
// baseURL returns the site's base URL, e.g. http://localhost:1313 in development, https://<SITE_DOMAIN> in production
|
||||
func baseURL() string {
|
||||
if env() == "development" && siteDomain() == "" {
|
||||
addr := os.Getenv("HOSTNAME")
|
||||
|
|
@ -406,3 +439,27 @@ func baseURL() string {
|
|||
return fmt.Sprintf("https://%s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// apiURL returns the server's base URL, e.g. http://localhost:1314 in development, https://<API_HOST> in production
|
||||
func apiURL() string {
|
||||
if env() == "development" && siteDomain() == "" {
|
||||
addr := os.Getenv("HOSTNAME")
|
||||
if addr == "" {
|
||||
addr = "localhost"
|
||||
}
|
||||
return fmt.Sprintf("http://%s:1314", addr)
|
||||
} else {
|
||||
addr := os.Getenv("API_HOST")
|
||||
if addr == "" {
|
||||
addr = siteDomain()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s", addr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ise(err error, w http.ResponseWriter) {
|
||||
fmt.Fprintf(w, "error: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue