diff options
| author | Alexander Kavon <hawk@alexkavon.com> | 2023-11-29 02:05:38 -0500 |
|---|---|---|
| committer | Alexander Kavon <hawk@alexkavon.com> | 2023-11-29 02:05:38 -0500 |
| commit | 1c32e1497525bc6f609898f0f79bd6cc3e6d92fb (patch) | |
| tree | 99abc1c2690b151bfb3d45e9f76a8770b3597e7a | |
| parent | 5e26ebc96bd41f9d232573d873cb6cb55f0050c4 (diff) | |
user create/insert into users table, pgxpool defer moved to top of app, me.tmpl.html, secret hashing with argon2id, updated migrations
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 30 | ||||
| -rw-r--r-- | migrations/002_create_users_table.sql | 5 | ||||
| -rw-r--r-- | src/db/db.go | 30 | ||||
| -rw-r--r-- | src/main.go | 4 | ||||
| -rw-r--r-- | src/server/server.go | 31 | ||||
| -rw-r--r-- | src/server/session.go | 9 | ||||
| -rw-r--r-- | src/user/routes.go | 56 | ||||
| -rw-r--r-- | src/user/user.go | 63 | ||||
| -rw-r--r-- | ui/pages/user/create.tmpl.html | 8 | ||||
| -rw-r--r-- | ui/pages/user/me.tmpl.html | 5 |
11 files changed, 218 insertions, 33 deletions
@@ -5,14 +5,22 @@ go 1.21.3 require ( github.com/BurntSushi/toml v1.3.2 github.com/go-chi/chi/v5 v5.0.10 + github.com/go-playground/validator/v10 v10.16.0 + github.com/google/uuid v1.4.0 github.com/jackc/pgx/v5 v5.5.0 + golang.org/x/crypto v0.16.0 ) require ( + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect - golang.org/x/crypto v0.14.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect ) @@ -3,8 +3,20 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= +github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -13,17 +25,27 @@ github.com/jackc/pgx/v5 v5.5.0 h1:NxstgwndsTRy7eq9/kqYc/BZh5w2hHJV86wjvO+1xPw= github.com/jackc/pgx/v5 v5.5.0/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/migrations/002_create_users_table.sql b/migrations/002_create_users_table.sql index a561a05..d2322f9 100644 --- a/migrations/002_create_users_table.sql +++ b/migrations/002_create_users_table.sql @@ -1,7 +1,8 @@ CREATE TABLE users( id SERIAL NOT NULL PRIMARY KEY, - username VARCHAR(50) NOT NULL, - secret VARCHAR(255) NOT NULL, + username VARCHAR(50) UNIQUE NOT NULL, + secret VARCHAR(256) NOT NULL, + email VARCHAR(256) UNIQUE NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/src/db/db.go b/src/db/db.go index 2b72382..6f617c2 100644 --- a/src/db/db.go +++ b/src/db/db.go @@ -2,8 +2,11 @@ package db import ( "context" + "fmt" "log" + "strings" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "gitlab.com/alexkavon/newsstand/src/conf" @@ -18,7 +21,6 @@ func NewDb(config *conf.Conf) *Database { if err != nil { log.Fatal(err) } - defer pool.Close() var testquery string err = pool.QueryRow(context.Background(), "select 'Hello, PostgreSQL!'").Scan(&testquery) @@ -30,3 +32,29 @@ func NewDb(config *conf.Conf) *Database { p: pool, } } + +func (d *Database) InsertTable(table string, columns []string, values pgx.NamedArgs) error { + columnstr := stringifyColumns(columns, "") + valuesstr := stringifyColumns(columns, "@") + query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s) RETURNING *", table, columnstr, valuesstr) + log.Println(query, values) + ctx := context.Background() + row, err := d.p.Exec(ctx, query, values) + if err != nil { + return err + } + log.Println("Row", row) + return nil +} + +func (d *Database) Close() { + d.p.Close() +} + +func stringifyColumns(columns []string, prefix string) string { + ncolumns := []string{} + for _, v := range columns { + ncolumns = append(ncolumns, fmt.Sprintf("%s%s", prefix, v)) + } + return strings.Join(ncolumns, ", ") +} diff --git a/src/main.go b/src/main.go index 0687aae..4dc2caf 100644 --- a/src/main.go +++ b/src/main.go @@ -12,7 +12,9 @@ func main() { config := conf.NewConf() // connect database // start server - s := server.NewServer(config, db.NewDb(config)) + database := db.NewDb(config) + defer database.Close() + s := server.NewServer(config, database) s.BuildUi() routers := []server.Routes{ user.Routes, diff --git a/src/server/server.go b/src/server/server.go index 9cb2a3f..c483069 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -4,23 +4,26 @@ import ( "net/http" "github.com/go-chi/chi/v5" + "github.com/google/uuid" "gitlab.com/alexkavon/newsstand/src/conf" "gitlab.com/alexkavon/newsstand/src/db" ) type Server struct { - Router *chi.Mux - Db *db.Database - Config *conf.Conf - Ui Ui + Router *chi.Mux + Db *db.Database + Config *conf.Conf + Ui Ui + Sessions map[string]session } func NewServer(config *conf.Conf, database *db.Database) *Server { return &Server{ - Router: NewRouter(config), - Db: database, - Config: config, - Ui: NewUi(config), + Router: NewRouter(config), + Db: database, + Config: config, + Ui: NewUi(config), + Sessions: map[string]session{}, } } @@ -37,3 +40,15 @@ func (s *Server) RegisterRoutes(routes Routes) { func (s *Server) Serve() { http.ListenAndServe(":"+s.Config.Server.Port, s.Router) } + +func (s *Server) NewSession(w http.ResponseWriter, username string) { + token := uuid.NewString() + s.Sessions[token] = session{ + username: username, + } + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: token, + }) +} diff --git a/src/server/session.go b/src/server/session.go new file mode 100644 index 0000000..3a12215 --- /dev/null +++ b/src/server/session.go @@ -0,0 +1,9 @@ +package server + +type session struct { + username string +} + +func (s *session) Username() string { + return s.username +} diff --git a/src/user/routes.go b/src/user/routes.go index f826822..64018d0 100644 --- a/src/user/routes.go +++ b/src/user/routes.go @@ -1,9 +1,10 @@ package user import ( - "fmt" + "log" "net/http" + "github.com/go-playground/validator/v10" "gitlab.com/alexkavon/newsstand/src/server" ) @@ -11,7 +12,7 @@ var Routes = server.Routes{ server.Route{ Name: "Create", Method: "GET", - Path: "/user/create", + Path: "/u/create", HandlerFunc: Create, }, server.Route{ @@ -23,19 +24,19 @@ var Routes = server.Routes{ server.Route{ Name: "LoginForm", Method: "GET", - Path: "/user/auth", + Path: "/u/auth", HandlerFunc: LoginForm, }, server.Route{ Name: "Authenticate", Method: "POST", - Path: "/user/auth", + Path: "/u/auth", HandlerFunc: Authenticate, }, server.Route{ Name: "Me", Method: "GET", - Path: "/user/me", + Path: "/u/me", AuthRequired: true, HandlerFunc: Show, }, @@ -50,8 +51,36 @@ func Create(s *server.Server) http.HandlerFunc { func Store(s *server.Server) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.ParseForm() - fmt.Println(r.PostForm) - s.Ui.RenderTemplate(w, "core", "messages", &struct{ Message string }{Message: "Congrats"}) + + user := &User{ + Db: s.Db, + Username: r.PostFormValue("username"), + Secret: r.PostFormValue("secret"), + Email: r.PostFormValue("email"), + } + // Validate User Input + v := validator.New() + err := v.Struct(user) + if err != nil { + log.Println("Validator failed", err.(validator.ValidationErrors)) + } + + // Hash secret + err = user.HashSecret() + if err != nil { + log.Println("Hash failure", err) + } + + // Store user + err = user.Insert() + if err != nil { + log.Println("Insert Error", err) + } + // Send email validation + // Create cookie session + s.NewSession(w, user.Username) + // Redirect to user profile + http.Redirect(w, r, "/u/me", http.StatusSeeOther) } } @@ -66,5 +95,16 @@ func Authenticate(s *server.Server) http.HandlerFunc { } func Show(s *server.Server) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) {} + return func(w http.ResponseWriter, r *http.Request) { + token, err := r.Cookie("session_token") + if err != nil { + s.Ui.Render(w, "user/login", &struct{ Message string }{"You are not logged in! Missing Cookie"}) + } + session, ok := s.Sessions[token.Value] + if !ok { + s.Ui.Render(w, "user/login", &struct{ Message string }{"You are not logged in! With Session."}) + } + + s.Ui.Render(w, "user/me", &struct{ Message, Username string }{"Congrats on getting this far!", session.Username()}) + } } diff --git a/src/user/user.go b/src/user/user.go index 83e39e3..e35c7c5 100644 --- a/src/user/user.go +++ b/src/user/user.go @@ -1,18 +1,73 @@ package user import ( - "errors" + "crypto/rand" + "encoding/base64" + "fmt" "time" + + "github.com/jackc/pgx/v5" + "gitlab.com/alexkavon/newsstand/src/db" + "golang.org/x/crypto/argon2" ) type User struct { Id int64 - Username string + Username string `validate:"required,max=50"` + Secret string `validate:"required,min=8,max=128"` + Email string `validate:"required,email"` Karma uint64 UpdatedAt time.Time CreatedAt time.Time + hash string + Db *db.Database +} + +func (u *User) Insert() error { + err := u.Db.InsertTable( + "users", + []string{"username", "secret", "email"}, + pgx.NamedArgs{"username": u.Username, "secret": string(u.hash), "email": u.Email}, + ) + if err != nil { + return err + } + return nil } -func NewUser(username, secret string) error { - return errors.New("Not Implemented") +func (u *User) HashSecret() error { + hashconf := &struct { + memory uint32 + iterations uint32 + parallelism uint8 + keyLength uint32 + saltLength uint32 + }{64 * 1024, 3, 2, 12, 16} + salt := make([]byte, hashconf.saltLength) + _, err := rand.Read(salt) + if err != nil { + return err + } + + hash := argon2.IDKey( + []byte(u.Secret), + salt, + hashconf.iterations, + hashconf.memory, + hashconf.parallelism, + hashconf.keyLength, + ) + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Hash := base64.RawStdEncoding.EncodeToString(hash) + encodedHash := fmt.Sprintf( + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, + hashconf.memory, + hashconf.iterations, + hashconf.parallelism, + b64Salt, + b64Hash, + ) + u.hash = encodedHash + return nil } diff --git a/ui/pages/user/create.tmpl.html b/ui/pages/user/create.tmpl.html index 5e1087a..47143c6 100644 --- a/ui/pages/user/create.tmpl.html +++ b/ui/pages/user/create.tmpl.html @@ -2,7 +2,7 @@ {{define "main"}} <h1>Create User</h1> - <form hx-post="/user" action="/user" method="POST" hx-target="#messages" hx-swap="outerHTML"> + <form action="/user" method="POST"> <label> Email <input type="email" placeholder="email" name="email" /> @@ -12,9 +12,9 @@ <input type="text" placeholder="username" name="username" /> </label> <label> - Password - <input type="password" placeholder="password" name="password" /> + Secret + <input type="password" placeholder="secret" name="secret" /> </label> - <button type="submit" hx-disabled-elt="this">Create</button> + <button type="submit">Create</button> </form> {{end}} diff --git a/ui/pages/user/me.tmpl.html b/ui/pages/user/me.tmpl.html new file mode 100644 index 0000000..3f3950c --- /dev/null +++ b/ui/pages/user/me.tmpl.html @@ -0,0 +1,5 @@ +{{ define "title"}}Profile{{end}} + +{{ define "main"}} +<h1>Welcome, {{.Username}}</h1> +{{end}} |
