small pixel drawing of a pufferfish vore

add user auth system
Jes Olson j3s@c3f.net
Wed, 15 Mar 2023 23:41:54 -0700
commit

01c09f0e776736dd5eebb2b56503690fdb01ff47

parent

d07519db817474cdef5034279dc79b864d229961

8 files changed, 210 insertions(+), 24 deletions(-)

jump to
M .gitignore.gitignore

@@ -1,1 +1,2 @@

feeds.gay +feeds.gay.db
A auth/auth.go

@@ -0,0 +1,12 @@

+package auth + +import ( + "fmt" + "time" +) + +func GenerateSessionToken() string { + // TODO: don't use the time ffs + token := fmt.Sprintf("%x", time.Now().UnixNano()) + return token +}
M go.modgo.mod

@@ -2,14 +2,17 @@ module git.j3s.sh/feeds.gay

go 1.20 -require github.com/glebarez/go-sqlite v1.21.0 +require ( + github.com/glebarez/go-sqlite v1.21.0 + golang.org/x/crypto v0.7.0 +) require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/sys v0.6.0 // indirect modernc.org/libc v1.22.2 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect
M go.sumgo.sum

@@ -10,9 +10,11 @@ github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M= github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
M http.gohttp.go

@@ -8,7 +8,7 @@

func internalServerError(w http.ResponseWriter, details string) { status := "oopsie woopsie, uwu\n" status += "we made a fucky wucky!!\n\n" - status += details + status += "500 internal server error: " + details http.Error(w, status, http.StatusInternalServerError) }
M main.gomain.go

@@ -9,7 +9,7 @@ func main() {

s := New() log.Println("listening on http://localhost:5544") mux := http.NewServeMux() - mux.HandleFunc("/", s.rootHandler) + mux.HandleFunc("/", s.indexHandler) mux.HandleFunc("/login", s.loginHandler) mux.HandleFunc("/logout", s.logoutHandler) mux.HandleFunc("/register", s.registerHandler)
M site.gosite.go

@@ -1,22 +1,29 @@

package main import ( - "database/sql" "fmt" "log" "net/http" + "git.j3s.sh/feeds.gay/auth" "git.j3s.sh/feeds.gay/sqlite" + "golang.org/x/crypto/bcrypt" ) type Site struct { - db *sql.DB + // title of the website + title string + + // site database handle + db *sqlite.DB } // New returns a fully populated & ready for action Site func New() *Site { + title := "feeds.gay" s := Site{ - db: sqlite.SetupAndOpen("feeds.gay.db"), + title: "feeds.gay", + db: sqlite.New(title + ".db"), } return &s }

@@ -25,7 +32,7 @@ func (s *Site) Start(addr string, mux *http.ServeMux) {

log.Fatal(http.ListenAndServe(addr, mux)) } -func (s *Site) rootHandler(w http.ResponseWriter, r *http.Request) { +func (s *Site) indexHandler(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, "GET") { return }

@@ -35,7 +42,15 @@ if r.URL.Path != "/" {

http.NotFound(w, r) return } - fmt.Fprintf(w, "feeds.gay is dope & you should like it\n") + if s.authenticated(r) { + fmt.Fprintf(w, `<h1>sup shitbag</h1> + <a href="/logout">logout</a>`) + } else { + fmt.Fprintf(w, `<h1>sup shitbag</h1> + <a href="/login">login</a> + <a href="/register">register</a> + <a href="/logout">logout</a>`) + } } func (s *Site) loginHandler(w http.ResponseWriter, r *http.Request) {

@@ -43,29 +58,122 @@ if !methodAllowed(w, r, "GET", "POST") {

return } if r.Method == "GET" { - // if logged out: - fmt.Fprintf(w, "display login forms\n") - // if logged in: - fmt.Fprintf(w, "you are already logged in :D\n") + if s.authenticated(r) { + fmt.Fprintf(w, "you are already logged in :3\n") + } else { + fmt.Fprintf(w, `<h1>login</h1> + <form method="POST" action="/login"> + <label for="username">username:</label> + <input type="text" name="username" required><br> + <label for="password">password:</label> + <input type="password" name="password" required><br> + <input type="submit" value="login"> + </form>`) + } } if r.Method == "POST" { - fmt.Fprintf(w, "cmon POST\n") + username := r.FormValue("username") + password := r.FormValue("password") + + err := s.login(w, username, password) + if err != nil { + fmt.Fprintf(w, "<h1>incorrect username/password</h1>") + return + } + http.Redirect(w, r, "/", http.StatusSeeOther) } } func (s *Site) logoutHandler(w http.ResponseWriter, r *http.Request) { - if !methodAllowed(w, r, "POST") { + if !methodAllowed(w, r, "GET") { return } - // TODO: delete session cookie + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: "", + }) http.Redirect(w, r, "/", http.StatusSeeOther) } func (s *Site) registerHandler(w http.ResponseWriter, r *http.Request) { - if !methodAllowed(w, r, "POST") { + if !methodAllowed(w, r, "GET", "POST") { return } - // TODO: create user in database - // TODO: add session cookie - http.Redirect(w, r, "/", http.StatusSeeOther) + + if r.Method == "GET" { + fmt.Fprintf(w, `<h1>register</h1> + <form method="POST" action="/register"> + <label for="username">username:</label> + <input type="text" name="username" required><br> + <label for="password">password:</label> + <input type="password" name="password" required><br> + <input type="submit" value="login"> + </form>`) + } + + if r.Method == "POST" { + username := r.FormValue("username") + password := r.FormValue("password") + err := s.register(username, password) + if err != nil { + internalServerError(w, "failed to register user") + return + } + err = s.login(w, username, password) + if err != nil { + internalServerError(w, "extremely weird login error") + return + } + http.Redirect(w, r, "/", http.StatusSeeOther) + } +} + +func (s *Site) authenticated(r *http.Request) bool { + sessionToken, err := r.Cookie("session_token") + if err != nil { + return false + } + + username := s.db.GetUsernameBySessionToken(sessionToken.Value) + if username == "" { + return false + } + + return true +} + +// login compares the sqlite password field against the user supplied password and +// sets a session token against the supplied writer. +func (s *Site) login(w http.ResponseWriter, username string, password string) error { + storedPassword := s.db.GetPassword(username) + if storedPassword == "" { + return fmt.Errorf("blank stored password") + } + if username == "" { + return fmt.Errorf("username cannot be nil") + } + if password == "" { + return fmt.Errorf("password cannot be nil") + } + err := bcrypt.CompareHashAndPassword([]byte(storedPassword), []byte(password)) + if err != nil { + return fmt.Errorf("invalid password") + } + sessionToken := auth.GenerateSessionToken() + s.db.SetSessionToken(username, sessionToken) + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: sessionToken, + }) + return nil +} + +func (s *Site) register(username string, password string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err + } + + s.db.AddUser(username, string(hashedPassword)) + return nil }
M sqlite/sql.gosqlite/sql.go

@@ -7,10 +7,70 @@

_ "github.com/glebarez/go-sqlite" ) -func SetupAndOpen(path string) *sql.DB { +type DB struct { + sql *sql.DB +} + +// New opens a sqlite database, populates it with tables, and +// returns a ready-to-use *sqlite.DB object which is used for +// abstracting database queries. +func New(path string) *DB { db, err := sql.Open("sqlite", path) if err != nil { log.Fatal(err) } - return db + _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + session_token TEXT UNIQUE + )`) + if err != nil { + panic(err) + } + + wrapper := DB{ + sql: db, + } + + return &wrapper +} + +// TODO: evaluate error cases + +func (s *DB) GetUsernameBySessionToken(token string) string { + var username string + err := s.sql.QueryRow("SELECT username FROM users WHERE session_token=?", token).Scan(&username) + if err == sql.ErrNoRows { + return "" + } + if err != nil { + panic(err) + } + return username +} + +func (s *DB) GetPassword(username string) string { + var password string + err := s.sql.QueryRow("SELECT password FROM users WHERE username=?", username).Scan(&password) + if err == sql.ErrNoRows { + return "" + } + if err != nil { + panic(err) + } + return password +} +func (s *DB) SetSessionToken(username string, token string) { + _, err := s.sql.Exec("UPDATE users SET session_token=? WHERE username=?", token, username) + if err != nil { + panic(err) + } +} + +func (s *DB) AddUser(username string, passwordHash string) { + _, err := s.sql.Exec("INSERT INTO users (username, password) VALUES (?, ?)", username, passwordHash) + if err != nil { + panic(err) + } }