├── static └── css │ └── input.css ├── tailwind.config.js ├── go.mod ├── package.json ├── .gitignore ├── migrations └── 001_create_users_table.sql ├── internal ├── auth │ ├── token.go │ └── middleware.go ├── templates │ ├── edit_user_form.templ │ ├── components.templ │ ├── users.templ │ ├── login.templ │ ├── error_components.templ │ ├── layout.templ │ ├── userdetails.templ │ ├── home.templ │ ├── profile.templ │ └── register.templ ├── models │ └── user.go ├── app │ └── app.go ├── routes │ └── routes.go └── handlers │ ├── authentication.go │ └── handlers.go ├── .air.toml ├── go.sum ├── cmd └── server │ └── main.go ├── README.md └── Makefile copy /static/css/input.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./internal/templates/**/*.{html,templ,go}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lordaris/gotth-boilerplate 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/a-h/templ v0.3.833 7 | github.com/go-chi/chi/v5 v5.2.1 8 | github.com/jmoiron/sqlx v1.4.0 9 | github.com/joho/godotenv v1.5.1 10 | github.com/lib/pq v1.10.9 11 | golang.org/x/crypto v0.35.0 12 | ) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gotth-boilerplate", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "type": "commonjs", 13 | "dependencies": { 14 | "@tailwindcss/cli": "^4.0.7", 15 | "tailwindcss": "^4.0.7" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries and temporaries 2 | /tmp/ 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | Makefile 9 | .env 10 | 11 | # Test output 12 | *.test 13 | *.out 14 | 15 | # Dependencies 16 | /vendor/ 17 | /node_modules/ 18 | 19 | # Generated files 20 | /static/css/output.css 21 | *_templ.go 22 | 23 | # Logs 24 | *.log 25 | 26 | # Editor configuration 27 | .vscode/ 28 | .idea/ 29 | 30 | # System files 31 | .DS_Store 32 | -------------------------------------------------------------------------------- /migrations/001_create_users_table.sql: -------------------------------------------------------------------------------- 1 | -- Up 2 | CREATE TABLE IF NOT EXISTS users ( 3 | id SERIAL PRIMARY KEY, 4 | username VARCHAR(100) NOT NULL, 5 | password_hash VARCHAR(100) NOT NULL, 6 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 7 | ); 8 | 9 | CREATE TABLE IF NOT EXISTS tokens ( 10 | id SERIAL PRIMARY KEY, 11 | user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, 12 | token_hash BYTEA NOT NULL, 13 | plaintext_token VARCHAR(255) NOT NULL, 14 | expiry TIMESTAMP WITH TIME ZONE NOT NULL, 15 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 16 | ); 17 | 18 | CREATE INDEX IF NOT EXISTS tokens_token_hash_idx ON tokens(token_hash); 19 | CREATE INDEX IF NOT EXISTS tokens_user_id_idx ON tokens(user_id); 20 | 21 | -- Down 22 | --DROP TABLE IF EXISTS tokens; 23 | --DROP TABLE IF EXISTS users; 24 | 25 | -- INSERT INTO users (name, email) VALUES 26 | -- ('User 1', 'user1@example.com'), 27 | -- ('User 2', 'user2@example.com'), 28 | -- ('User 3', 'user3@example.com'); 29 | 30 | -------------------------------------------------------------------------------- /internal/auth/token.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "errors" 8 | "time" 9 | ) 10 | 11 | const ( 12 | TokenLength = 32 // Length in bytes 13 | ) 14 | 15 | func GenerateToken(expiry time.Duration) (string, []byte, time.Time, error) { 16 | tokenBytes := make([]byte, TokenLength) 17 | 18 | _, err := rand.Read(tokenBytes) 19 | if err != nil { 20 | return "", nil, time.Time{}, err 21 | } 22 | 23 | plaintextToken := base64.URLEncoding.EncodeToString(tokenBytes) 24 | 25 | // Hash the token 26 | hash := sha256.Sum256([]byte(plaintextToken)) 27 | tokenHash := hash[:] 28 | 29 | expiryTime := time.Now().Add(expiry) 30 | 31 | return plaintextToken, tokenHash, expiryTime, nil 32 | } 33 | 34 | func ValidateTokenPlaintext(plaintextToken string) error { 35 | _, err := base64.URLEncoding.DecodeString(plaintextToken) 36 | if err != nil { 37 | return errors.New("invalid token format") 38 | } 39 | return nil 40 | } 41 | 42 | func HashToken(plaintextToken string) []byte { 43 | hash := sha256.Sum256([]byte(plaintextToken)) 44 | return hash[:] 45 | } 46 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "templ generate && go build -o ./tmp/main ./cmd/server" 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go", "_templ\\.go$"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html", "templ"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | silent = false 40 | time = false 41 | 42 | [misc] 43 | clean_on_exit = false 44 | 45 | [proxy] 46 | app_port = 0 47 | enabled = false 48 | proxy_port = 0 49 | 50 | [screen] 51 | clear_on_rebuild = true 52 | keep_scroll = true 53 | -------------------------------------------------------------------------------- /internal/templates/edit_user_form.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lordaris/gotth-boilerplate/internal/models" 6 | ) 7 | 8 | templ EditUserForm(user models.User) { 9 | @Layout(fmt.Sprintf("Edit User: %s", user.Username)) { 10 |
11 |

Edit User

12 |
13 |
14 | 15 | 17 |
18 |
19 | 23 | 26 |
27 |
28 |
29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/templates/components.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | templ Alert(alertType string, message string) { 4 |
5 |

{ message }

6 |
7 | } 8 | 9 | templ FormInput(id string, name string, label string, inputType string, required bool, value string) { 10 |
11 | 12 | 13 |
14 | } 15 | 16 | templ Button(buttonType string, text string, classes string) { 17 | 20 | } 21 | 22 | templ LoadingSpinner() { 23 |
24 |
25 |
26 | } 27 | -------------------------------------------------------------------------------- /internal/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | type User struct { 11 | ID int `db:"id" json:"id"` 12 | Username string `db:"username" json:"username"` 13 | PasswordHash string `db:"password_hash" json:"-"` 14 | CreatedAt time.Time `db:"created_at" json:"created_at"` 15 | } 16 | 17 | type Token struct { 18 | ID int `db:"id" json:"-"` 19 | UserID int `db:"user_id" json:"-"` 20 | TokenHash []byte `db:"token_hash" json:"-"` 21 | PlaintextToken string `db:"plaintext_token" json:"-"` 22 | Expiry time.Time `db:"expiry" json:"expiry"` 23 | CreatedAt time.Time `db:"created_at" json:"-"` 24 | } 25 | 26 | func (u *User) SetPassword(password string) error { 27 | if len(password) < 8 { 28 | return errors.New("password must be at least 8 characters long") 29 | } 30 | 31 | hash, err := bcrypt.GenerateFromPassword([]byte(password), 12) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | u.PasswordHash = string(hash) 37 | return nil 38 | } 39 | 40 | func (u *User) CheckPassword(password string) (bool, error) { 41 | err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)) 42 | if err != nil { 43 | if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { 44 | return false, nil 45 | } 46 | return false, err 47 | } 48 | return true, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/templates/users.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lordaris/gotth-boilerplate/internal/models" 6 | ) 7 | 8 | templ UsersList(users []models.User) { 9 |
10 |

Users List

11 | if len(users) == 0 { 12 |
13 |

No users found

14 |
15 | } else { 16 |
17 | for _, user := range users { 18 |
19 |
20 |
21 | { string([]rune(user.Username)[0]) } 22 |
23 |
24 |

{ user.Username}

25 |
26 |
27 |
28 | 33 |
34 |
35 | } 36 |
37 |
38 |

Select a user to view details

39 |
40 | } 41 |
42 | } 43 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/jmoiron/sqlx" 8 | _ "github.com/lib/pq" 9 | ) 10 | 11 | type Config struct { 12 | DB struct { 13 | Host string 14 | Port string 15 | User string 16 | Password string 17 | Name string 18 | } 19 | Server struct { 20 | Port string 21 | } 22 | } 23 | 24 | // Application holds dependencies and configuration 25 | type Application struct { 26 | Config Config 27 | DB *sqlx.DB 28 | } 29 | 30 | // NewApplication creates a new Application instance 31 | func NewApplication(cfg Config) *Application { 32 | return &Application{ 33 | Config: cfg, 34 | } 35 | } 36 | 37 | // ConnectToDatabase establishes connection to PostgreSQL 38 | func (app *Application) ConnectToDatabase() error { 39 | dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", 40 | app.Config.DB.Host, 41 | app.Config.DB.User, 42 | app.Config.DB.Password, 43 | app.Config.DB.Name, 44 | app.Config.DB.Port, 45 | ) 46 | log.Printf("Connecting to database: %s@%s:%s/%s", 47 | app.Config.DB.User, 48 | app.Config.DB.Host, 49 | app.Config.DB.Port, 50 | app.Config.DB.Name) 51 | 52 | db, err := sqlx.Connect("postgres", dsn) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // Test the connection 58 | err = db.Ping() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | app.DB = db 64 | log.Println("Successfully connected to database") 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/templates/login.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | templ LoginPage() { 4 | @Layout("Login") { 5 |
6 |

