diff --git a/admin_templates/admin_templates.go b/admin_templates/admin_templates.go new file mode 100644 index 0000000..efbfb9e --- /dev/null +++ b/admin_templates/admin_templates.go @@ -0,0 +1,13 @@ +package admintemplates + +var LoginPage string = ` + + +
+ + + +
+ + +` diff --git a/main.go b/main.go index 9095d50..e68cd3c 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,8 @@ import ( "strconv" "strings" "time" + + admintemplates "git.yetaga.in/alazyreader/alpha/admin_templates" ) type SessionProvider interface { @@ -23,9 +25,15 @@ type PageProvider interface { Save(key string, page *Page) error } +type UserProvider interface { + Authenticate(name string, password string) (User, error) +} + type RootHandler struct { Sessions SessionProvider Pages PageProvider + Users UserProvider + Admin *AdminHandler StaticDir string } @@ -37,13 +45,58 @@ func (h *AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("admin route")) } +func (h *RootHandler) LoginHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + // serve login form + w.Write([]byte(admintemplates.LoginPage)) + case "POST": + // handle login request + r.ParseForm() + username := r.PostForm.Get("username") + password := r.PostForm.Get("password") + if username == "" { + h.ErrorHandle(http.StatusBadRequest, w) + return + } + user, err := h.Users.Authenticate(username, password) + if err != nil { + h.ErrorHandle(http.StatusForbidden, w) + return + } + sessionKey, err := h.Sessions.Create(user, time.Duration(time.Hour*24)) + if err != nil { + h.ErrorHandle(http.StatusInternalServerError, w) + return + } + w.Header().Add("Set-Cookie", "alphasess="+sessionKey) + http.Redirect(w, r, "/admin", http.StatusFound) + default: + // anything else doesn't do anything + h.ErrorHandle(http.StatusMethodNotAllowed, w) + } +} + func (h *RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if strings.HasPrefix(r.URL.Path, "/admin") { - (&AdminHandler{ - Sessions: h.Sessions, - }).ServeHTTP(w, r) + // handle login route + if r.URL.Path == "/login" { + h.LoginHandler(w, r) return } + + // hand off to admin router + if strings.HasPrefix(r.URL.Path, "/admin") { + if user, ok := h.IsLoggedIn(r); !ok { + http.Redirect(w, r, "/login", http.StatusFound) + return + } else if HasRole(user.Roles, "admin") { + h.Admin.ServeHTTP(w, r) + return + } + h.ErrorHandle(http.StatusForbidden, w) + return + } + // attempt to serve from the managed pages page, err := h.Pages.Page(r.URL.Path) if err == nil { @@ -53,19 +106,19 @@ func (h *RootHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // fall back to serving out of the static directory, but: // 1. prevent the generated indexes from rendering if strings.HasSuffix(r.URL.Path, "/") { - h.ErrorHandle(404, w) + h.ErrorHandle(http.StatusNotFound, w) return } // 2. prevent hidden paths from rendering for _, seg := range strings.Split(r.URL.Path, "/") { if strings.HasPrefix(seg, ".") { - h.ErrorHandle(404, w) + h.ErrorHandle(http.StatusNotFound, w) return } } // 3. catch files that would 404 and serve our own 404 page if !staticFileExists(filepath.Join(h.StaticDir, r.URL.Path)) { - h.ErrorHandle(404, w) + h.ErrorHandle(http.StatusNotFound, w) return } // finally, use the built-in fileserver to serve @@ -85,10 +138,28 @@ func (h *RootHandler) ErrorHandle(status int, w http.ResponseWriter) { } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") - w.WriteHeader(404) + w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "%d\n", status) } +func (h *RootHandler) IsLoggedIn(r *http.Request) (User, bool) { + cookie, err := r.Cookie("alphasess") + if err != nil { + return User{}, false + } + user, err := h.Sessions.Get(cookie.Value) + return user, err == nil +} + +func HasRole(roles []string, role string) bool { + for _, r := range roles { + if r == role { + return true + } + } + return false +} + func staticFileExists(name string) bool { f, err := os.Open(name) if err != nil { @@ -100,18 +171,28 @@ func staticFileExists(name string) bool { } func main() { + sessions := &Sessions{} + pages := &Index{} + users := &Userstore{} + users.Create("testuser", []string{"admin"}, "password") + handler := &RootHandler{ - Sessions: &Sessions{}, - Pages: &Index{}, + Sessions: sessions, + Pages: pages, + Users: users, + Admin: &AdminHandler{ + Sessions: sessions, + }, StaticDir: "./static", } - handler.Pages.Save("foo", &Page{ + pages.Save("foo", &Page{ Contents: []byte("foobar"), }) - handler.Pages.Save("index", &Page{ + pages.Save("index", &Page{ Contents: []byte("root"), }) + log.Printf("serving on http://localhost:8080") err := http.ListenAndServe(":8080", handler) log.Fatalf("server error: %v", err) } diff --git a/session.go b/session.go index afd0d6f..5993744 100644 --- a/session.go +++ b/session.go @@ -40,7 +40,7 @@ func (s *Sessions) Get(key string) (User, error) { if !ok { return User{}, ErrInvalidSession } - if sess.expr.After(time.Now()) { + if sess.expr.Before(time.Now()) { delete(s.sessions, key) return User{}, ErrInvalidSession } @@ -56,7 +56,7 @@ func (s *Sessions) Refresh(key string, user User, expr time.Duration) error { if !ok { return ErrInvalidSession } - if sess.expr.After(time.Now()) { + if sess.expr.Before(time.Now()) { delete(s.sessions, key) return ErrInvalidSession }