finish implementing authentication

This commit is contained in:
Amolith 2023-10-25 20:16:36 -04:00
parent ef9544ff7d
commit 438f9fc7ae
Signed by: Amolith
GPG Key ID: 8AE30347CE28D101
8 changed files with 150 additions and 21 deletions

View File

@ -94,7 +94,7 @@ func checkAuthorised(dbConn *sql.DB, username string) {
} }
fmt.Println() fmt.Println()
authorised, err := users.Authorised(dbConn, username, string(password)) authorised, err := users.UserAuthorised(dbConn, username, string(password))
if err != nil { if err != nil {
fmt.Println("Error checking authorisation:", err) fmt.Println("Error checking authorisation:", err)
os.Exit(1) os.Exit(1)

View File

@ -109,6 +109,7 @@ func main() {
mux.HandleFunc("/static", ws.StaticHandler) mux.HandleFunc("/static", ws.StaticHandler)
mux.HandleFunc("/new", wsHandler.NewHandler) mux.HandleFunc("/new", wsHandler.NewHandler)
mux.HandleFunc("/login", wsHandler.LoginHandler) mux.HandleFunc("/login", wsHandler.LoginHandler)
mux.HandleFunc("/logout", wsHandler.LogoutHandler)
httpServer := &http.Server{ httpServer := &http.Server{
Addr: config.Server.Listen, Addr: config.Server.Listen,

View File

@ -64,6 +64,7 @@ func fetchReleases(p Project) (Project, error) {
case "github", "gitea", "forgejo": case "github", "gitea", "forgejo":
rssReleases, err := rss.GetReleases(p.URL) rssReleases, err := rss.GetReleases(p.URL)
if err != nil { if err != nil {
fmt.Println("Error getting RSS releases:", err)
return p, err return p, err
} }
for _, release := range rssReleases { for _, release := range rssReleases {

View File

@ -6,6 +6,7 @@ package rss
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
@ -27,7 +28,8 @@ var (
func GetReleases(feedURL string) ([]Release, error) { func GetReleases(feedURL string) ([]Release, error) {
fp := gofeed.NewParser() fp := gofeed.NewParser()
feed, err := fp.ParseURL(feedURL + "/releases.atom")
feed, err := fp.ParseURL(strings.TrimSuffix(feedURL, "/") + "/releases.atom")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return nil, 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 return releases, nil
} }

View File

@ -55,9 +55,9 @@ func Register(dbConn *sql.DB, username, password string) error {
// Delete removes a user from the database. // Delete removes a user from the database.
func Delete(dbConn *sql.DB, username string) error { return db.DeleteUser(dbConn, username) } 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. // 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) dbHash, dbSalt, err := db.GetUser(dbConn, username)
if err != nil { if err != nil {
return false, err return false, err
@ -71,21 +71,38 @@ func Authorised(dbConn *sql.DB, username, token string) (bool, error) {
return dbHash == providedHash, nil return dbHash == providedHash, nil
} }
// GetSession accepts a session cookie string and returns the username // SessionAuthorised accepts a session string and returns true if the session is
func GetSession(dbConn *sql.DB, session string) (string, time.Time, error) { // valid and false if not.
return db.GetSession(dbConn, session) 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
} }
// InvalidateSession invalidates a session by setting the expiration date to the return true, nil
// current time. }
// InvalidateSession invalidates a session by setting the expiration date to now.
func InvalidateSession(dbConn *sql.DB, session string) error { func InvalidateSession(dbConn *sql.DB, session string) error {
return db.InvalidateSession(dbConn, session, time.Now()) return db.InvalidateSession(dbConn, session, time.Now())
} }
// CreateSession accepts a username and a token and creates a session in the // CreateSession accepts a username, generates a token, stores it in the
// database. // database, and returns it
func CreateSession(dbConn *sql.DB, username, token string, expiry time.Time) error { func CreateSession(dbConn *sql.DB, username string) (string, time.Time, error) {
return db.CreateSession(dbConn, username, token, expiry) 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. // GetUsers returns a list of all users in the database as a slice of strings.

View File

@ -22,6 +22,9 @@ html {
} }
.project > h2 > span { .project > h2 > span {
float: right; float: right;
}
.project > details > pre {
overflow: scroll;
} }
</style> </style>
</head> </head>

View File

@ -30,8 +30,8 @@ html {
<label for="github">Github</label><br> <label for="github">Github</label><br>
<input type="radio" id="gitea" name="forge" value="gitea"> <input type="radio" id="gitea" name="forge" value="gitea">
<label for="gitea">Gitea</label><br> <label for="gitea">Gitea</label><br>
<input type="radio" id="Forgejo" name="forge" value="Forgejo"> <input type="radio" id="forgejo" name="forge" value="forgejo">
<label for="Forgejo">Forgejo</label><br> <label for="forgejo">Forgejo</label><br>
<p>Raw git</p> <p>Raw git</p>
<input type="radio" id="gitlab" name="forge" value="gitlab"> <input type="radio" id="gitlab" name="forge" value="gitlab">
<label for="gitlab">GitLab</label><br> <label for="gitlab">GitLab</label><br>

112
ws/ws.go
View File

@ -8,12 +8,14 @@ import (
"database/sql" "database/sql"
"embed" "embed"
"fmt" "fmt"
"git.sr.ht/~amolith/willow/users"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"sync" "sync"
"text/template" "text/template"
"time"
"git.sr.ht/~amolith/willow/project" "git.sr.ht/~amolith/willow/project"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
@ -153,14 +155,120 @@ func (h Handler) NewHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h Handler) LoginHandler(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 { func (h Handler) isAuthorised(r *http.Request) bool {
// TODO: do this cookie, err := r.Cookie("id")
if err != nil {
return false 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) { func StaticHandler(writer http.ResponseWriter, request *http.Request) {
resource := strings.TrimPrefix(request.URL.Path, "/") resource := strings.TrimPrefix(request.URL.Path, "/")
// if path ends in .css, set content type to text/css // if path ends in .css, set content type to text/css