Compare commits

...

11 Commits

7 changed files with 83 additions and 19 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
.aider* .aider*
golias golias
store.json

View File

@ -1,3 +1,4 @@
{{define "main"}}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -7,6 +8,8 @@
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script> <script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
</head> </head>
<body> <body>
<div id=ancestor>
{{template "login"}}
<h1>Word Color Cards</h1> <h1>Word Color Cards</h1>
<div style="display: flex; gap: 1rem; flex-wrap: wrap; padding: 1rem;"> <div style="display: flex; gap: 1rem; flex-wrap: wrap; padding: 1rem;">
{{range $word, $color := .}} {{range $word, $color := .}}
@ -18,11 +21,13 @@
min-width: 100px; min-width: 100px;
text-align: center; text-align: center;
color: white; color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.25); text-shadow: 0 2px 4px rgba(0,0,0,0.8);
"> ">
{{$word}} {{$word}}
</div> </div>
{{end}} {{end}}
</div> </div>
</div>
</body> </body>
</html> </html>
{{end}}

16
components/login.html Normal file
View File

@ -0,0 +1,16 @@
{{define "login"}}
<div id="logindiv">
<form class="space-y-6" hx-post="/login" hx-target="#ancestor">
<div>
<label For="username" class="block text-sm font-medium leading-6 text-white-900">tell us your username</label>
<div class="mt-2">
<input id="username" name="username" hx-target="#login_notice" hx-swap="outerHTML" hx-post="/check/name" hx-trigger="input changed delay:400ms" autocomplete="username" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6 text-center"/>
</div>
<div id="login_notice">this name looks available</div>
</div>
<div>
<button type="submit" class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Sign in</button>
</div>
</form>
</div>
{{end}}

View File

@ -8,7 +8,6 @@ import (
"golias/models" "golias/models"
"golias/utils" "golias/utils"
"html/template" "html/template"
"log/slog"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -24,7 +23,7 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) {
username := r.PostFormValue("username") username := r.PostFormValue("username")
if username == "" { if username == "" {
msg := "username not provided" msg := "username not provided"
slog.Error(msg) log.Error(msg)
abortWithError(w, msg) abortWithError(w, msg)
return return
} }
@ -34,7 +33,7 @@ func HandleFrontLogin(w http.ResponseWriter, r *http.Request) {
// login user // login user
cookie, err := makeCookie(cleanName, r.RemoteAddr) cookie, err := makeCookie(cleanName, r.RemoteAddr)
if err != nil { if err != nil {
slog.Error("failed to login", "error", err) log.Error("failed to login", "error", err)
abortWithError(w, err.Error()) abortWithError(w, err.Error())
return return
} }
@ -75,13 +74,13 @@ func makeCookie(username string, remote string) (*http.Cookie, error) {
SameSite: http.SameSiteNoneMode, SameSite: http.SameSiteNoneMode,
Domain: cfg.ServerConfig.Host, Domain: cfg.ServerConfig.Host,
} }
slog.Info("check remote addr for cookie set", log.Info("check remote addr for cookie set",
"remote", remote, "session", session) "remote", remote, "session", session)
if strings.Contains(remote, "192.168.0") { if strings.Contains(remote, "192.168.0") {
// no idea what is going on // no idea what is going on
// cookie.Domain = "192.168.0.15" // cookie.Domain = "192.168.0.15"
cookie.Domain = "home.host" cookie.Domain = "home.host"
slog.Info("changing cookie domain", "domain", cookie.Domain) log.Info("changing cookie domain", "domain", cookie.Domain)
} }
// set ctx? // set ctx?
// set user in session // set user in session

View File

@ -2,9 +2,20 @@ package handlers
import ( import (
"html/template" "html/template"
"log/slog"
"net/http" "net/http"
"os"
) )
var log *slog.Logger
func init() {
log = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true,
}))
}
var roundWords = map[string]string{ var roundWords = map[string]string{
"hamster": "blue", "hamster": "blue",
"child": "red", "child": "red",
@ -18,6 +29,10 @@ func HandlePing(w http.ResponseWriter, r *http.Request) {
} }
func HandleHome(w http.ResponseWriter, r *http.Request) { func HandleHome(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFiles("components/index.html")) tmpl, err := template.ParseGlob("components/*.html")
tmpl.Execute(w, roundWords) if err != nil {
abortWithError(w, err.Error())
return
}
tmpl.ExecuteTemplate(w, "main", roundWords)
} }

