Compare commits

...

19 Commits

Author SHA1 Message Date
1639dd787f Add template function experimental cmd 2024-08-14 12:32:31 -04:00
9c299bc709 Merge from main 2024-08-14 12:30:04 -04:00
56f98a9c14 Set cookie to http only 2024-08-13 11:39:19 -04:00
4cf50a7d81 Use a custom template function for csrf protection 2024-08-13 06:58:02 -04:00
8bc58eedbe Add request data to template Execute function 2024-08-12 18:55:57 -04:00
da5eeb3f0f Refactor FromFile constructor 2024-08-12 18:04:22 -04:00
de681c1ac3 Add csrf protection 2024-08-11 20:23:43 -04:00
faf9139d79 Add cookie 2024-08-08 15:44:19 -04:00
951c081680 Add sign in handler 2024-08-08 12:22:56 -04:00
c4b5dcedf9 Add user authentication function 2024-08-07 19:33:41 -04:00
2d53824194 Add Sign in page 2024-08-07 19:09:34 -04:00
7d234c5aad Connect users controller to db 2024-08-07 12:19:32 -04:00
fef066b449 Add user model 2024-08-07 00:15:41 -04:00
166049f4ff bcrypt 2024-08-06 12:35:27 -04:00
4f815151eb Sql 2024-08-05 22:42:28 -04:00
1fcbac610f Add query param autofill to form 2024-08-04 08:49:58 -04:00
1156cabe05 Add user signup controller 2024-08-03 07:34:27 -04:00
82b954af2e Add css to signup page 2024-08-01 22:40:41 -04:00
8eb491fa97 Add signup page 2024-08-01 21:31:39 -04:00
14 changed files with 795 additions and 28 deletions

41
cmd/bcrypt/bcrypt.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"os"
"golang.org/x/crypto/bcrypt"
)
func main() {
switch os.Args[1] {
case "hash":
hash(os.Args[2])
case "compare":
compare(os.Args[2], os.Args[3])
default:
fmt.Printf("Invalid command: %v\n", os.Args[1])
}
}
func hash(password string) {
fmt.Printf("Hash the password %q\n", password)
hashedBytes, err := bcrypt.GenerateFromPassword(
[]byte(password), bcrypt.DefaultCost)
if err != nil {
fmt.Printf("error hashing: %v\n", err)
return
}
hash := string(hashedBytes)
fmt.Println(hash)
}
func compare(password, hash string) {
fmt.Printf("Compare the password %q with the hash %q\n", password, hash)
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err != nil {
fmt.Printf("Password is invalid: %v\n", password)
return
}
fmt.Println("Password is correct!")
}

