Compare commits

...

6 Commits

Author SHA1 Message Date
30c552cf6c change postgres to docker 2024-07-30 20:03:44 +02:00
3eab2a3c9f add todo 2024-07-29 22:09:06 +02:00
5d225f0186 Use redis to store the sessions 2024-07-29 21:54:37 +02:00
52c3679158 Use cmd flags 2024-07-29 14:20:51 +02:00
aca8605870 fix a make reservation error 2024-07-29 14:04:43 +02:00
6631288843 add more tests 2024-07-29 13:52:10 +02:00
10 changed files with 158 additions and 15 deletions

View File

@ -2,6 +2,7 @@ package main
import (
"encoding/gob"
"flag"
"fmt"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/driver"
@ -14,7 +15,9 @@ import (
"os"
"time"
"github.com/alexedwards/scs/redisstore"
"github.com/alexedwards/scs/v2"
"github.com/gomodule/redigo/redis"
)
const portNumber = ":8080"
@ -55,15 +58,40 @@ func run() (*driver.DB, error) {
gob.Register(models.Restriction{})
gob.Register(map[string]int{})
// read flags
inProduction := flag.Bool("production", true, "Application is in production")
useCache := flag.Bool("cache", true, "Use template cache")
dbHost := flag.String("dbhost", "localhost", "Database host")
dbName := flag.String("dbname", "bookings", "Database name")
dbUser := flag.String("dbuser", os.Getenv("PGUSER"), "Database user")
dbPass := flag.String("dbpass", os.Getenv("PGPWD"), "Database password")
dbPort := flag.String("dbport", "5432", "Database port")
dbSSL := flag.String("dbssl", "disable", "Database ssl settings (disable, prefer, require)")
flag.Parse()
if *dbUser == "" {
fmt.Println("Missing required flags")
os.Exit(1)
}
mailChan := make(chan models.MailData)
app.MailChan = mailChan
listenForMail()
// change this to true when in production
app.InProduction = false
app.InProduction = *inProduction
// Establish connection pool to Redis.
pool := &redis.Pool{
MaxIdle: 10,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", "localhost:6379")
},
}
session = scs.New()
session.Store = redisstore.New(pool)
session.Lifetime = 24 * time.Hour
session.Cookie.Persist = true
session.Cookie.SameSite = http.SameSiteLaxMode
@ -73,7 +101,8 @@ func run() (*driver.DB, error) {
// connect to database
log.Println("Connecting to database...")
dsn := fmt.Sprintf("host=localhost port=5432 dbname=bookings user=%s password=%s", os.Getenv("PGUSER"), os.Getenv("PGPWD"))
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
*dbHost, *dbPort, *dbName, *dbUser, *dbPass, *dbSSL)
db, err := driver.ConnectSQL(dsn)
if err != nil {
log.Fatal("Cannot connect to database! Dying...")
@ -86,7 +115,7 @@ func run() (*driver.DB, error) {
return nil, err
}
app.TemplateCahce = tc
app.UseCache = false
app.UseCache = *useCache
infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
app.InfoLog = infoLog

View File

@ -38,7 +38,9 @@ func routes(app *config.AppConfig) http.Handler {
mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
mux.Route("/admin", func(mux chi.Router) {
// mux.Use(Auth)
if app.InProduction {
mux.Use(Auth)
}
mux.Get("/dashboard", handlers.Repo.AdminDashboard)
mux.Get("/reservations-new", handlers.Repo.AdminNewReservations)

View File

@ -0,0 +1,32 @@
# Use postgres/postgres user/password credentials
services:
db:
image: postgres
restart: always
# set shared memory limit when using docker-compose
shm_size: 128mb
# or set shared memory limit when deploy via swarm stack
#volumes:
# - type: tmpfs
# target: /dev/shm
# tmpfs:
# size: 134217728 # 128*2^20 bytes = 128Mb
environment:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
adminer:
image: adminer
restart: always
ports:
- 8081:8080
cache:
image: redis
restart: always
ports:
- 6379:6379
command: redis-server --save 60 1 --loglevel warning
volumes:
- cache:/data

View File

@ -23,3 +23,9 @@ services:
ports:
- 8081:8080
cache:
image: redis
restart: always
ports:
- 6379:6379
command: redis-server --save 60 1 --loglevel warning

7
go.mod
View File

@ -2,11 +2,12 @@ module go-udemy-web-1
go 1.21.0
// github.com/CloudyKit/jet --> Check this later
// Template: github.com/CloudyKit/jet --> Check this later
//
// ORM packages:
// upper/db https://github.com/upper/db
// gorm.io/gorm
// TODO Checkout sqlc and sqlx
// TODO Use default http package to rewrite the project with go 1.22.1
@ -17,10 +18,13 @@ require github.com/justinas/nosurf v1.1.1
require github.com/alexedwards/scs/v2 v2.8.0
require (
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/gomodule/redigo v1.9.2
github.com/jackc/pgconn v1.14.3
github.com/jackc/pgx/v5 v5.6.0
github.com/xhit/go-simple-mail v2.2.2+incompatible
golang.org/x/crypto v0.25.0
)
require (
@ -30,7 +34,6 @@ require (
github.com/jackc/pgproto3/v2 v2.3.3 // 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.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/text v0.16.0 // indirect
)

10
go.sum
View File

@ -1,3 +1,5 @@
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885 h1:UdHeICe7BgRbDq5yjA/yjCyJnohROtyD8PpJjhdAvF8=
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885/go.mod h1:ceKFatoD+hfHWWeHOAYue1J+XgOJjE7dw8l3JtIRTGY=
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@ -7,6 +9,9 @@ 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.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/gomodule/redigo v1.8.0/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
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=
@ -33,9 +38,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74=
github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=

View File

@ -139,15 +139,20 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
reservation.Email = form.Get("email")
reservation.Phone = form.Get("phone")
// TODO: Should I check the validity of reservation.StartDate / EndDate?
if !form.Valid() {
data := make(map[string]interface{})
data["reservation"] = reservation
sd := reservation.StartDate.Format("2006-01-02")
ed := reservation.EndDate.Format("2006-01-02")
stringMap := make(map[string]string)
stringMap["start_date"] = sd
stringMap["end_date"] = ed
render.Template(w, r, "make-reservation.page.tmpl", &models.TemplateData{
Data: data,
Form: form,
Data: data,
Form: form,
StringMap: stringMap,
})
return
}
@ -501,7 +506,6 @@ func (m *Repository) PostShowLogin(w http.ResponseWriter, r *http.Request) {
// Logout logs a user out
func (m *Repository) Logout(w http.ResponseWriter, r *http.Request) {
// TODO Use Redis to store the session. Check the documentation of scs package
m.App.Session.Destroy(r.Context())
m.App.Session.RenewToken(r.Context())
http.Redirect(w, r, "/user/login", http.StatusSeeOther)

View File

@ -574,6 +574,64 @@ func Test_BookRoom(t *testing.T) {
}
}
// }}}
// {{{ Login
var loginTests = []struct {
name string
email string
expectedStatusCode int
expectedHTML string
expectedLocation string
}{
{"valid-credentials", "a@b.c", http.StatusSeeOther, "", "/admin/dashboard"},
{"invalid-credentials", "invalid@b.c", http.StatusSeeOther, "", "/user/login"},
{"invalid-data", "a@b", http.StatusOK, `action="/user/login"`, ""},
}
func TestLogin(t *testing.T) {
// range through all tests
for _, e := range loginTests {
postedData := url.Values{}
postedData.Add("email", e.email)
postedData.Add("password", "password")
// create request
req, _ := http.NewRequest("POST", "/user/login", strings.NewReader(postedData.Encode()))
ctx := getCtx(req)
req = req.WithContext(ctx)
// set the header
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
// call the handler
handler := http.HandlerFunc(Repo.PostShowLogin)
handler.ServeHTTP(rr, req)
if rr.Code != e.expectedStatusCode {
t.Errorf("failed %s: expected code %d, but got %d", e.name, e.expectedStatusCode, rr.Code)
}
if e.expectedLocation != "" {
// get the URL from test
actualLoc, _ := rr.Result().Location()
if actualLoc.String() != e.expectedLocation {
t.Errorf("failed %s: expected location %s, but got location %s", e.name, e.expectedLocation, actualLoc.String())
}
}
// checking for expected values in HTML
if e.expectedHTML != "" {
// read the response body into a string
html := rr.Body.String()
if !strings.Contains(html, e.expectedHTML) {
t.Errorf("failed %s: expected html contains %s, but got html %s", e.name, e.expectedHTML, html)
}
}
}
}
// }}}
// {{{ Test Helpers

View File

@ -79,7 +79,10 @@ func (m *testDBRepo) UpdateUser(u models.User) error {
// Authenticate authenticates a user
func (m *testDBRepo) Authenticate(email, testPassword string) (int, string, error) {
return 1, "", nil
if email == "a@b.c" {
return 1, "", nil
}
return 0, "", errors.New("deliberate errors")
}
// AllReservations returns a slice of all reservations

2
run.sh
View File

@ -1,3 +1,3 @@
#!/bin/bash
go build -o bookings cmd/web/*go && ./bookings
go build -o bookings cmd/web/*go && ./bookings -cache=false -production=false