diff --git a/cmd/cli.go b/cmd/cli.go index a8a7895..d4f7576 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -94,7 +94,7 @@ func checkAuthorised(dbConn *sql.DB, username string) { } fmt.Println() - authorised, err := users.Authorised(dbConn, username, string(password)) + authorised, err := users.UserAuthorised(dbConn, username, string(password)) if err != nil { fmt.Println("Error checking authorisation:", err) os.Exit(1) diff --git a/cmd/willow.go b/cmd/willow.go index cdf414a..6229cfc 100644 --- a/cmd/willow.go +++ b/cmd/willow.go @@ -109,6 +109,7 @@ func main() { mux.HandleFunc("/static", ws.StaticHandler) mux.HandleFunc("/new", wsHandler.NewHandler) mux.HandleFunc("/login", wsHandler.LoginHandler) + mux.HandleFunc("/logout", wsHandler.LogoutHandler) httpServer := &http.Server{ Addr: config.Server.Listen, diff --git a/project/project.go b/project/project.go index 3ee1a3c..c040bf9 100644 --- a/project/project.go +++ b/project/project.go @@ -64,6 +64,7 @@ func fetchReleases(p Project) (Project, error) { case "github", "gitea", "forgejo": rssReleases, err := rss.GetReleases(p.URL) if err != nil { + fmt.Println("Error getting RSS releases:", err) return p, err } for _, release := range rssReleases { diff --git a/rss/rss.go b/rss/rss.go index 2302630..2ef4120 100644 --- a/rss/rss.go +++ b/rss/rss.go @@ -6,6 +6,7 @@ package rss import ( "fmt" + "strings" "time" "github.com/microcosm-cc/bluemonday" @@ -27,7 +28,8 @@ var ( func GetReleases(feedURL string) ([]Release, error) { fp := gofeed.NewParser() - feed, err := fp.ParseURL(feedURL + "/releases.atom") + + feed, err := fp.ParseURL(strings.TrimSuffix(feedURL, "/") + "/releases.atom") if err != nil { fmt.Println(err) return nil, err @@ -44,8 +46,5 @@ func GetReleases(feedURL string) ([]Release, error) { }) } - // TODO: Doesn't seem to work? - // sort.Slice(p.Releases, func(i, j int) bool { return p.Releases[i].Date.After(p.Releases[j].Date) }) - return releases, nil } diff --git a/users/users.go b/users/users.go index 043edbd..ed96cb6 100644 --- a/users/users.go +++ b/users/users.go @@ -55,9 +55,9 @@ func Register(dbConn *sql.DB, username, password string) error { // Delete removes a user from the database. func Delete(dbConn *sql.DB, username string) error { return db.DeleteUser(dbConn, username) } -// Authorised accepts a username string, a token string, and returns true if the +// UserAuthorised accepts a username string, a token string, and returns true if the // user is authorised, false if not, and an error if one is encountered. -func Authorised(dbConn *sql.DB, username, token string) (bool, error) { +func UserAuthorised(dbConn *sql.DB, username, token string) (bool, error) { dbHash, dbSalt, err := db.GetUser(dbConn, username) if err != nil { return false, err @@ -71,21 +71,38 @@ func Authorised(dbConn *sql.DB, username, token string) (bool, error) { return dbHash == providedHash, nil } -// GetSession accepts a session cookie string and returns the username -func GetSession(dbConn *sql.DB, session string) (string, time.Time, error) { - return db.GetSession(dbConn, session) +// SessionAuthorised accepts a session string and returns true if the session is +// valid and false if not. +func SessionAuthorised(dbConn *sql.DB, session string) (bool, error) { + dbResult, expiry, err := db.GetSession(dbConn, session) + if dbResult == "" || expiry.Before(time.Now()) || err != nil { + return false, err + } + + return true, nil } -// InvalidateSession invalidates a session by setting the expiration date to the -// current time. +// InvalidateSession invalidates a session by setting the expiration date to now. func InvalidateSession(dbConn *sql.DB, session string) error { return db.InvalidateSession(dbConn, session, time.Now()) } -// CreateSession accepts a username and a token and creates a session in the -// database. -func CreateSession(dbConn *sql.DB, username, token string, expiry time.Time) error { - return db.CreateSession(dbConn, username, token, expiry) +// CreateSession accepts a username, generates a token, stores it in the +// database, and returns it +func CreateSession(dbConn *sql.DB, username string) (string, time.Time, error) { + token, err := generateSalt() + if err != nil { + return "", time.Time{}, err + } + + expiry := time.Now().Add(7 * 24 * time.Hour) + + err = db.CreateSession(dbConn, username, token, expiry) + if err != nil { + return "", time.Time{}, err + } + + return token, expiry, nil } // GetUsers returns a list of all users in the database as a slice of strings. diff --git a/ws/static/home.html b/ws/static/home.html index 2805e0e..75d5d7d 100644 --- a/ws/static/home.html +++ b/ws/static/home.html @@ -22,6 +22,9 @@ html { } .project > h2 > span { float: right; +} +.project > details > pre { + overflow: scroll; } diff --git a/ws/static/new.html b/ws/static/new.html index 6657bbd..80889e8 100644 --- a/ws/static/new.html +++ b/ws/static/new.html @@ -30,8 +30,8 @@ html {

- -
+ +

Raw git


diff --git a/ws/ws.go b/ws/ws.go index 82cf5fa..6ae344b 100644 --- a/ws/ws.go +++ b/ws/ws.go @@ -8,12 +8,14 @@ import ( "database/sql" "embed" "fmt" + "git.sr.ht/~amolith/willow/users" "io" "net/http" "net/url" "strings" "sync" "text/template" + "time" "git.sr.ht/~amolith/willow/project" "github.com/microcosm-cc/bluemonday" @@ -153,12 +155,118 @@ func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) { } func (h Handler) LoginHandler(w http.ResponseWriter, r *http.Request) { - // TODO: do this + if r.Method == http.MethodGet { + if h.isAuthorised(r) { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + login, err := fs.ReadFile("static/login.html") + if err != nil { + fmt.Println("Error reading login.html:", err) + } + + if _, err := io.WriteString(w, string(login)); err != nil { + fmt.Println(err) + } + } + + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + fmt.Println(err) + } + username := bmStrict.Sanitize(r.FormValue("username")) + password := bmStrict.Sanitize(r.FormValue("password")) + + if username == "" || password == "" { + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte("No data provided")) + if err != nil { + fmt.Println(err) + } + return + } + + authorised, err := users.UserAuthorised(h.DbConn, username, password) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(fmt.Sprintf("Error logging in: %s", err))) + if err != nil { + fmt.Println(err) + } + return + } + + if !authorised { + w.WriteHeader(http.StatusUnauthorized) + _, err := w.Write([]byte("Incorrect username or password")) + if err != nil { + fmt.Println(err) + } + return + } + + session, expiry, err := users.CreateSession(h.DbConn, username) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(fmt.Sprintf("Error creating session: %s", err))) + if err != nil { + fmt.Println(err) + } + return + } + + maxAge := int(expiry.Sub(time.Now()).Seconds()) + + cookie := http.Cookie{ + Name: "id", + Value: session, + MaxAge: maxAge, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + Secure: true, + } + + http.SetCookie(w, &cookie) + http.Redirect(w, r, "/", http.StatusSeeOther) + } } +func (h Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("id") + if err != nil { + fmt.Println(err) + } + + err = users.InvalidateSession(h.DbConn, cookie.Value) + if err != nil { + fmt.Println(err) + _, err = w.Write([]byte(fmt.Sprintf("Error logging out: %s", err))) + if err != nil { + fmt.Println(err) + } + } + cookie.MaxAge = -1 + http.SetCookie(w, cookie) + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +// isAuthorised makes a database request to the sessions table to see if the +// user has a valid session cookie. func (h Handler) isAuthorised(r *http.Request) bool { - // TODO: do this - return false + cookie, err := r.Cookie("id") + if err != nil { + return false + } + + authorised, err := users.SessionAuthorised(h.DbConn, cookie.Value) + if err != nil { + fmt.Println("Error checking session:", err) + return false + } + + return authorised } func StaticHandler(writer http.ResponseWriter, request *http.Request) {