Added database support and update login to utilize db

This commit is contained in:
2025-02-08 19:39:44 +11:00
parent a842d09f96
commit 597fc6f072
8 changed files with 151 additions and 32 deletions

19
db/connection.go Normal file
View File

@@ -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
}

62
db/users.go Normal file
View File

@@ -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
}

8
go.mod
View File

@@ -6,4 +6,12 @@ require (
github.com/a-h/templ v0.3.833 github.com/a-h/templ v0.3.833
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.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
) )

10
go.sum
View File

@@ -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 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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=

View File

@@ -1,37 +1,32 @@
package handlers package handlers
import ( import (
"errors" "database/sql"
"fmt" "fmt"
"net/http" "net/http"
"projectreshoot/cookies" "projectreshoot/cookies"
"projectreshoot/db"
"projectreshoot/view/component/form" "projectreshoot/view/component/form"
"projectreshoot/view/page" "projectreshoot/view/page"
"github.com/pkg/errors"
) )
// TODO: here for testing only, move to database func validateLogin(conn *sql.DB, r *http.Request) (db.User, error) {
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) {
formUsername := r.FormValue("username") formUsername := r.FormValue("username")
formPassword := r.FormValue("password") formPassword := r.FormValue("password")
// TODO: search database for username
validUser := testUser() user, err := db.GetUserFromUsername(conn, formUsername)
// TODO: check password is valid if err != nil {
if formUsername != validUser.username || formPassword != validUser.password { return db.User{}, errors.Wrap(err, "db.GetUserFromUsername")
return 0, errors.New("Username or password incorrect")
} }
// 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 { 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( return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
r.ParseForm() r.ParseForm()
userID, err := validateLogin(r) user, err := validateLogin(conn, r)
if err != nil { if err != nil {
// TODO: add debug log
fmt.Printf("Login failed: %s\n", err)
form.LoginForm(err.Error()).Render(r.Context(), w) form.LoginForm(err.Error()).Render(r.Context(), w)
return return
} }
// TODO: login success, use the userID to set the session // TODO: login success, use the userID to set the session
rememberMe := checkRememberMe(r) 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) pageFrom := cookies.CheckPageFrom(w, r)
w.Header().Set("HX-Redirect", pageFrom) w.Header().Set("HX-Redirect", pageFrom)

14
main.go
View File

@@ -4,15 +4,18 @@ import (
"context" "context"
"embed" "embed"
"fmt" "fmt"
"github.com/pkg/errors"
"io" "io"
"net" "net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"projectreshoot/server"
"sync" "sync"
"time" "time"
"projectreshoot/db"
"projectreshoot/server"
"github.com/pkg/errors"
) )
func run(ctx context.Context, w io.Writer) error { 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") 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{ httpServer := &http.Server{
Addr: net.JoinHostPort(config.Host, config.Port), Addr: net.JoinHostPort(config.Host, config.Port),
Handler: srv, Handler: srv,

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"database/sql"
"net/http" "net/http"
"projectreshoot/handlers" "projectreshoot/handlers"
"projectreshoot/view/page" "projectreshoot/view/page"
@@ -9,6 +10,7 @@ import (
func addRoutes( func addRoutes(
mux *http.ServeMux, mux *http.ServeMux,
config *Config, config *Config,
conn *sql.DB,
) { ) {
// Static files // Static files
mux.Handle("GET /static/", http.StripPrefix("/static/", handlers.HandleStatic())) mux.Handle("GET /static/", http.StripPrefix("/static/", handlers.HandleStatic()))
@@ -21,5 +23,5 @@ func addRoutes(
// Login page and handlers // Login page and handlers
mux.Handle("GET /login", handlers.HandleLoginPage(config.TrustedHost)) mux.Handle("GET /login", handlers.HandleLoginPage(config.TrustedHost))
mux.Handle("POST /login", handlers.HandleLoginRequest()) mux.Handle("POST /login", handlers.HandleLoginRequest(conn))
} }

View File

@@ -1,19 +1,23 @@
package server package server
import ( import (
"database/sql"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"projectreshoot/middleware" "projectreshoot/middleware"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
type Config struct { type Config struct {
TrustedHost string
Host string Host string
Port string Port string
TrustedHost string
TursoURL string
TursoToken string
} }
func GetConfig() (*Config, error) { func GetConfig() (*Config, error) {
@@ -26,6 +30,8 @@ func GetConfig() (*Config, error) {
Host: os.Getenv("HOST"), Host: os.Getenv("HOST"),
Port: os.Getenv("PORT"), Port: os.Getenv("PORT"),
TrustedHost: os.Getenv("TRUSTED_HOST"), TrustedHost: os.Getenv("TRUSTED_HOST"),
TursoURL: os.Getenv("TURSO_DATABASE_URL"),
TursoToken: os.Getenv("TURSO_AUTH_TOKEN"),
} }
if config.Host == "" { if config.Host == "" {
return nil, errors.New("Envar not set: HOST") return nil, errors.New("Envar not set: HOST")
@@ -36,15 +42,22 @@ func GetConfig() (*Config, error) {
if config.TrustedHost == "" { if config.TrustedHost == "" {
return nil, errors.New("Envar not set: TRUSTED_HOST") 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 return config, nil
} }
func NewServer(config *Config) http.Handler { func NewServer(config *Config, conn *sql.DB) http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
addRoutes( addRoutes(
mux, mux,
config, config,
conn,
) )
var handler http.Handler = mux var handler http.Handler = mux
handler = middleware.Logging(handler) handler = middleware.Logging(handler)