144
cmd/exp/exp.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"database/sql"
"fmt"
_ "github.com/jackc/pgx/v4/stdlib"
)
func AddUser(db *sql.DB, name string, email string) (error, int) {
row := db.QueryRow(`
INSERT INTO users(name, email)
VALUES($1, $2)
RETURNING id;
`, name, email)
var id int
err := row.Scan(&id)
return err, id
}
func QueryUser(db *sql.DB, id int) (error, string, string) {
row := db.QueryRow(`
SELECT name, email
FROM users
WHERE id=$1;`, id)
var name, email string
err := row.Scan(&name, &email)
return err, name, email
}
type Order struct {
ID int
UID int
Amount int
Desc string
}
func AddOrder(db *sql.DB, uid int, amount int, desc string) (error, int) {
row := db.QueryRow(`
insert into orders(user_id, amount, description)
values($1, $2, $3)
returning id;
`, uid, amount, desc)
var id int
err := row.Scan(&id)
return err, id
}
func QueryOrders(db *sql.DB, uid int) ([]Order, error) {
var orders []Order
rows, err := db.Query(`
select id, amount, description from orders where user_id=$1
`, uid)
if err != nil {
return orders, err
}
defer rows.Close()
for rows.Next() {
var order Order
order.UID = uid
err := rows.Scan(&order.ID, &order.Amount, &order.Desc)
if err != nil {
return orders, err
}
orders = append(orders, order)
}
err = rows.Err()
return orders, err
}
func main() {
db, err := sql.Open("pgx", "host=localhost port=5432 user=baloo dbname=lenslocked sslmode=disable")
if err != nil {
panic(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
panic(err)
}
fmt.Println("Connected!")
_, err = db.Exec(`
create table if not exists users (
id serial primary key,
name text,
email text not null
);
create table if not exists orders (
id serial primary key,
user_id int not null,
amount int,
description text
);`)
if err != nil {
panic(err)
}
fmt.Println("Tables created")
var name string
var email string
// Create User
/*
name = "Jon Rob Baker"
email = "demo@user.com"
err, id := AddUser(db, name, email)
if err != nil {
panic(err)
}
fmt.Println("User ", id, " created")
*/
// Query User
err, name, email = QueryUser(db, 5)
if err == sql.ErrNoRows {
fmt.Println("Error, no rows!")
}
if err != nil {
panic(err)
}
fmt.Printf("User information: name=%s, email=%s\n", name, email)
uid := 5
// Create fake orders
/*
for i := 1; i <= 5; i++ {
amount := i * 100
desc := fmt.Sprintf("Fake order #%d", i)
err, _ := AddOrder(db, uid, amount, desc)
if err != nil {
panic(err)
}
}
fmt.Println("Created fake orders")
*/
// Query Orders
orders, err := QueryOrders(db, uid)
if err != nil {
panic(err)
}
fmt.Println("Orders: ", orders)
}

26
cmd/tplfn/tplfn.go Normal file
View File

@@ -0,0 +1,26 @@
package main
import (
"fmt"
"html/template"
"os"
)
func main() {
tplstr := "hello {{myFunc}}\n"
fmap := template.FuncMap{}
fmap["myFunc"] = func() (string, error) {
return "world", fmt.Errorf("hi")
}
tpl, err := template.New("demo").Funcs(fmap).Parse(tplstr)
if err != nil {
panic(fmt.Errorf("Error creating template: %v", err))
}
err = tpl.Execute(os.Stdout, nil)
if err != nil {
panic(err)
}
}

View File

@@ -4,19 +4,21 @@ import (
"git.kealoha.me/lks/lenslocked/templates" "git.kealoha.me/lks/lenslocked/templates"
"git.kealoha.me/lks/lenslocked/views" "git.kealoha.me/lks/lenslocked/views"
"net/http" "net/http"
"strings"
) )
func StaticTemplate(templatePath ...string) http.HandlerFunc { type Template interface {
Execute(w http.ResponseWriter, r *http.Request, data interface{})
}
func StaticController(templatePath ...string) http.HandlerFunc {
tpl := views.Must(views.FromFS(templates.FS, templatePath...)) tpl := views.Must(views.FromFS(templates.FS, templatePath...))
var testWriter strings.Builder err := tpl.TestTemplate(nil)
err := tpl.ExecuteWriter(&testWriter, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return func(w http.ResponseWriter, r *http.Request) { tpl.Execute(w, nil) } return func(w http.ResponseWriter, r *http.Request) { tpl.Execute(w, r, nil) }
} }
func FAQ(templatePath ...string) http.HandlerFunc { func FAQ(templatePath ...string) http.HandlerFunc {
@@ -36,13 +38,12 @@ func FAQ(templatePath ...string) http.HandlerFunc {
tpl := views.Must(views.FromFS(templates.FS, templatePath...)) tpl := views.Must(views.FromFS(templates.FS, templatePath...))
var testWriter strings.Builder err := tpl.TestTemplate(nil)
err := tpl.ExecuteWriter(&testWriter, nil)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, questions) tpl.Execute(w, r, questions)
} }
} }

104
controllers/users.go Normal file
View File

