package main import ( "context" "database/sql" "encoding/gob" "errors" "flag" "fmt" "log" "net" "net/http" "os" "strconv" "github.com/gorilla/handlers" "github.com/gorilla/schema" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" "golang.org/x/crypto/bcrypt" "gosten/model" "gosten/templ" ) // flags var ( port uint64 host string ) func init() { flag.StringVar(&host, "h", "localhost", "address to listen on") flag.Uint64Var(&port, "p", 8080, "port to listen on") gob.Register(SessionData{}) } var Q *model.Queries var s *schema.Decoder var sessionStore sessions.Store const sessionCookie = "sessionKeks" func main() { flag.Parse() db := openDB("") if err := db.Ping(); err != nil { log.Fatal(err) } Q = model.New(db) s = schema.NewDecoder() sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(32)) mux := http.NewServeMux() mux.Handle("/{$}", showTemplate("index", nil)) mux.HandleFunc("GET /login", loginPage) mux.HandleFunc("POST /login", handleLogin) mux.HandleFunc("GET /logout", handleLogout) handler := sessionHandler(mux) handler = handlers.CombinedLoggingHandler(os.Stderr, handler) handler = handlers.ProxyHeaders(handler) address := net.JoinHostPort(host, strconv.FormatUint(port, 10)) log.Fatal(http.ListenAndServe(address, handler)) } type User struct { Name string `form:"options=required"` Password string `form:"type=password;options=required"` RememberMe bool `form:"type=checkbox;value=y;options=checked"` Errors []error `form:"-"` } type fieldError struct { Field string Issue string } func (fe fieldError) Error() string { return fmt.Sprintf("%s: %v", fe.Field, fe.Issue) } func (fe fieldError) FieldError() (field, err string) { return fe.Field, fe.Issue } func showTemplate(tpl string, data any) http.HandlerFunc { return func(w http.ResponseWriter, _ *http.Request) { if err := templ.Lookup(tpl).Execute(w, data); err != nil { log.Panicf("Executing '%s' with %+v: %v", tpl, data, err) } } } func parseForm[T any](r *http.Request, data *T) { if err := r.ParseForm(); err != nil { log.Panic("Parsing form: ", err) } if err := s.Decode(data, r.PostForm); err != nil { log.Panic("Decoding form: ", err) } } func loginPage(w http.ResponseWriter, r *http.Request) { s := session(r) if !s.s.IsNew && s.Authenticated { u, err := Q.GetUserById(r.Context(), s.UserID) if err != nil { s.Authenticated = false s.Save(w, r) } else { u2 := User{Name: u.Name} showTemplate("login2", u2).ServeHTTP(w, r) return } } showTemplate("login", User{}).ServeHTTP(w, r) } func handleLogin(w http.ResponseWriter, r *http.Request) { u := User{} parseForm(r, &u) invalid := false dbUser, err := Q.GetUserByName(r.Context(), u.Name) if err == nil { hash := []byte(dbUser.Pwd) pwd := []byte(u.Password) if bcrypt.CompareHashAndPassword(hash, pwd) != nil { invalid = true } } else if errors.Is(err, sql.ErrNoRows) { invalid = true } else { log.Panicf("Could not load user '%s': %v", u.Name, err) } if invalid { u.Errors = []error{fieldError{"Password", "Invalid"}} showTemplate("login", u).ServeHTTP(w, r) return } s := session(r) if u.RememberMe { s.MaxAge(86400 * 7) // 1 week } else { s.MaxAge(0) } s.UserID = dbUser.ID s.Authenticated = true s.Save(w, r) showTemplate("login2", u).ServeHTTP(w, r) } func handleLogout(w http.ResponseWriter, r *http.Request) { s := session(r) s.Authenticated = false s.MaxAge(-1) s.Save(w, r) } func sessionHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session, err := sessionStore.Get(r, sessionCookie) if err != nil { } ctx := context.WithValue(r.Context(), "_session", session) next.ServeHTTP(w, r.WithContext(ctx)) }) } type Session struct { *SessionData s *sessions.Session } type SessionData struct { UserID int64 Authenticated bool } func (s *Session) Save(w http.ResponseWriter, r *http.Request) { s.s.Values["data"] = *s.SessionData if err := s.s.Save(r, w); err != nil { log.Panic("Storing session: ", err) } } func (s *Session) MaxAge(maxAge int) { s.s.Options.MaxAge = maxAge } func session(r *http.Request) Session { s := r.Context().Value("_session").(*sessions.Session) s.Options.HttpOnly = true sd, ok := s.Values["data"].(SessionData) if !ok { sd = SessionData{} } return Session{&sd, s} }