From 25bcf4d706a412924c91fc51402d37d4e7647578 Mon Sep 17 00:00:00 2001 From: Maximilian Friedersdorff Date: Wed, 30 Jul 2025 13:41:03 +0100 Subject: [PATCH] Add half arsed user separation --- cmd/server/main.go | 29 +++++++----- internal/conf/templates/base.tmpl.html | 3 ++ internal/middleware/session.go | 62 ++++++++++++++++++++++---- internal/notes/notes.go | 13 +++--- internal/notes/views/views.go | 50 +++++++++++++++------ internal/templ/templ.go | 9 +++- 6 files changed, 125 insertions(+), 41 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index c1bf96b..e4d8976 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,7 +1,6 @@ package main import ( - "crypto/rand" "flag" "log" "net" @@ -17,7 +16,7 @@ import ( func main() { var confFile string - cache := make(map[string]string, 20) + sessions := middleware.NewSessionStore() flag.StringVar(&confFile, "c", "/etc/gonotes/conf.toml", "Specify path to config file.") flag.Parse() @@ -39,22 +38,28 @@ func main() { if !conf.Conf.Production { router.HandleFunc("/login/", func(w http.ResponseWriter, r *http.Request) { user := r.FormValue("user") - log.Printf("Trying to log in %s", user) - - sessionID := rand.Text() - cache[sessionID] = user - - cookie := http.Cookie{ - Name: "id", Value: sessionID, MaxAge: 3600, - Secure: true, HttpOnly: true, Path: "/", + if len(user) == 0 { + user = "anon" } - http.SetCookie(w, &cookie) + sessions.Login(user, w) + http.Redirect(w, r, "/notes/", http.StatusFound) }) } + router.Handle("/logout/", sessions.AsMiddleware( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := r.FormValue("user") + if len(user) == 0 { + user = "anon" + } + + sessions.Logout(w, r) + + http.Redirect(w, r, "/notes/", http.StatusFound) + }))) router.Handle("/", middleware.LoggingMiddleware(http.RedirectHandler("/notes/", http.StatusFound))) - router.Handle("/notes/", middleware.SessionMiddleware(cache, middleware.LoggingMiddleware(http.StripPrefix("/notes", notesRouter)))) + router.Handle("/notes/", sessions.AsMiddleware(middleware.LoggingMiddleware(http.StripPrefix("/notes", notesRouter)))) router.Handle( "/static/", middleware.LoggingMiddleware( diff --git a/internal/conf/templates/base.tmpl.html b/internal/conf/templates/base.tmpl.html index 67efee4..cf1ff0e 100644 --- a/internal/conf/templates/base.tmpl.html +++ b/internal/conf/templates/base.tmpl.html @@ -36,6 +36,9 @@ All Notes {{template "navLinks" .}} +
  • + Logout {{.user}} +
  • {{template "navExtra" .}} diff --git a/internal/middleware/session.go b/internal/middleware/session.go index 1e19825..199792e 100644 --- a/internal/middleware/session.go +++ b/internal/middleware/session.go @@ -1,33 +1,77 @@ -// Middleware to deal with sessions +// Package middleware to deal with sessions package middleware import ( "context" - "log" + "crypto/rand" "net/http" ) -func SessionMiddleware(cache map[string]string, next http.Handler) http.Handler { +type Session struct { + User string +} + +type SessionStore struct { + sessions map[string]Session +} + +type ContextKey string + +func NewSessionStore() SessionStore { + return SessionStore{sessions: make(map[string]Session, 10)} +} + +func (s *SessionStore) Login(user string, w http.ResponseWriter) { + sessionID := rand.Text() + s.sessions[sessionID] = Session{User: user} + + cookie := http.Cookie{ + Name: "id", Value: sessionID, MaxAge: 3600, + Secure: true, HttpOnly: true, Path: "/", + } + + http.SetCookie(w, &cookie) +} + +func (s *SessionStore) Logout(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value(ContextKey("session")).(string) + + delete(s.sessions, session) + cookie := http.Cookie{ + Name: "id", Value: "", MaxAge: -1, + Secure: true, HttpOnly: true, Path: "/", + } + + http.SetCookie(w, &cookie) +} + +func (s *SessionStore) AsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessionCookie, err := r.Cookie("id") // No session yet if err != nil { - http.Redirect(w, r, "/login/", http.StatusUnauthorized) + http.Redirect(w, r, "/login/", http.StatusFound) return } - user, ok := cache[sessionCookie.Value] + session, ok := s.sessions[sessionCookie.Value] // Session expired if !ok { - http.Redirect(w, r, "/login/", http.StatusUnauthorized) + http.Redirect(w, r, "/login/", http.StatusFound) return } - log.Printf("User is %s", user) - ctx := r.Context() - ctx = context.WithValue(ctx, "user", user) + ctx = context.WithValue( + context.WithValue( + ctx, + ContextKey("user"), + session.User, + ), + ContextKey("session"), + sessionCookie.Value, + ) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/internal/notes/notes.go b/internal/notes/notes.go index 999cd83..dbcc9f7 100644 --- a/internal/notes/notes.go +++ b/internal/notes/notes.go @@ -29,6 +29,7 @@ type Note struct { Title string BodyRendered template.HTML Body []byte + Owner string } func fmtPath(path string) string { @@ -37,7 +38,7 @@ func fmtPath(path string) string { // Save a note to a path derived from the title func (n *Note) Save() error { - filename := filepath.Join(conf.Conf.NotesDir, fmtPath(n.EncodedTitle())) + filename := filepath.Join(conf.Conf.NotesDir, n.Owner, fmtPath(n.EncodedTitle())) return os.WriteFile(filename, n.Body, 0600) } @@ -58,18 +59,18 @@ func (n *Note) Render() { } // LoadNote from the disk. The path is derived from the title -func LoadNote(encodedTitle string) (*Note, error) { - filename := filepath.Join(conf.Conf.NotesDir, fmtPath(encodedTitle)) +func LoadNote(owner string, encodedTitle string) (*Note, error) { + filename := filepath.Join(conf.Conf.NotesDir, owner, fmtPath(encodedTitle)) body, err := os.ReadFile(filename) if err != nil { return nil, err } title := DecodeTitle(encodedTitle) - return &Note{Title: title, Body: body}, nil + return &Note{Title: title, Body: body, Owner: owner}, nil } -func DeleteNote(title string) error { - filename := filepath.Join(conf.Conf.NotesDir, fmtPath(title)) +func DeleteNote(owner string, title string) error { + filename := filepath.Join(conf.Conf.NotesDir, owner, fmtPath(title)) return os.Remove(filename) } diff --git a/internal/notes/views/views.go b/internal/notes/views/views.go index 60e7f62..48db392 100644 --- a/internal/notes/views/views.go +++ b/internal/notes/views/views.go @@ -4,18 +4,24 @@ import ( "log" "net/http" "os" + "path" "path/filepath" "strconv" "strings" urls "forgejo.gwairfelin.com/max/gispatcho" "forgejo.gwairfelin.com/max/gonotes/internal/conf" + "forgejo.gwairfelin.com/max/gonotes/internal/middleware" "forgejo.gwairfelin.com/max/gonotes/internal/notes" "forgejo.gwairfelin.com/max/gonotes/internal/templ" ) var myurls urls.URLs +func addRequestContext(r *http.Request, ctx templ.Ctx) templ.Ctx { + return ctx +} + func GetRoutes(prefix string) *http.ServeMux { myurls = urls.URLs{ Prefix: prefix, @@ -34,8 +40,10 @@ func GetRoutes(prefix string) *http.ServeMux { } func view(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(middleware.ContextKey("user")).(string) + title := r.PathValue("note") - note, err := notes.LoadNote(title) + note, err := notes.LoadNote(user, title) urlEdit := myurls.Reverse("edit", urls.Repl{"note": title}) urlDelete := myurls.Reverse("delete", urls.Repl{"note": title}) if err != nil { @@ -45,7 +53,7 @@ func view(w http.ResponseWriter, r *http.Request) { context := templ.Ctx{"note": note, "urlEdit": urlEdit, "urlDelete": urlDelete} note.Render() - err = templ.RenderTemplate(w, "view.tmpl.html", context) + err = templ.RenderTemplate(w, r, "view.tmpl.html", context) if err != nil { log.Print(err.Error()) http.Error(w, "Couldn't load template", http.StatusInternalServerError) @@ -54,8 +62,10 @@ func view(w http.ResponseWriter, r *http.Request) { } func edit(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(middleware.ContextKey("user")).(string) + encodedTitle := r.PathValue("note") - note, err := notes.LoadNote(encodedTitle) + note, err := notes.LoadNote(user, encodedTitle) if err != nil { title := notes.DecodeTitle(encodedTitle) note = ¬es.Note{Title: title} @@ -63,7 +73,7 @@ func edit(w http.ResponseWriter, r *http.Request) { urlSave := myurls.Reverse("save", urls.Repl{"note": encodedTitle}) context := templ.Ctx{"note": note, "urlSave": urlSave, "text": string(note.Body)} - err = templ.RenderTemplate(w, "edit.tmpl.html", context) + err = templ.RenderTemplate(w, r, "edit.tmpl.html", context) if err != nil { log.Print(err.Error()) http.Error(w, "Couldn't load template", http.StatusInternalServerError) @@ -84,8 +94,10 @@ func new(w http.ResponseWriter, r *http.Request) { } func delete(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(middleware.ContextKey("user")).(string) + encodedTitle := r.PathValue("note") - err := notes.DeleteNote(encodedTitle) + err := notes.DeleteNote(user, encodedTitle) if err != nil { log.Print(err.Error()) http.Error(w, "Couldn't delete note", http.StatusInternalServerError) @@ -97,21 +109,25 @@ func delete(w http.ResponseWriter, r *http.Request) { } func save(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(middleware.ContextKey("user")).(string) + oldTitle := r.PathValue("note") title := r.FormValue("title") body := r.FormValue("body") - note := ¬es.Note{Title: title, Body: []byte(body)} + note := ¬es.Note{Title: title, Body: []byte(body), Owner: user} note.Save() if oldTitle != note.EncodedTitle() { - notes.DeleteNote(oldTitle) + notes.DeleteNote(user, oldTitle) } http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": note.EncodedTitle()}), http.StatusFound) } func togglebox(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(middleware.ContextKey("user")).(string) + title := r.PathValue("note") nthBox, err := strconv.Atoi(r.FormValue("box")) if err != nil { @@ -119,7 +135,7 @@ func togglebox(w http.ResponseWriter, r *http.Request) { return } - note, err := notes.LoadNote(title) + note, err := notes.LoadNote(user, title) if err != nil { http.Redirect(w, r, myurls.Reverse("view", urls.Repl{"note": title}), http.StatusFound) return @@ -136,11 +152,19 @@ type titleAndURL struct { } func list(w http.ResponseWriter, r *http.Request) { - files, err := os.ReadDir(conf.Conf.NotesDir) + user := r.Context().Value(middleware.ContextKey("user")).(string) + + notesDir := path.Join(conf.Conf.NotesDir, user) + + files, err := os.ReadDir(notesDir) if err != nil { - log.Print(err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return + if os.IsNotExist(err) { + os.MkdirAll(notesDir, os.FileMode(0750)) + } else { + log.Print(err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } } titlesAndUrls := make([]titleAndURL, 0) @@ -162,7 +186,7 @@ func list(w http.ResponseWriter, r *http.Request) { urlNew := myurls.Reverse("new", urls.Repl{}) - err = templ.RenderTemplate(w, "list.tmpl.html", templ.Ctx{"notes": titlesAndUrls, "urlNew": urlNew}) + err = templ.RenderTemplate(w, r, "list.tmpl.html", templ.Ctx{"notes": titlesAndUrls, "urlNew": urlNew}) if err != nil { log.Print(err.Error()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) diff --git a/internal/templ/templ.go b/internal/templ/templ.go index 1897e9d..b1759ba 100644 --- a/internal/templ/templ.go +++ b/internal/templ/templ.go @@ -6,11 +6,12 @@ import ( "path/filepath" "forgejo.gwairfelin.com/max/gonotes/internal/conf" + "forgejo.gwairfelin.com/max/gonotes/internal/middleware" ) type Ctx map[string]any -func RenderTemplate(w http.ResponseWriter, tmpl string, context any) error { +func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, context Ctx) error { var err error files := []string{ filepath.Join("templates", "base.tmpl.html"), @@ -26,6 +27,12 @@ func RenderTemplate(w http.ResponseWriter, tmpl string, context any) error { } t, err := template.ParseFS(conf.Templates, files...) + if err != nil { + return err + } + + context["user"] = r.Context().Value(middleware.ContextKey("user")).(string) + err = t.ExecuteTemplate(w, "base", context) return err }