@@ -0,0 +1,104 @@
package controllers
import (
"fmt"
"net/http"
"git.kealoha.me/lks/lenslocked/models"
"git.kealoha.me/lks/lenslocked/templates"
"git.kealoha.me/lks/lenslocked/views"
)
type Users struct {
Templates struct {
Signup Template
Signin Template
}
UserService *models.UserService
}
func (u Users) GetSignup(w http.ResponseWriter, r *http.Request) {
var data struct {
Email string
}
data.Email = r.FormValue("email")
u.Templates.Signup.Execute(w, r, data)
}
func (u Users) PostSignup(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
password := r.FormValue("password")
user, err := u.UserService.Create(email, password)
if err != nil {
fmt.Println(err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "User created: %+v", user)
}
func (u Users) GetSignin(w http.ResponseWriter, r *http.Request) {
var data struct {
Email string
}
data.Email = r.FormValue("email")
u.Templates.Signin.Execute(w, r, data)
}
func (u Users) PostSignin(w http.ResponseWriter, r *http.Request) {
var data struct {
Email string
Password string
}
data.Email = r.FormValue("email")
data.Password = r.FormValue("password")
user, err := u.UserService.Authenticate(data.Email, data.Password)
if err != nil {
fmt.Println(err)
http.Error(w, "Something went wrong.", http.StatusInternalServerError)
return
}
// Bad cookie
cookie := http.Cookie{
Name: "bad",
Value: user.Email,
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, &cookie)
fmt.Fprintf(w, "User authenticated: %+v", user)
}
func (u Users) CurrentUser(w http.ResponseWriter, r *http.Request) {
email, err := r.Cookie("bad")
if err != nil {
fmt.Fprint(w, "The bad cookie could not be read.")
return
}
fmt.Fprintf(w, "Bad cookie: %s\n", email.Value)
}
func WithTemplates(user_service *models.UserService, signup Template, signin Template) Users {
u := Users{}
u.Templates.Signup = signup
u.Templates.Signin = signin
u.UserService = user_service
return u
}
func Default(user_service *models.UserService, templatePath ...string) Users {
signup_tpl := views.Must(views.FromFS(templates.FS, "signup.gohtml", "tailwind.gohtml"))
signin_tpl := views.Must(views.FromFS(templates.FS, "signin.gohtml", "tailwind.gohtml"))
err := signup_tpl.TestTemplate(nil)
if err != nil {
panic(err)
}
err = signin_tpl.TestTemplate(nil)
if err != nil {
panic(err)
}
return WithTemplates(user_service, signup_tpl, signin_tpl)
}

19
go.mod
View File

@@ -2,4 +2,21 @@ module git.kealoha.me/lks/lenslocked
go 1.22.5 go 1.22.5
require github.com/go-chi/chi/v5 v5.1.0 // indirect require (
github.com/go-chi/chi/v5 v5.1.0
github.com/jackc/pgx/v4 v4.18.3
golang.org/x/crypto v0.26.0
)
require (
github.com/gorilla/csrf v1.7.2
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
golang.org/x/text v0.17.0 // indirect
)

186
go.sum
View File

@@ -1,2 +1,188 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
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/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

47
main.go
View File

@@ -1,27 +1,68 @@
package main package main
import ( import (
"database/sql"
"fmt" "fmt"
"net/http" "net/http"
"os"
ctrlrs "git.kealoha.me/lks/lenslocked/controllers" ctrlrs "git.kealoha.me/lks/lenslocked/controllers"
"git.kealoha.me/lks/lenslocked/models"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/gorilla/csrf"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
_ "github.com/jackc/pgx/v4/stdlib"
) )
const DEBUG bool = true
func notFoundHandler(w http.ResponseWriter, r *http.Request) { func notFoundHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf8") w.Header().Set("Content-Type", "text/html; charset=utf8")
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "404 page not found") fmt.Fprint(w, "404 page not found")
} }
func ConnectDB() *sql.DB {
db, err := sql.Open("pgx", os.Getenv("LENSLOCKED_DB_STRING"))
if err != nil {
panic(fmt.Sprint("Error connecting to database: %w", err))
}
err = db.Ping()
if err != nil {
panic(fmt.Sprint("Error connecting to database: %w", err))
}
return db
}
func main() { func main() {
csrfKey := []byte(os.Getenv("LENSLOCKED_CSRF_KEY"))
if len(csrfKey) < 32 {
panic("Error: bad csrf protection key")
}
db := ConnectDB()
defer db.Close()
userService := models.UserService{DB: db}
var usersCtrlr ctrlrs.Users = ctrlrs.Default(&userService)
r := chi.NewRouter() r := chi.NewRouter()
r.Use(middleware.Logger) r.Use(middleware.Logger)
r.Get("/", ctrlrs.StaticTemplate("home.gohtml", "tailwind.gohtml"))
r.Get("/contact", ctrlrs.StaticTemplate("contact.gohtml", "tailwind.gohtml")) r.Get("/", ctrlrs.StaticController("home.gohtml", "tailwind.gohtml"))
r.Get("/contact", ctrlrs.StaticController("contact.gohtml", "tailwind.gohtml"))
r.Get("/faq", ctrlrs.FAQ("faq.gohtml", "tailwind.gohtml")) r.Get("/faq", ctrlrs.FAQ("faq.gohtml", "tailwind.gohtml"))
r.Get("/signup", usersCtrlr.GetSignup)
r.Post("/signup", usersCtrlr.PostSignup)
r.Get("/signin", usersCtrlr.GetSignin)
r.Post("/signin", usersCtrlr.PostSignin)
r.Get("/user", usersCtrlr.CurrentUser)
r.NotFound(notFoundHandler) r.NotFound(notFoundHandler)
fmt.Println("Starting the server on :3000...") fmt.Println("Starting the server on :3000...")
http.ListenAndServe(":3000", r) http.ListenAndServe(":3000", csrf.Protect(csrfKey, csrf.Secure(!DEBUG))(r))
} }

