diff --git a/db/connection.go b/db/connection.go new file mode 100644 index 0000000..23381a2 --- /dev/null +++ b/db/connection.go @@ -0,0 +1,19 @@ +package db + +import ( + "database/sql" + "fmt" + + "github.com/pkg/errors" + _ "github.com/tursodatabase/libsql-client-go/libsql" +) + +func ConnectToDatabase(primaryUrl *string, authToken *string) (*sql.DB, error) { + url := fmt.Sprintf("libsql://%s.turso.io?authToken=%s", *primaryUrl, *authToken) + + db, err := sql.Open("libsql", url) + if err != nil { + return nil, errors.Wrap(err, "sql.Open") + } + return db, nil +} diff --git a/db/users.go b/db/users.go new file mode 100644 index 0000000..b0cb5e8 --- /dev/null +++ b/db/users.go @@ -0,0 +1,62 @@ +package db + +import ( + "database/sql" + "fmt" + + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" +) + +type User struct { + ID int + Username string + Password_hash string + Created_at int64 +} + +func (user *User) SetPassword(conn *sql.DB, password string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return errors.Wrap(err, "bcrypt.GenerateFromPassword") + } + user.Password_hash = string(hashedPassword) + query := `UPDATE users SET password_hash = ? WHERE id = ?` + result, err := conn.Exec(query, user.Password_hash, user.ID) + if err != nil { + return errors.Wrap(err, "conn.Exec") + } + fmt.Println(result) + return nil +} + +func (user *User) CheckPassword(password string) error { + err := bcrypt.CompareHashAndPassword([]byte(user.Password_hash), []byte(password)) + if err != nil { + return errors.Wrap(err, "bcrypt.CompareHashAndPassword") + } + return nil +} + +func GetUserFromUsername(conn *sql.DB, username string) (User, error) { + query := `SELECT id, username, password_hash, created_at FROM users + WHERE username = ? COLLATE NOCASE` + rows, err := conn.Query(query, username) + if err != nil { + return User{}, errors.Wrap(err, "conn.Query") + } + defer rows.Close() + var user User + for rows.Next() { + err := rows.Scan( + &user.ID, + &user.Username, + &user.Password_hash, + &user.Created_at, + ) + if err != nil { + return User{}, errors.Wrap(err, "rows.Scan") + } + } + return user, nil +} diff --git a/go.mod b/go.mod index 91c444a..c3655ff 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,12 @@ require ( github.com/a-h/templ v0.3.833 github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 + github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d + golang.org/x/crypto v0.33.0 +) + +require ( + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/coder/websocket v1.8.12 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect ) diff --git a/go.sum b/go.sum index c4b5043..666d29a 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,18 @@ github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= diff --git a/handlers/login.go b/handlers/login.go index 2558fff..1bc6806 100644 --- a/handlers/login.go +++ b/handlers/login.go @@ -1,37 +1,32 @@ package handlers import ( - "errors" + "database/sql" "fmt" "net/http" + "projectreshoot/cookies" + "projectreshoot/db" "projectreshoot/view/component/form" "projectreshoot/view/page" + + "github.com/pkg/errors" ) -// TODO: here for testing only, move to database -type User struct { - id int - username string - password string -} - -// TODO: here for testing only, move to database -func testUser() User { - return User{id: 1, username: "Haelnorr", password: "test"} -} - -func validateLogin(r *http.Request) (int, error) { +func validateLogin(conn *sql.DB, r *http.Request) (db.User, error) { formUsername := r.FormValue("username") formPassword := r.FormValue("password") - // TODO: search database for username - validUser := testUser() - // TODO: check password is valid - if formUsername != validUser.username || formPassword != validUser.password { - return 0, errors.New("Username or password incorrect") + + user, err := db.GetUserFromUsername(conn, formUsername) + if err != nil { + return db.User{}, errors.Wrap(err, "db.GetUserFromUsername") } - // TODO: return the users ID - return validUser.id, nil + + err = user.CheckPassword(formPassword) + if err != nil { + return db.User{}, errors.New("Username or password incorrect") + } + return user, nil } func checkRememberMe(r *http.Request) bool { @@ -43,21 +38,23 @@ func checkRememberMe(r *http.Request) bool { } } -func HandleLoginRequest() http.Handler { +func HandleLoginRequest(conn *sql.DB) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { r.ParseForm() - userID, err := validateLogin(r) + user, err := validateLogin(conn, r) if err != nil { - // TODO: add debug log - fmt.Printf("Login failed: %s\n", err) form.LoginForm(err.Error()).Render(r.Context(), w) return } // TODO: login success, use the userID to set the session rememberMe := checkRememberMe(r) - fmt.Printf("Login success, user ID: %v - remember me?: %t\n", userID, rememberMe) + fmt.Printf( + "Login success, user: %v - remember me?: %t\n", + user.Username, + rememberMe, + ) pageFrom := cookies.CheckPageFrom(w, r) w.Header().Set("HX-Redirect", pageFrom) diff --git a/main.go b/main.go index baf74f2..3e5e31d 100644 --- a/main.go +++ b/main.go @@ -4,15 +4,18 @@ import ( "context" "embed" "fmt" - "github.com/pkg/errors" "io" "net" "net/http" "os" "os/signal" - "projectreshoot/server" "sync" "time" + + "projectreshoot/db" + "projectreshoot/server" + + "github.com/pkg/errors" ) func run(ctx context.Context, w io.Writer) error { @@ -24,7 +27,12 @@ func run(ctx context.Context, w io.Writer) error { return errors.Wrap(err, "server.GetConfig") } - srv := server.NewServer(config) + conn, err := db.ConnectToDatabase(&config.TursoURL, &config.TursoToken) + if err != nil { + return errors.Wrap(err, "db.ConnectToDatabase") + } + + srv := server.NewServer(config, conn) httpServer := &http.Server{ Addr: net.JoinHostPort(config.Host, config.Port), Handler: srv, diff --git a/server/routes.go b/server/routes.go index 5cab0ef..1edc17a 100644 --- a/server/routes.go +++ b/server/routes.go @@ -1,6 +1,7 @@ package server import ( + "database/sql" "net/http" "projectreshoot/handlers" "projectreshoot/view/page" @@ -9,6 +10,7 @@ import ( func addRoutes( mux *http.ServeMux, config *Config, + conn *sql.DB, ) { // Static files mux.Handle("GET /static/", http.StripPrefix("/static/", handlers.HandleStatic())) @@ -21,5 +23,5 @@ func addRoutes( // Login page and handlers mux.Handle("GET /login", handlers.HandleLoginPage(config.TrustedHost)) - mux.Handle("POST /login", handlers.HandleLoginRequest()) + mux.Handle("POST /login", handlers.HandleLoginRequest(conn)) } diff --git a/server/server.go b/server/server.go index 78bf445..ba1eb82 100644 --- a/server/server.go +++ b/server/server.go @@ -1,19 +1,23 @@ package server import ( + "database/sql" "errors" "fmt" "net/http" "os" + "projectreshoot/middleware" "github.com/joho/godotenv" ) type Config struct { - TrustedHost string Host string Port string + TrustedHost string + TursoURL string + TursoToken string } func GetConfig() (*Config, error) { @@ -26,6 +30,8 @@ func GetConfig() (*Config, error) { Host: os.Getenv("HOST"), Port: os.Getenv("PORT"), TrustedHost: os.Getenv("TRUSTED_HOST"), + TursoURL: os.Getenv("TURSO_DATABASE_URL"), + TursoToken: os.Getenv("TURSO_AUTH_TOKEN"), } if config.Host == "" { return nil, errors.New("Envar not set: HOST") @@ -36,15 +42,22 @@ func GetConfig() (*Config, error) { if config.TrustedHost == "" { return nil, errors.New("Envar not set: TRUSTED_HOST") } + if config.TursoURL == "" { + return nil, errors.New("Envar not set: TURSO_DATABASE_URL") + } + if config.TursoToken == "" { + return nil, errors.New("Envar not set: TURSO_AUTH_TOKEN") + } return config, nil } -func NewServer(config *Config) http.Handler { +func NewServer(config *Config, conn *sql.DB) http.Handler { mux := http.NewServeMux() addRoutes( mux, config, + conn, ) var handler http.Handler = mux handler = middleware.Logging(handler)