View File

@ -8,8 +8,8 @@ import (
"errors" "errors"
"golias/config" "golias/config"
"golias/pkg/cache" "golias/pkg/cache"
"log/slog"
"net/http" "net/http"
"time"
) )
var ( var (
@ -17,13 +17,41 @@ var (
memcache cache.Cache memcache cache.Cache
) )
// responseWriterWrapper wraps http.ResponseWriter to capture status code
type responseWriterWrapper struct {
http.ResponseWriter
status int
}
func (w *responseWriterWrapper) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
// LogRequests logs all HTTP requests with method, path and duration
func LogRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrap response writer to capture status code
ww := &responseWriterWrapper{ResponseWriter: w}
next.ServeHTTP(ww, r)
duration := time.Since(start)
log.Debug("request completed",
"method", r.Method,
"path", r.URL.Path,
"status", ww.status,
"duration", duration.String(),
)
})
}
func GetSession(next http.Handler) http.Handler { func GetSession(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cookieName := "session_token" cookieName := "session_token"
sessionCookie, err := r.Cookie(cookieName) sessionCookie, err := r.Cookie(cookieName)
if err != nil { if err != nil {
msg := "auth failed; failed to get session token from cookies" msg := "auth failed; failed to get session token from cookies"
slog.Debug(msg, "error", err) log.Debug(msg, "error", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -31,13 +59,13 @@ func GetSession(next http.Handler) http.Handler {
DecodeString(sessionCookie.Value) DecodeString(sessionCookie.Value)
if err != nil { if err != nil {
msg := "auth failed; failed to decode b64 cookie" msg := "auth failed; failed to decode b64 cookie"
slog.Debug(msg, "error", err) log.Debug(msg, "error", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
cookieValue := string(cookieValueB) cookieValue := string(cookieValueB)
if len(cookieValue) < sha256.Size { if len(cookieValue) < sha256.Size {
slog.Warn("small cookie", "size", len(cookieValue)) log.Warn("small cookie", "size", len(cookieValue))
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -50,7 +78,7 @@ func GetSession(next http.Handler) http.Handler {
mac.Write([]byte(sessionToken)) mac.Write([]byte(sessionToken))
expectedSignature := mac.Sum(nil) expectedSignature := mac.Sum(nil)
if !hmac.Equal([]byte(signature), expectedSignature) { if !hmac.Equal([]byte(signature), expectedSignature) {
slog.Debug("cookie with an invalid sign") log.Debug("cookie with an invalid sign")
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -58,14 +86,14 @@ func GetSession(next http.Handler) http.Handler {
if err != nil { if err != nil {
msg := "auth failed; session does not exists" msg := "auth failed; session does not exists"
err = errors.New(msg) err = errors.New(msg)
slog.Debug(msg, "error", err) log.Debug(msg, "error", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
if userSession.IsExpired() { if userSession.IsExpired() {
memcache.RemoveKey(sessionToken) memcache.RemoveKey(sessionToken)
msg := "session is expired" msg := "session is expired"
slog.Debug(msg, "error", err, "token", sessionToken) log.Debug(msg, "error", err, "token", sessionToken)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -74,7 +102,7 @@ func GetSession(next http.Handler) http.Handler {
if err := cacheSetSession(sessionToken, if err := cacheSetSession(sessionToken,
userSession); err != nil { userSession); err != nil {
msg := "failed to marshal user session" msg := "failed to marshal user session"
slog.Warn(msg, "error", err) log.Warn(msg, "error", err)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }

View File

@ -2,8 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"net/http"
"golias/handlers" "golias/handlers"
"net/http"
"time" "time"
) )
@ -11,7 +11,7 @@ import (
func ListenToRequests(port string) error { func ListenToRequests(port string) error {
mux := http.NewServeMux() mux := http.NewServeMux()
server := &http.Server{ server := &http.Server{
Handler: mux, Handler: handlers.LogRequests(handlers.GetSession(mux)),
Addr: port, Addr: port,
ReadTimeout: time.Second * 5, ReadTimeout: time.Second * 5,
WriteTimeout: time.Second * 5, WriteTimeout: time.Second * 5,