Login

7 |
8 |
9 | 10 | 17 |
18 |
19 | 20 | 27 |
28 |
29 | 35 |
36 |
37 |
38 |

39 | Not registered? Sign up 40 |

41 |
42 |
43 | } 44 | } 45 | 46 | templ LoginError(message string) { 47 |
48 |

{ message }

49 |
50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= 4 | github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= 5 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 6 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 7 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 8 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 12 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 13 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 14 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 15 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 16 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 17 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 18 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 19 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 20 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 21 | -------------------------------------------------------------------------------- /internal/templates/error_components.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import "fmt" 4 | 5 | templ Error404() { 6 | @Layout("404 not found") { 7 |
8 |

404

9 |

Not found

10 |

The page you're trying to reach doesn't exist or it has been moved

11 | 15 | Go back to main 16 | 17 |
18 | } 19 | } 20 | 21 | templ Error500() { 22 | @Layout("Server error") { 23 |
24 |

500

25 |

Server error

26 |

27 | I'm sorry, there have been an error in the server. Please try again later. 28 |

29 | 33 | Go back to main 34 | 35 |
36 | } 37 | } 38 | 39 | templ ErrorPage(statusCode int, message string) { 40 | @Layout("Error") { 41 |
42 |

{ fmt.Sprint(statusCode) }

43 |

Error

44 |

{ message }

45 | 49 | Volver al inicio 50 | 51 |
52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/templates/layout.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import "github.com/lordaris/gotth-boilerplate/internal/auth" 4 | 5 | templ Layout(title string) { 6 | 7 | 8 | 9 | 10 | 11 | 12 | { title } 13 | 14 | 15 | 16 | 17 | 18 |
19 | @Navbar() 20 |
21 | { children... } 22 |
23 | @Footer() 24 |
25 | 26 | 27 | 28 | } 29 | 30 | templ Navbar() { 31 | 48 | } 49 | 50 | templ Footer() { 51 | 56 | } 57 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/lordaris/gotth-boilerplate/internal/app" 11 | "github.com/lordaris/gotth-boilerplate/internal/routes" 12 | 13 | "github.com/joho/godotenv" 14 | ) 15 | 16 | func main() { 17 | // Load environment variables from .env file 18 | err := godotenv.Load(".env") 19 | if err != nil { 20 | log.Println("Warning: .env file not found, using environment variables") 21 | } 22 | 23 | // Setup configuration 24 | var cfg app.Config 25 | flag.StringVar(&cfg.DB.Host, "db-host", getEnvOrDefault("DB_HOST", "localhost"), "PostgreSQL host") 26 | flag.StringVar(&cfg.DB.Port, "db-port", getEnvOrDefault("DB_PORT", "5432"), "PostgreSQL port") 27 | flag.StringVar(&cfg.DB.User, "db-user", getEnvOrDefault("DB_USER", "postgres"), "PostgreSQL user") 28 | flag.StringVar(&cfg.DB.Password, "db-password", getEnvOrDefault("DB_PASSWORD", "postgres"), "PostgreSQL password") 29 | flag.StringVar(&cfg.DB.Name, "db-name", getEnvOrDefault("DB_NAME", "gotth-boilerplate"), "PostgreSQL database name") 30 | flag.StringVar(&cfg.Server.Port, "port", getEnvOrDefault("PORT", "8080"), "Server port") 31 | flag.Parse() 32 | 33 | // Initialize application 34 | application := app.NewApplication(cfg) 35 | 36 | // Connect to PostgreSQL 37 | if err := application.ConnectToDatabase(); err != nil { 38 | log.Fatal("Could not connect to PostgreSQL: ", err) 39 | } 40 | 41 | // Setup router 42 | router := routes.SetupRouter(application) 43 | 44 | // Start server 45 | port := cfg.Server.Port 46 | fmt.Printf("Server started at http://localhost:%s\n", port) 47 | log.Fatal(http.ListenAndServe(":"+port, router)) 48 | } 49 | 50 | func getEnvOrDefault(key, defaultValue string) string { 51 | value := os.Getenv(key) 52 | if value == "" { 53 | return defaultValue 54 | } 55 | return value 56 | } 57 | -------------------------------------------------------------------------------- /internal/templates/userdetails.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lordaris/gotth-boilerplate/internal/models" 6 | ) 7 | 8 | templ UserDetails(user models.User) { 9 | @Layout("User Details") { 10 |
11 |
12 |
13 |

{ user.Username}

14 |

{ user.Username }

15 |
16 | Active 17 |
18 |
19 |

User Information

20 |
21 |
ID
22 |
{ fmt.Sprint(user.ID) }
23 |
Joined
24 |
February 20, 2025
25 |
Role
26 |
Standard User
27 |
28 |
29 |
30 | 38 | 46 | 55 |
56 |
57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/templates/home.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "github.com/lordaris/gotth-boilerplate/internal/auth" 5 | "github.com/lordaris/gotth-boilerplate/internal/models" 6 | ) 7 | 8 | templ Home(user models.User) { 9 | @Layout("Home") { 10 |
11 |
12 |

Welcome to my GotTH boilerplate

13 | by Armando Peña L. 14 |

15 | a template to quickstart your web development using Go,temple, Tailwind and HTMX 16 |

17 |
18 |

Content:

19 |
    20 |
  • User creation and authentication
  • 21 |
  • HTMX navigation without reloading
  • 22 |
  • Design using tailwindcss
  • 23 |
24 |
25 | 26 | 27 |

How to use it

28 |
29 |

This template includes some cases of use of HTMX, using a stateful authentication system and a database

30 |

31 | Just explore the code and play with this site. You can adapt the code to your needs. 32 |

33 |
34 | 35 | if auth.GetUserFromContext(ctx) != nil { 36 |
37 |

38 | You're logged in Watch your profile 39 |

40 |
41 | } else { 42 | 52 | } 53 |
54 |
55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/templates/profile.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import "github.com/lordaris/gotth-boilerplate/internal/models" 4 | import "time" 5 | import "fmt" 6 | import "github.com/lordaris/gotth-boilerplate/internal/auth" 7 | 8 | templ ProfilePage(user *models.User) { 9 | @Layout("Profile") { 10 |
11 |
12 |

My profile

13 |
14 |
15 |
16 |
17 | { string(user.Username[0]) } 18 |
19 |
20 |

{ user.Username }

21 |

Registered since { formatDate(user.CreatedAt) }

22 |
23 |
24 |
25 |

Account information

26 |
27 |
28 |
User ID
29 |
{ fmt.Sprintf("%d", user.ID) }
30 |
31 |
32 |
Username
33 |
{ user.Username }
34 |
35 |
36 |
Sign up date
37 |
{ user.CreatedAt.Format("02/01/2006") }
38 |
39 |
40 |
41 |
42 | 43 | if auth.GetUserFromContext(ctx) != nil { 44 | 45 | Logout 46 | 47 | } 48 |
49 |
50 |
51 | } 52 | } 53 | 54 | func formatDate(date time.Time) string { 55 | return date.Format("02 de January de 2006") 56 | } 57 | -------------------------------------------------------------------------------- /internal/templates/register.templ: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | templ RegisterPage() { 4 | @Layout("Sign up") { 5 |
6 |

Sign up

7 |
8 |
9 | 10 | 17 |
18 |
19 | 20 | 28 |

Password should be at least 8 characters long

29 |
30 |
31 | 32 | 40 |
41 |
42 | 48 |
49 |
50 |
51 |

52 | Already registered? Login 53 |

54 |
55 |
56 | } 57 | } 58 | 59 | templ RegisterError(message string) { 60 |
61 |

{ message }

62 |
63 | } 64 | -------------------------------------------------------------------------------- /internal/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/lordaris/gotth-boilerplate/internal/app" 7 | "github.com/lordaris/gotth-boilerplate/internal/auth" 8 | "github.com/lordaris/gotth-boilerplate/internal/handlers" 9 | 10 | "github.com/go-chi/chi/v5" 11 | "github.com/go-chi/chi/v5/middleware" 12 | ) 13 | 14 | // SetupRouter configures and returns the application router 15 | func SetupRouter(app *app.Application) *chi.Mux { 16 | r := chi.NewRouter() 17 | 18 | // Middleware 19 | r.Use(middleware.Logger) 20 | r.Use(middleware.Recoverer) 21 | r.Use(middleware.RealIP) 22 | 23 | authMiddleware := auth.NewMiddleware(app) 24 | 25 | // Use cookie-based auth by checking for auth_token cookie 26 | r.Use(func(next http.Handler) http.Handler { 27 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 28 | cookie, err := r.Cookie("auth_token") 29 | if err == nil { 30 | // Add Authorization header if cookie exists 31 | r.Header.Set("Authorization", "Bearer "+cookie.Value) 32 | } 33 | next.ServeHTTP(w, r) 34 | }) 35 | }) 36 | 37 | // Apply authentication middleware to parse the auth token 38 | r.Use(authMiddleware.Authenticate) 39 | 40 | // Create handlers 41 | h := handlers.NewHandlers(app) 42 | 43 | // Static files 44 | fileServer := http.FileServer(http.Dir("./static")) 45 | r.Handle("/static/*", http.StripPrefix("/static/", fileServer)) 46 | 47 | // Public routes 48 | r.Group(func(r chi.Router) { 49 | r.Get("/", h.Home()) 50 | r.Get("/login", h.LoginForm()) 51 | r.Post("/login", h.Login()) 52 | r.Get("/register", h.RegisterForm()) 53 | r.Post("/register", h.Register()) 54 | r.Get("/logout", h.Logout()) 55 | }) 56 | 57 | // User management routes 58 | r.Group(func(r chi.Router) { 59 | // These routes require authentication 60 | r.Use(authMiddleware.RequireAuth) 61 | r.Get("/profile", h.ProfileHandler()) 62 | 63 | // User management 64 | // r.Get("/users", h.GetUsersHandler()) 65 | // r.Post("/users", h.CreateUserHandler()) 66 | // r.Get("/users/{id}", h.GetUserDetailHandler()) 67 | // r.Get("/users/{id}/edit", h.EditUserHandler()) 68 | // r.Put("/users/{id}", h.UpdateUserHandler()) 69 | // r.Delete("/users/{id}", h.DeleteUserHandler()) 70 | }) 71 | 72 | return r 73 | } 74 | -------------------------------------------------------------------------------- /internal/auth/middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/lordaris/gotth-boilerplate/internal/app" 10 | "github.com/lordaris/gotth-boilerplate/internal/models" 11 | ) 12 | 13 | type contextKey string 14 | 15 | const UserContextKey = contextKey("user") 16 | 17 | type Middleware struct { 18 | App *app.Application 19 | } 20 | 21 | func NewMiddleware(app *app.Application) *Middleware { 22 | return &Middleware{App: app} 23 | } 24 | 25 | func (m *Middleware) Authenticate(next http.Handler) http.Handler { 26 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 | db := m.App.DB 28 | // Add Vary header to ensure cached responses are based on the Authorization header 29 | w.Header().Add("Vary", "Authorization") 30 | 31 | authHeader := r.Header.Get("Authorization") 32 | 33 | if authHeader == "" { 34 | next.ServeHTTP(w, r) 35 | return 36 | } 37 | 38 | headerParts := strings.Split(authHeader, " ") 39 | if len(headerParts) != 2 || headerParts[0] != "Bearer" { 40 | http.Error(w, "Invalid authentication token", http.StatusUnauthorized) 41 | return 42 | } 43 | 44 | token := headerParts[1] 45 | 46 | if err := ValidateTokenPlaintext(token); err != nil { 47 | http.Error(w, err.Error(), http.StatusUnauthorized) 48 | return 49 | } 50 | 51 | tokenHash := HashToken(token) 52 | 53 | var userToken models.Token 54 | var user models.User 55 | 56 | query := ` 57 | SELECT id, user_id, expiry 58 | FROM tokens 59 | WHERE token_hash = $1 60 | AND expiry > $2 61 | ` 62 | err := db.Get(&userToken, query, tokenHash, time.Now()) 63 | if err != nil { 64 | http.Error(w, "Invalid authentication token", http.StatusUnauthorized) 65 | return 66 | } 67 | 68 | query = ` 69 | SELECT id, username, password_hash, created_at 70 | FROM users 71 | WHERE id = $1 72 | ` 73 | err = db.Get(&user, query, userToken.UserID) 74 | if err != nil { 75 | http.Error(w, "Invalid user", http.StatusUnauthorized) 76 | return 77 | } 78 | 79 | ctx := context.WithValue(r.Context(), UserContextKey, &user) 80 | r = r.WithContext(ctx) 81 | 82 | next.ServeHTTP(w, r) 83 | }) 84 | } 85 | 86 | func (m *Middleware) RequireAuth(next http.Handler) http.Handler { 87 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 88 | user := GetUserFromContext(r.Context()) 89 | if user == nil { 90 | http.Error(w, "Authentication required", http.StatusUnauthorized) 91 | return 92 | } 93 | 94 | next.ServeHTTP(w, r) 95 | }) 96 | } 97 | 98 | func GetUserFromContext(ctx context.Context) *models.User { 99 | user, ok := ctx.Value(UserContextKey).(*models.User) 100 | if !ok { 101 | return nil 102 | } 103 | return user 104 | } 105 | 106 | func (m *Middleware) CleanupExpiredTokens() error { 107 | db := m.App.DB 108 | query := `DELETE FROM tokens WHERE expiry < $1` 109 | _, err := db.Exec(query, time.Now()) 110 | return err 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOTTH Stack stateful authentication boilerplate 2 | 3 | A web application boilerplate with stateful authentication using the GOTTH (golang, templ, tailwind, HTMX) stack: 4 | 5 | - **Go** 6 | - **Chi** Lightweight and idiomatic router for Go 7 | - **Templ** - Type-safe templates for Go 8 | - **Tailwind CSS** - CSS framework 9 | - **HTMX** - Modern frontend interactivity without JavaScript frameworks 10 | - **PostgreSQL + SQLx** - Database and SQL handling 11 | - **Air** - Live reload for development 12 | 13 | ## Requirements 14 | 15 | Ensure you have the following installed before starting: 16 | 17 | - **Go 1.20+** 18 | - **Node.js and npm** 19 | - **PostgreSQL** 20 | 21 | ## Installation 22 | 23 | 1. **Clone the repository** 24 | 25 | ```bash 26 | git clone https://github.com/lordaris/gotth-boilerplate 27 | cd gotth-boilerplate 28 | ``` 29 | 30 | 2. **Install Go dependencies** 31 | 32 | ```bash 33 | go mod tidy 34 | ``` 35 | 36 | 3. **Install Node dependencies** 37 | 38 | ```bash 39 | npm install 40 | ``` 41 | 42 | 4. **Configure the PostgreSQL database** 43 | - Set up your database and update the connection details in the environment configuration. 44 | - Create a .env file in the main folder and add the following content: 45 | 46 | ```bash 47 | # Database Configuration 48 | DB_HOST=localhost 49 | DB_PORT=5432 50 | DB_USER=postgres 51 | DB_PASSWORD=postgres 52 | DB_NAME=gotth_boilerplate 53 | 54 | # Server Configuration 55 | PORT=8080 56 | ``` 57 | 58 | This .env file is essential for storing sensitive configuration values and should not be committed to version control. 59 | 60 | 5. **Run database migrations** 61 | 62 | ```bash 63 | make db-migrate 64 | ``` 65 | 66 | 6. **Start the development server** 67 | 68 | ```bash 69 | make dev 70 | ``` 71 | 72 | 7. Visit `http://localhost:8080` in your browser. 73 | 74 | ## Makefile Commands 75 | 76 | This project includes a `Makefile` to simplify common operations, just remove the "copy" text from the file name and modify the project and database variables to your own: 77 | 78 | ```bash 79 | # Install required tools 80 | make install-tools 81 | 82 | # Start development environment (Air + Tailwind) 83 | make dev 84 | 85 | # Database operations 86 | make db-create # Create the database 87 | make db-migrate # Run migrations 88 | make db-reset # Reset the database 89 | 90 | # Generate Templ files 91 | make generate-templ 92 | 93 | # See all available commands 94 | make help 95 | ``` 96 | 97 | Stateful Login System 98 | 99 | In this boilerplate, the login system has been upgraded to use a stateful approach, meaning sessions are maintained with cookies on the client-side. Upon logging in, a session token is stored in an HTTP-only cookie, which is used for subsequent requests to authenticate the user. This is a more secure approach compared to stateless systems like JWTs because the session token remains on the client and is tied to a session on the server-side. 100 | Improvements with State Management: 101 | 102 | Session management: Instead of relying on tokens passed with each request, the server maintains sessions tied to cookies. 103 | Logout functionality: Logs out the user by clearing the session cookie, ensuring no leftover sessions are active. 104 | Redirect after logout: After logging out, users are automatically redirected to the home page or the login page as configured. 105 | 106 | Session Flow: 107 | Login: When a user logs in, the server creates a session and stores the session ID in an HTTP-only cookie. 108 | Authentication: For every request, the server retrieves the session cookie to authenticate the user. 109 | Logout: The session cookie is cleared, and the user is logged out. 110 | 111 | 112 | ## Improvement Areas & Future Plans 113 | 114 | - **Better HTMX usage**: As I'm still learning HTMX, the implementation isn't perfect. Expect improvements in the future. 115 | - **General refinements**: UI, error handling, and overall experience will be enhanced over time. 116 | - Add CRUD elements 117 | 118 | For now, if you modify a `templ` file, run: 119 | 120 | ```bash 121 | make generate-templ 122 | ``` 123 | 124 | and restart the server using: 125 | 126 | ```bash 127 | make run # or make dev 128 | ``` 129 | 130 | --- 131 | 132 | Feel free to improve on this template and make it your own. Happy coding! 133 | -------------------------------------------------------------------------------- /Makefile copy: -------------------------------------------------------------------------------- 1 | # Project variables 2 | APP_NAME = gotth-boilerplate 3 | MAIN_PATH = ./cmd/server 4 | BUILD_DIR = ./tmp 5 | BINARY_NAME = $(BUILD_DIR)/$(APP_NAME) 6 | 7 | # Database variables 8 | DB_USER ?= postgres 9 | DB_PASSWORD ?= postgres 10 | DB_NAME ?= gotth_boilerplate 11 | DB_HOST ?= localhost 12 | DB_PORT ?= 5432 13 | DB_URL ?= postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable 14 | 15 | # Templ variables 16 | TEMPL_SRC_DIR = ./internal/templates 17 | TEMPL_FILES = $(shell find $(TEMPL_SRC_DIR) -name "*.templ") 18 | 19 | # Tailwind variables 20 | TAILWIND_INPUT = ./static/css/input.css 21 | TAILWIND_OUTPUT = ./static/css/output.css 22 | 23 | .PHONY: all build run clean dev dev-server dev-tailwind dev-all install-tools db-create db-migrate db-reset generate-templ test help 24 | 25 | # Default target 26 | all: build 27 | 28 | # Build the application 29 | build: generate-templ 30 | go build -o $(BINARY_NAME) $(MAIN_PATH) 31 | 32 | # Run the compiled application 33 | run: build 34 | $(BINARY_NAME) 35 | 36 | # Clean generated files 37 | clean: 38 | rm -rf $(BUILD_DIR) 39 | rm -f $(TAILWIND_OUTPUT) 40 | find . -name '*_templ.go' -delete 41 | 42 | # Start server with Air (hot reload) 43 | dev-server: 44 | air 45 | 46 | # Start Tailwind in development mode (watch) 47 | dev-tailwind: 48 | npx @tailwindcss/cli -i $(TAILWIND_INPUT) -o $(TAILWIND_OUTPUT) --watch 49 | 50 | # Build Tailwind for production 51 | build-tailwindpostgres: 52 | npx tailwindcss -i $(TAILWIND_INPUT) -o $(TAILWIND_OUTPUT) --minify 53 | 54 | # Start both services in parallel (using foreman if available, or an alternative solution) 55 | dev-all: 56 | @if command -v foreman > /dev/null; then \ 57 | echo "web: air" > Procfile && \ 58 | echo "css: npx tailwindcss -i $(TAILWIND_INPUT) -o $(TAILWIND_OUTPUT) --watch" >> Procfile && \ 59 | foreman start; \ 60 | else \ 61 | echo "Starting services in separate terminals..."; \ 62 | $(MAKE) dev-server & $(MAKE) dev-tailwind; \ 63 | fi 64 | 65 | # Convenient alias for dev-all 66 | dev: dev-all 67 | 68 | # Install necessary tools 69 | install-tools: 70 | go install github.com/air-verse/air@latest 71 | go install github.com/a-h/templ/cmd/templ@latest 72 | go get -u github.com/go-chi/chi/v5 73 | npm install 74 | 75 | # Generate Go files from Templ templates 76 | generate-templ: $(TEMPL_FILES) 77 | templ generate 78 | 79 | # Create database 80 | db-create: 81 | @echo "Creating database $(DB_NAME)..." 82 | @PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d postgres -c "CREATE DATABASE $(DB_NAME);" || echo "Database already exists" 83 | 84 | # Run migrations 85 | db-migrate: 86 | @echo "Running migrations..." 87 | @for file in ./migrations/*_*.sql; do \ 88 | echo "Applying $$file..."; \ 89 | PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -d $(DB_NAME) -f $$file; \ 90 | done 91 | 92 | # Reset database (drop + create + migrate) 93 | db-reset: 94 | @echo "Resetting database $(DB_NAME)..." 95 | @PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -p $(DB_PORT) -U $(DB_USER) -c "DROP DATABASE IF EXISTS $(DB_NAME);" || true 96 | @$(MAKE) db-create 97 | @$(MAKE) db-migrate 98 | 99 | # Run tests 100 | test: 101 | go test -v ./... 102 | 103 | # Help 104 | help: 105 | @echo "Makefile Help for $(APP_NAME)" 106 | @echo "" 107 | @echo "Available commands:" 108 | @echo " make build - Build the application" 109 | @echo " make run - Run the compiled application" 110 | @echo " make clean - Clean generated files" 111 | @echo " make dev - Start the complete development environment" 112 | @echo " make dev-server - Start only the server with Air (hot reload)" 113 | @echo " make dev-tailwind - Start only the Tailwind compiler (watch mode)" 114 | @echo " make build-tailwind - Build Tailwind for production (minified)" 115 | @echo " make install-tools - Install necessary tools" 116 | @echo " make generate-templ - Generate Go files from Templ templates" 117 | @echo " make db-create - Create the database" 118 | @echo " make db-migrate - Run migrations" 119 | @echo " make db-reset - Reset the database (drop+create+migrate)" 120 | @echo " make test - Run tests" 121 | @echo "" 122 | @echo "Environment variables you can configure:" 123 | @echo " DB_USER=user - PostgreSQL user (default: postgres)" 124 | @echo " DB_PASSWORD=password - PostgreSQL password (default: postgres)" 125 | @echo " DB_NAME=name - Database name (default: github.com/lordaris/gotth-boilerplate)" 126 | @echo " DB_HOST=host - PostgreSQL host (default: localhost)" 127 | @echo " DB_PORT=port - PostgreSQL port (default: 5432)" 128 | @echo "" 129 | @echo "Example: make db-reset DB_USER=myuser DB_PASSWORD=mypassword" 130 | -------------------------------------------------------------------------------- /internal/handlers/authentication.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/lordaris/gotth-boilerplate/internal/auth" 9 | "github.com/lordaris/gotth-boilerplate/internal/models" 10 | 11 | "github.com/lordaris/gotth-boilerplate/internal/templates" 12 | ) 13 | 14 | func (h *Handlers) LoginForm() http.HandlerFunc { 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | user := auth.GetUserFromContext(r.Context()) 17 | if user != nil { 18 | http.Redirect(w, r, "/", http.StatusSeeOther) 19 | return 20 | } 21 | 22 | templates.LoginPage().Render(r.Context(), w) 23 | } 24 | } 25 | 26 | func (h *Handlers) RegisterForm() http.HandlerFunc { 27 | return func(w http.ResponseWriter, r *http.Request) { 28 | user := auth.GetUserFromContext(r.Context()) 29 | if user != nil { 30 | http.Redirect(w, r, "/", http.StatusSeeOther) 31 | return 32 | } 33 | 34 | templates.RegisterPage().Render(r.Context(), w) 35 | } 36 | } 37 | 38 | func (h *Handlers) Login() http.HandlerFunc { 39 | return func(w http.ResponseWriter, r *http.Request) { 40 | db := h.App.DB 41 | 42 | if err := r.ParseForm(); err != nil { 43 | http.Error(w, "Invalid form data", http.StatusBadRequest) 44 | return 45 | } 46 | 47 | username := r.FormValue("username") 48 | password := r.FormValue("password") 49 | 50 | if username == "" || password == "" { 51 | templates.LoginPage().Render(r.Context(), w) 52 | return 53 | } 54 | 55 | var user models.User 56 | query := `SELECT id, username, password_hash, created_at FROM users WHERE username = $1` 57 | err := db.Get(&user, query, username) 58 | if err != nil { 59 | templates.LoginPage().Render(r.Context(), w) 60 | return 61 | } 62 | 63 | match, err := user.CheckPassword(password) 64 | if err != nil || !match { 65 | templates.LoginPage().Render(r.Context(), w) 66 | return 67 | } 68 | 69 | tokenString, tokenHash, expiryTime, err := auth.GenerateToken(24 * time.Hour) 70 | if err != nil { 71 | http.Error(w, "Server error", http.StatusInternalServerError) 72 | return 73 | } 74 | 75 | query = ` 76 | INSERT INTO tokens (user_id, token_hash, plaintext_token, expiry) 77 | VALUES ($1, $2, $3, $4) 78 | ` 79 | _, err = db.Exec(query, user.ID, tokenHash, tokenString, expiryTime) 80 | if err != nil { 81 | http.Error(w, "Server error", http.StatusInternalServerError) 82 | return 83 | } 84 | 85 | http.SetCookie(w, &http.Cookie{ 86 | Name: "auth_token", 87 | Value: tokenString, 88 | Path: "/", 89 | Expires: expiryTime, 90 | HttpOnly: true, 91 | Secure: r.TLS != nil, 92 | SameSite: http.SameSiteStrictMode, 93 | }) 94 | 95 | 96 | if r.Header.Get("HX-Request") == "true" { 97 | w.Header().Set("HX-Redirect", "/") 98 | return 99 | } 100 | 101 | http.Redirect(w, r, "/", http.StatusSeeOther) 102 | } 103 | } 104 | 105 | func (h *Handlers) Register() http.HandlerFunc { 106 | return func(w http.ResponseWriter, r *http.Request) { 107 | db := h.App.DB 108 | 109 | if err := r.ParseForm(); err != nil { 110 | log.Println("Error while parsing the form:", err) 111 | http.Error(w, "Invalid form data", http.StatusBadRequest) 112 | return 113 | } 114 | 115 | username := r.FormValue("username") 116 | password := r.FormValue("password") 117 | confirmPassword := r.FormValue("confirm_password") 118 | 119 | if username == "" || password == "" || password != confirmPassword { 120 | log.Println("Failed validation") 121 | templates.RegisterPage().Render(r.Context(), w) 122 | return 123 | } 124 | 125 | user := models.User{ 126 | Username: username, 127 | } 128 | 129 | if err := user.SetPassword(password); err != nil { 130 | log.Println("Error while setting the password:", err) 131 | http.Error(w, err.Error(), http.StatusBadRequest) 132 | return 133 | } 134 | 135 | if db == nil { 136 | log.Println("Error: Database not initialized") 137 | http.Error(w, "Internal server error", http.StatusInternalServerError) 138 | return 139 | } 140 | 141 | query := ` 142 | INSERT INTO users (username, password_hash) 143 | VALUES ($1, $2) 144 | RETURNING id, created_at 145 | ` 146 | 147 | row := db.QueryRow(query, user.Username, user.PasswordHash) 148 | if err := row.Scan(&user.ID, &user.CreatedAt); err != nil { 149 | log.Println("Error while executing the query:", err) 150 | http.Error(w, "Failed to create the user", http.StatusInternalServerError) 151 | return 152 | } 153 | 154 | log.Println("User successfully registered", user.Username) 155 | 156 | // If HTMX request 157 | if r.Header.Get("HX-Request") == "true" { 158 | w.Header().Set("HX-Redirect", "/login") 159 | return 160 | } 161 | 162 | // Regular form submission 163 | http.Redirect(w, r, "/login", http.StatusSeeOther) 164 | } 165 | } 166 | 167 | 168 | func (h *Handlers) Logout() http.HandlerFunc { 169 | return func(w http.ResponseWriter, r *http.Request) { 170 | db := h.App.DB 171 | 172 | cookie, err := r.Cookie("auth_token") 173 | if err == nil { 174 | tokenHash := auth.HashToken(cookie.Value) 175 | query := `DELETE FROM tokens WHERE token_hash = $1` 176 | _, err := db.Exec(query, tokenHash) 177 | if err != nil { 178 | log.Printf("Error deleting token from DB: %v", err) 179 | } 180 | 181 | http.SetCookie(w, &http.Cookie{ 182 | Name: "auth_token", 183 | Value: "", 184 | Path: "/", 185 | MaxAge: -1, // Expire immediately 186 | HttpOnly: true, 187 | Secure: r.TLS != nil, 188 | SameSite: http.SameSiteStrictMode, 189 | }) 190 | } 191 | 192 | // If HTMX request, set redirect header 193 | if r.Header.Get("HX-Request") == "true" { 194 | w.Header().Set("HX-Redirect", "/") // Redirect to main 195 | return 196 | } 197 | 198 | http.Redirect(w, r, "/", http.StatusSeeOther) 199 | } 200 | } 201 | 202 | -------------------------------------------------------------------------------- /internal/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | // "github.com/go-chi/chi/v5" 7 | "github.com/lordaris/gotth-boilerplate/internal/app" 8 | "github.com/lordaris/gotth-boilerplate/internal/auth" 9 | "github.com/lordaris/gotth-boilerplate/internal/models" 10 | "github.com/lordaris/gotth-boilerplate/internal/templates" 11 | ) 12 | 13 | type Handlers struct { 14 | App *app.Application 15 | } 16 | 17 | func NewHandlers(app *app.Application) *Handlers { 18 | return &Handlers{ 19 | App: app, 20 | } 21 | } 22 | 23 | func (h *Handlers) Home() http.HandlerFunc { 24 | return func(w http.ResponseWriter, r *http.Request) { 25 | user := auth.GetUserFromContext(r.Context()) 26 | if user == nil { 27 | user = &models.User{} 28 | } 29 | templates.Home(*user).Render(r.Context(), w) 30 | } 31 | } 32 | // 33 | func (h *Handlers) ProfileHandler() http.HandlerFunc { 34 | return func(w http.ResponseWriter, r *http.Request) { 35 | user := auth.GetUserFromContext(r.Context()) 36 | if user == nil { 37 | user = &models.User{} 38 | } 39 | templates.ProfilePage(user).Render(r.Context(), w) 40 | } 41 | } 42 | 43 | // func (h *Handlers) CreateUserHandler() http.HandlerFunc { 44 | // return func(w http.ResponseWriter, r *http.Request) { 45 | // users := []models.User{} 46 | // err := r.ParseForm() 47 | // if err != nil { 48 | // http.Error(w, "Invalid input", http.StatusBadRequest) 49 | // return 50 | // } 51 | // 52 | // name := r.FormValue("name") 53 | // email := r.FormValue("email") 54 | // 55 | // if name == "" || email == "" { 56 | // http.Error(w, "Missing fields", http.StatusBadRequest) 57 | // return 58 | // } 59 | // 60 | // _, err = h.App.DB.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email) 61 | // if err != nil { 62 | // http.Error(w, "Error creating user", http.StatusInternalServerError) 63 | // return 64 | // } 65 | // 66 | // err = h.App.DB.Select(&users, "SELECT id, name, email FROM users") 67 | // if err != nil { 68 | // http.Error(w, err.Error(), http.StatusInternalServerError) 69 | // return 70 | // } 71 | // 72 | // w.WriteHeader(http.StatusCreated) 73 | // templates.UsersList(users).Render(r.Context(), w) 74 | // } 75 | // } 76 | // 77 | // // GetUsersHandler retrieves users from the database 78 | // func (h *Handlers) GetUsersHandler() http.HandlerFunc { 79 | // return func(w http.ResponseWriter, r *http.Request) { 80 | // users := []models.User{} 81 | // err := h.App.DB.Select(&users, "SELECT id, name, email FROM users") 82 | // if err != nil { 83 | // http.Error(w, err.Error(), http.StatusInternalServerError) 84 | // return 85 | // } 86 | // 87 | // templates.UsersList(users).Render(r.Context(), w) 88 | // } 89 | // } 90 | // 91 | // func (h *Handlers) GetUserDetailHandler() http.HandlerFunc { 92 | // return func(w http.ResponseWriter, r *http.Request) { 93 | // idStr := chi.URLParam(r, "id") 94 | // id, err := strconv.Atoi(idStr) 95 | // if err != nil { 96 | // log.Println("Error converting ID:", err) 97 | // http.Error(w, "Invalid user ID", http.StatusBadRequest) 98 | // return 99 | // } 100 | // 101 | // user := models.User{} 102 | // err = h.App.DB.Get(&user, "SELECT id, name, email FROM users WHERE id = $1", id) 103 | // if err != nil { 104 | // log.Println("Error fetching user from DB:", err) 105 | // http.Error(w, "User not found", http.StatusNotFound) 106 | // return 107 | // } 108 | // 109 | // templates.UserDetails(user).Render(r.Context(), w) 110 | // } 111 | // } 112 | // 113 | // func (h *Handlers) EditUserHandler() http.HandlerFunc { 114 | // return func(w http.ResponseWriter, r *http.Request) { 115 | // idStr := chi.URLParam(r, "id") 116 | // id, err := strconv.Atoi(idStr) 117 | // if err != nil { 118 | // http.Error(w, "Invalid user ID", http.StatusBadRequest) 119 | // return 120 | // } 121 | // 122 | // user := models.User{} 123 | // err = h.App.DB.Get(&user, "SELECT id, name, email FROM users WHERE id = $1", id) 124 | // if err != nil { 125 | // http.Error(w, "User not found", http.StatusNotFound) 126 | // return 127 | // } 128 | // 129 | // templates.EditUserForm(user).Render(r.Context(), w) 130 | // } 131 | // } 132 | // 133 | // func (h *Handlers) UpdateUserHandler() http.HandlerFunc { 134 | // return func(w http.ResponseWriter, r *http.Request) { 135 | // id := chi.URLParam(r, "id") 136 | // err := r.ParseForm() 137 | // if err != nil { 138 | // http.Error(w, "Invalid input", http.StatusBadRequest) 139 | // return 140 | // } 141 | // 142 | // name := r.FormValue("name") 143 | // email := r.FormValue("email") 144 | // 145 | // _, err = h.App.DB.Exec("UPDATE users SET name = $1, email = $2 WHERE id = $3", name, email, id) 146 | // if err != nil { 147 | // http.Error(w, "Error updating user", http.StatusInternalServerError) 148 | // return 149 | // } 150 | // users := []models.User{} 151 | // err = h.App.DB.Select(&users, "SELECT id, name, email FROM users") 152 | // if err != nil { 153 | // http.Error(w, err.Error(), http.StatusInternalServerError) 154 | // return 155 | // } 156 | // 157 | // templates.UsersList(users).Render(r.Context(), w) 158 | // } 159 | // } 160 | // 161 | // func (h *Handlers) DeleteUserHandler() http.HandlerFunc { 162 | // return func(w http.ResponseWriter, r *http.Request) { 163 | // id := chi.URLParam(r, "id") 164 | // 165 | // _, err := h.App.DB.Exec("DELETE FROM users WHERE id = $1", id) 166 | // if err != nil { 167 | // http.Error(w, "Error deleting user", http.StatusInternalServerError) 168 | // return 169 | // } 170 | // 171 | // w.WriteHeader(http.StatusOK) 172 | // 173 | // users := []models.User{} 174 | // err = h.App.DB.Select(&users, "SELECT id, name, email FROM users") 175 | // if err != nil { 176 | // http.Error(w, err.Error(), http.StatusInternalServerError) 177 | // return 178 | // } 179 | // 180 | // templates.UsersList(users).Render(r.Context(), w) 181 | // } 182 | // } 183 | --------------------------------------------------------------------------------