5
models/sql/users.sql Normal file
View File

@@ -0,0 +1,5 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL
);

64
models/user.go Normal file
View File

@@ -0,0 +1,64 @@
package models
import (
"database/sql"
"fmt"
"strings"
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID int
Email string
PasswordHash string
}
type UserService struct {
DB *sql.DB
}
func (us *UserService) Create(email, password string) (*User, error) {
email = strings.ToLower(email)
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
passwordHash := string(hashedBytes)
user := User{
Email: email,
PasswordHash: passwordHash,
}
row := us.DB.QueryRow(`
INSERT INTO users (email, password_hash)
VALUES ($1, $2) RETURNING id
`, email, passwordHash)
err = row.Scan(&user.ID)
if err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return &user, nil
}
func (us UserService) Authenticate(email, password string) (*User, error) {
user := User{
Email: strings.ToLower(email),
}
row := us.DB.QueryRow(`
SELECT id, password_hash
FROM users WHERE email=$1
`, email)
err := row.Scan(&user.ID, &user.PasswordHash)
if err != nil {
return nil, fmt.Errorf("authenticate: %w", err)
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
if err != nil {
return nil, fmt.Errorf("authenticate: %w", err)
}
return &user, nil
}

65
templates/signin.gohtml Normal file
View File

@@ -0,0 +1,65 @@
<!doctype html>
<html>
{{template "head" .}}
<body class="min-h-screen bg-gray-100">
{{template "header".}}
<main class="py-12 flex justify-center">
<div class="px-8 py-8 bg-white rounded shadow">
<h1 class="pt-4 pb-8 text-center text-3xl font-bold text-gray-900">
Welcome back!
</h1>
<form action="/signin" method="post">
{{csrfField}}
<div class="py-2">
<label for="email" class="text-sm font-semibold text-gray-800">
Email Address
</label>
<input
name="email"
id="email"
type="email"
placeholder="Email address"
required
autocomplete="email"
class="w-full px-3 py-2 border border-gray-300 placeholder-gray-500
text-gray-800 rounded"
value="{{.Email}}"
{{if not .Email}}autofocus{{end}}
/>
</div>
<div class="py-2">
<label for="password" class="text-sm font-semibold text-gray-800">
Password
</label>
<input
name="password"
id="password"
type="password"
placeholder="Password"
required
class="w-full px-3 py-2 border border-gray-300 placeholder-gray-500
text-gray-800 rounded"
{{if .Email}}autofocus{{end}}
/>
</div>
<div class="py-4">
<button class="w-full py-4 px-2 bg-indigo-600 hover:bg-indigo-700
text-white rounded font-bold text-lg">
Sign in
</button>
</div>
<div class="py-2 w-full flex justify-between">
<p class="text-xs text-gray-500">
Need an account?
<a href="/signup" class="underline">Sign up</a>
</p>
<p class="text-xs text-gray-500">
<a href="/reset-pw" class="underline">Forgot your password?</a>
</p>
</div>
</form>
</div>
</main>
{{template "footer" .}}
</body>
</html>

53
templates/signup.gohtml Normal file
View File

@@ -0,0 +1,53 @@
<!doctype html>
<html>
{{template "head" .}}
<body class="min-h-screen bg-gray-100">
{{template "header".}}
<main class="px-6">
<div class="py-12 flex justify-center">
<div class="px-8 py-8 bg-white rounded shadow">
<h1 class="pt-4 pb-8 text-center text-3xl font-bold text-gray-900">
Sign Up!
</h1>
<form action="/signup" method="post">
{{csrfField}}
<div>
<label for="signupEmail" class="text-sm font-semibold text-gray-800">Email Address</label>
<input name="email" id="signupEmail" type="email" placeholder="Email address" required autocomplete="email"
class="w-full px-3 py-2 boarder boarder-gray-300 placeholder-gray-500 text-gray-800 rounded"
value="{{.Email}}"
{{if not .Email}}autofocus{{end}}
/>
</div>
<div>
<label for="signupPassword" class="text-sm font-semibold text-gray-800">Password</label>
<input name="password" id="signupPassword" type="password" placeholder="password" required
class="w-full px-3 py-2 boarder boarder-gray-300 placeholder-gray-500 text-gray-800 rounded"
{{if .Email}}autofocus{{end}}
/>
</div>
<div>
<label for="referalCode" class="text-sm font-semibold text-gray-800">Referal Code</label>
<input name="referalCode" id="referalCode" type="text" placeholder="Referal Code" required
class="w-full px-3 py-2 boarder boarder-gray-300 placeholder-gray-500 text-gray-800 rounded"
/>
</div>
<div>
<button type="submit" class="w-full py-4 px-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded font-bold text-lg">Sign up</button>
</div>
<div class="py-2 w-full flex justify-between">
<p class="text-xs text-gray-500">
Already have an account?
<a href="/signin" class="underline">Sign in</a>
</p>
<p class="text-xs text-gray-500">
<a href="/reset-pw" class="underline">Forgot your password?</a>
</p>
</div>
</form>
</div>
</div>
</main>
{{template "footer" .}}
</body>
</html>

View File

@@ -16,8 +16,8 @@
<a class="text-base font-semibold hover:text-blue-100 pr-8" href="/faq">FAQ</a> <a class="text-base font-semibold hover:text-blue-100 pr-8" href="/faq">FAQ</a>
</div> </div>
<div class="space-x-4"> <div class="space-x-4">
<a href="#">Sign in</a> <a href="/signin">Sign in</a>
<a href="#" clss="px-4 py-2 bg-blue-700 hover:bg-blue-600 rounded">Sign up</a> <a href="/signup" clss="px-4 py-2 bg-blue-700 hover:bg-blue-600 rounded">Sign up</a>
</div> </div>
</nav> </nav>
</header> </header>

View File

@@ -3,40 +3,60 @@ package views
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"io"
"io/fs" "io/fs"
"log" "log"
"net/http" "net/http"
"os"
"strings"
"github.com/gorilla/csrf"
) )
type Template struct { type Template struct {
htmlTpl *template.Template htmlTpl *template.Template
} }
func (t Template) Execute(w http.ResponseWriter, data interface{}) { func (t Template) Execute(w http.ResponseWriter, r *http.Request, data interface{}) {
tpl, err := t.htmlTpl.Clone()
if err != nil {
log.Printf("Template Clone Error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
tpl = tpl.Funcs(template.FuncMap{
"csrfField": func() template.HTML { return csrf.TemplateField(r) },
})
w.Header().Set("Content-Type", "text/html; charset=utf8") w.Header().Set("Content-Type", "text/html; charset=utf8")
err := t.htmlTpl.Execute(w, data) err = tpl.Execute(w, data)
if err != nil { if err != nil {
log.Printf("Error executing template: %v", err) log.Printf("Error executing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
} }
func (t Template) ExecuteWriter(w io.Writer, data interface{}) error { func (t Template) TestTemplate(data interface{}) error {
return t.htmlTpl.Execute(w, data) var testWriter strings.Builder
tpl, err := t.htmlTpl.Clone()
if err != nil {
return err
}
return tpl.Execute(&testWriter, data)
} }
func FromFile(filepath string) (Template, error) { func FromFile(pattern ...string) (Template, error) {
tpl, err := template.ParseFiles(filepath) fs := os.DirFS(".")
if err != nil { return FromFS(fs, pattern...)
return Template{}, fmt.Errorf("Error parsing template: %v", err)
}
return Template{
htmlTpl: tpl,
}, nil
} }
func FromFS(fs fs.FS, pattern ...string) (Template, error) { func FromFS(fs fs.FS, pattern ...string) (Template, error) {
tpl, err := template.ParseFS(fs, pattern...) tpl := template.New(pattern[0])
tpl = tpl.Funcs(template.FuncMap{
"csrfField": func() template.HTML {
return `<div class="hidden">STUB: PLACEHOLDER</div>`
},
})
tpl, err := tpl.ParseFS(fs, pattern...)
if err != nil { if err != nil {
return Template{}, fmt.Errorf("Error parsing template: %v", err) return Template{}, fmt.Errorf("Error parsing template: %v", err)
} }