Compare commits
	
		
			15 Commits
		
	
	
		
			toml_confi
			...
			6d6aff50b3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6d6aff50b3 | ||
| 
						 | 
					a6be73765a | ||
| 
						 | 
					ddc9e51831 | ||
| 
						 | 
					dc450e26dd | ||
| 
						 | 
					de4a217c5f | ||
| 
						 | 
					c4e83d06b9 | ||
| 
						 | 
					51da24be9b | ||
| 
						 | 
					e497f4d2f0 | ||
| 
						 | 
					b30af86e58 | ||
| 
						 | 
					3ffd548623 | ||
| 
						 | 
					cb4f10e0b4 | ||
| 
						 | 
					878ce01b29 | ||
| 
						 | 
					c82cdb4f13 | ||
| 
						 | 
					ce81c36e9f | ||
| 
						 | 
					ab1b82c680 | 
@@ -18,7 +18,7 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
 | 
			
		||||
- Minimal user login/registration + sessions
 | 
			
		||||
- Config file handling
 | 
			
		||||
- Scheduled tasks
 | 
			
		||||
- Entire website compiles into a single binary (~10mb) (excluding env.toml)
 | 
			
		||||
- Entire website compiles into a single binary (~10mb) (excluding env.json)
 | 
			
		||||
- Minimal dependencies (just standard library, postgres driver, and experimental package for bcrypt)
 | 
			
		||||
 | 
			
		||||
<hr>
 | 
			
		||||
@@ -41,7 +41,7 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
 | 
			
		||||
1. Clone
 | 
			
		||||
2. Delete the git folder, so you can start tracking in your own repo
 | 
			
		||||
3. Run `go get` to install dependencies
 | 
			
		||||
4. Copy env_example.toml to env.toml and fill in the values
 | 
			
		||||
4. Copy env_example.json to env.json and fill in the values
 | 
			
		||||
5. Run `go run main.go` to start the server
 | 
			
		||||
6. Rename the occurences of "GoWeb" to your app name
 | 
			
		||||
7. Start building your app!
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,8 @@ type Scheduled struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Task struct {
 | 
			
		||||
	Interval time.Duration
 | 
			
		||||
	Funcs    []func(app *App)
 | 
			
		||||
	Interval time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RunScheduledTasks(app *App, poolSize int, stop <-chan struct{}) {
 | 
			
		||||
@@ -27,13 +27,13 @@ func RunScheduledTasks(app *App, poolSize int, stop <-chan struct{}) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tasks := []Task{
 | 
			
		||||
		{Interval: time.Second, Funcs: app.ScheduledTasks.EverySecond},
 | 
			
		||||
		{Interval: time.Minute, Funcs: app.ScheduledTasks.EveryMinute},
 | 
			
		||||
		{Interval: time.Hour, Funcs: app.ScheduledTasks.EveryHour},
 | 
			
		||||
		{Interval: 24 * time.Hour, Funcs: app.ScheduledTasks.EveryDay},
 | 
			
		||||
		{Interval: 7 * 24 * time.Hour, Funcs: app.ScheduledTasks.EveryWeek},
 | 
			
		||||
		{Interval: 30 * 24 * time.Hour, Funcs: app.ScheduledTasks.EveryMonth},
 | 
			
		||||
		{Interval: 365 * 24 * time.Hour, Funcs: app.ScheduledTasks.EveryYear},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EverySecond, Interval: time.Second},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EveryMinute, Interval: time.Minute},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EveryHour, Interval: time.Hour},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EveryDay, Interval: 24 * time.Hour},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EveryWeek, Interval: 7 * 24 * time.Hour},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EveryMonth, Interval: 30 * 24 * time.Hour},
 | 
			
		||||
		{Funcs: app.ScheduledTasks.EveryYear, Interval: 365 * 24 * time.Hour},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var wg sync.WaitGroup
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +1,54 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"github.com/BurntSushi/toml"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Configuration struct {
 | 
			
		||||
	Db struct {
 | 
			
		||||
		Ip          string `toml:"DbIp"`
 | 
			
		||||
		Port        string `toml:"DbPort"`
 | 
			
		||||
		Name        string `toml:"DbName"`
 | 
			
		||||
		User        string `toml:"DbUser"`
 | 
			
		||||
		Password    string `toml:"DbPassword"`
 | 
			
		||||
		AutoMigrate bool   `toml:"DbAutoMigrate"`
 | 
			
		||||
		Ip          string `json:"DbIp"`
 | 
			
		||||
		Port        string `json:"DbPort"`
 | 
			
		||||
		Name        string `json:"DbName"`
 | 
			
		||||
		User        string `json:"DbUser"`
 | 
			
		||||
		Password    string `json:"DbPassword"`
 | 
			
		||||
		AutoMigrate bool   `json:"DbAutoMigrate"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Listen struct {
 | 
			
		||||
		Ip   string `toml:"HttpIp"`
 | 
			
		||||
		Port string `toml:"HttpPort"`
 | 
			
		||||
		Ip   string `json:"HttpIp"`
 | 
			
		||||
		Port string `json:"HttpPort"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	Template struct {
 | 
			
		||||
		BaseName string `toml:"BaseTemplateName"`
 | 
			
		||||
		BaseName    string `json:"BaseTemplateName"`
 | 
			
		||||
		ContentPath string `json:"ContentPath"`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadConfig loads and returns a configuration struct
 | 
			
		||||
func LoadConfig() Configuration {
 | 
			
		||||
	c := flag.String("c", "env.toml", "Path to the toml configuration file")
 | 
			
		||||
	c := flag.String("c", "env.json", "Path to the json configuration file")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	file, err := os.ReadFile(*c)
 | 
			
		||||
	file, err := os.Open(*c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic("Unable to read TOML config file: " + err.Error())
 | 
			
		||||
		panic("unable to open JSON config file: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var Config Configuration
 | 
			
		||||
	_, err = toml.Decode(string(file), &Config)
 | 
			
		||||
	defer func(file *os.File) {
 | 
			
		||||
		err := file.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		panic("Unable to decode TOML config file: " + err.Error())
 | 
			
		||||
			slog.Error("unable to close JSON config file: ", err)
 | 
			
		||||
		}
 | 
			
		||||
	}(file)
 | 
			
		||||
 | 
			
		||||
	decoder := json.NewDecoder(file)
 | 
			
		||||
	Config := Configuration{}
 | 
			
		||||
	err = decoder.Decode(&Config)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic("unable to decode JSON config file: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return Config
 | 
			
		||||
 
 | 
			
		||||
@@ -13,21 +13,37 @@ type Get struct {
 | 
			
		||||
	App *app.App
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *Get) ShowHome(w http.ResponseWriter, _ *http.Request) {
 | 
			
		||||
func (g *Get) ShowHome(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	type dataStruct struct {
 | 
			
		||||
		CsrfToken       string
 | 
			
		||||
		IsAuthenticated bool
 | 
			
		||||
		Test            string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := dataStruct{
 | 
			
		||||
		Test: "Hello World!",
 | 
			
		||||
	CsrfToken, err := security.GenerateCsrfToken(w, r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templating.RenderTemplate(g.App, w, "templates/pages/home.html", data)
 | 
			
		||||
	isAuthenticated := true
 | 
			
		||||
	user, err := models.CurrentUser(g.App, r)
 | 
			
		||||
	if err != nil || user.Id == 0 {
 | 
			
		||||
		isAuthenticated = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := dataStruct{
 | 
			
		||||
		CsrfToken:       CsrfToken,
 | 
			
		||||
		Test:            "Hello World!",
 | 
			
		||||
		IsAuthenticated: isAuthenticated,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templating.RenderTemplate(w, "templates/pages/home.html", data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *Get) ShowRegister(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	type dataStruct struct {
 | 
			
		||||
		CsrfToken       string
 | 
			
		||||
		IsAuthenticated bool
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	CsrfToken, err := security.GenerateCsrfToken(w, r)
 | 
			
		||||
@@ -35,16 +51,24 @@ func (g *Get) ShowRegister(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := dataStruct{
 | 
			
		||||
		CsrfToken: CsrfToken,
 | 
			
		||||
	isAuthenticated := true
 | 
			
		||||
	user, err := models.CurrentUser(g.App, r)
 | 
			
		||||
	if err != nil || user.Id == 0 {
 | 
			
		||||
		isAuthenticated = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templating.RenderTemplate(g.App, w, "templates/pages/register.html", data)
 | 
			
		||||
	data := dataStruct{
 | 
			
		||||
		CsrfToken:       CsrfToken,
 | 
			
		||||
		IsAuthenticated: isAuthenticated,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templating.RenderTemplate(w, "templates/pages/register.html", data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *Get) ShowLogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	type dataStruct struct {
 | 
			
		||||
		CsrfToken       string
 | 
			
		||||
		IsAuthenticated bool
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	CsrfToken, err := security.GenerateCsrfToken(w, r)
 | 
			
		||||
@@ -56,10 +80,5 @@ func (g *Get) ShowLogin(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		CsrfToken: CsrfToken,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templating.RenderTemplate(g.App, w, "templates/pages/login.html", data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *Get) Logout(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	models.LogoutUser(g.App, w, r)
 | 
			
		||||
	http.Redirect(w, r, "/", http.StatusFound)
 | 
			
		||||
	templating.RenderTemplate(w, "templates/pages/login.html", data)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,3 +50,8 @@ func (p *Post) Register(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	http.Redirect(w, r, "/login", http.StatusFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Post) Logout(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	models.LogoutUser(p.App, w, r)
 | 
			
		||||
	http.Redirect(w, r, "/", http.StatusFound)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,8 @@ import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Migrate given a dummy object of any type, it will create a table with the same name as the type and create columns with the same name as the fields of the object
 | 
			
		||||
// Migrate given a dummy object of any type, it will create a table with the same name
 | 
			
		||||
// as the type and create columns with the same name as the fields of the object
 | 
			
		||||
func Migrate(app *app.App, anyStruct interface{}) error {
 | 
			
		||||
	valueOfStruct := reflect.ValueOf(anyStruct)
 | 
			
		||||
	typeOfStruct := valueOfStruct.Type()
 | 
			
		||||
@@ -23,6 +24,10 @@ func Migrate(app *app.App, anyStruct interface{}) error {
 | 
			
		||||
	for i := 0; i < valueOfStruct.NumField(); i++ {
 | 
			
		||||
		fieldType := typeOfStruct.Field(i)
 | 
			
		||||
		fieldName := fieldType.Name
 | 
			
		||||
 | 
			
		||||
		// Create column if dummy for migration is NOT zero value
 | 
			
		||||
		fieldValue := valueOfStruct.Field(i).Interface()
 | 
			
		||||
		if !reflect.ValueOf(fieldValue).IsZero() {
 | 
			
		||||
			if fieldName != "Id" && fieldName != "id" {
 | 
			
		||||
				err := createColumn(app, tableName, fieldName, fieldType.Type.Name())
 | 
			
		||||
				if err != nil {
 | 
			
		||||
@@ -30,6 +35,7 @@ func Migrate(app *app.App, anyStruct interface{}) error {
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								env_example.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								env_example.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
{
 | 
			
		||||
  "Db": {
 | 
			
		||||
    "DbIp": "127.0.0.1",
 | 
			
		||||
    "DbPort": "5432",
 | 
			
		||||
    "DbName": "database",
 | 
			
		||||
    "DbUser": "user",
 | 
			
		||||
    "DbPassword": "password",
 | 
			
		||||
    "DbAutoMigrate": true
 | 
			
		||||
  },
 | 
			
		||||
  "Listen": {
 | 
			
		||||
    "HttpIp": "127.0.0.1",
 | 
			
		||||
    "HttpPort": "8090"
 | 
			
		||||
  },
 | 
			
		||||
  "Template": {
 | 
			
		||||
    "BaseTemplateName": "templates/base.html",
 | 
			
		||||
    "ContentPath": "templates"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
[Db]
 | 
			
		||||
DbIp = "127.0.0.1"
 | 
			
		||||
DbPort = "5432"
 | 
			
		||||
DbName = "test"
 | 
			
		||||
DbUser = "postgres"
 | 
			
		||||
DbPassword = "postgres"
 | 
			
		||||
DbAutoMigrate = true
 | 
			
		||||
 | 
			
		||||
[Listen]
 | 
			
		||||
HttpIp = "127.0.0.1"
 | 
			
		||||
HttpPort = "8090"
 | 
			
		||||
 | 
			
		||||
[Template]
 | 
			
		||||
BaseTemplateName = "templates/base.html"
 | 
			
		||||
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							@@ -1,10 +1,8 @@
 | 
			
		||||
module GoWeb
 | 
			
		||||
 | 
			
		||||
go 1.21
 | 
			
		||||
go 1.22
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/lib/pq v1.10.9
 | 
			
		||||
	golang.org/x/crypto v0.13.0
 | 
			
		||||
	golang.org/x/crypto v0.19.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require github.com/BurntSushi/toml v1.3.2
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
								
							@@ -1,6 +1,4 @@
 | 
			
		||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
 | 
			
		||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 | 
			
		||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 | 
			
		||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 | 
			
		||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
 | 
			
		||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
 | 
			
		||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
 | 
			
		||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								main.go
									
									
									
									
									
								
							@@ -6,6 +6,7 @@ import (
 | 
			
		||||
	"GoWeb/database"
 | 
			
		||||
	"GoWeb/models"
 | 
			
		||||
	"GoWeb/routes"
 | 
			
		||||
	"GoWeb/templating"
 | 
			
		||||
	"context"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"errors"
 | 
			
		||||
@@ -67,6 +68,13 @@ func main() {
 | 
			
		||||
	routes.Get(&appLoaded)
 | 
			
		||||
	routes.Post(&appLoaded)
 | 
			
		||||
 | 
			
		||||
	// Prepare templates
 | 
			
		||||
	err = templating.BuildPages(&appLoaded)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Error("error building templates: " + err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Start server
 | 
			
		||||
	server := &http.Server{Addr: appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port}
 | 
			
		||||
	go func() {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ func RunAllMigrations(app *app.App) error {
 | 
			
		||||
		Id:         1,
 | 
			
		||||
		UserId:     1,
 | 
			
		||||
		AuthToken:  "migrate",
 | 
			
		||||
		RememberMe: false,
 | 
			
		||||
		RememberMe: true, // Booleans must be true to migrate properly
 | 
			
		||||
		CreatedAt:  time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
	err = database.Migrate(app, session)
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ type Session struct {
 | 
			
		||||
	CreatedAt  time.Time
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const sessionColumnsNoId = "\"UserId\", \"AuthToken\",\"RememberMe\", \"CreatedAt\""
 | 
			
		||||
const sessionColumnsNoId = "\"UserId\", \"AuthToken\", \"RememberMe\", \"CreatedAt\""
 | 
			
		||||
const sessionColumns = "\"Id\", " + sessionColumnsNoId
 | 
			
		||||
const sessionTable = "public.\"Session\""
 | 
			
		||||
 | 
			
		||||
@@ -62,7 +62,7 @@ func CreateSession(app *app.App, w http.ResponseWriter, userId int64, remember b
 | 
			
		||||
	return session, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetSessionByAuthToken(app *app.App, authToken string) (Session, error) {
 | 
			
		||||
func SessionByAuthToken(app *app.App, authToken string) (Session, error) {
 | 
			
		||||
	session := Session{}
 | 
			
		||||
 | 
			
		||||
	err := app.Db.QueryRow(selectSessionByAuthToken, authToken).Scan(&session.Id, &session.UserId, &session.AuthToken, &session.RememberMe, &session.CreatedAt)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"GoWeb/app"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -27,23 +29,23 @@ const (
 | 
			
		||||
	insertUser           = "INSERT INTO " + userTable + " (" + userColumnsNoId + ") VALUES ($1, $2, $3, $4) RETURNING \"Id\""
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetCurrentUser finds the currently logged-in user by session cookie
 | 
			
		||||
func GetCurrentUser(app *app.App, r *http.Request) (User, error) {
 | 
			
		||||
// CurrentUser finds the currently logged-in user by session cookie
 | 
			
		||||
func CurrentUser(app *app.App, r *http.Request) (User, error) {
 | 
			
		||||
	cookie, err := r.Cookie("session")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return User{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session, err := GetSessionByAuthToken(app, cookie.Value)
 | 
			
		||||
	session, err := SessionByAuthToken(app, cookie.Value)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return User{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return GetUserById(app, session.UserId)
 | 
			
		||||
	return UserById(app, session.UserId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserById finds a User table row in the database by id and returns a struct representing this row
 | 
			
		||||
func GetUserById(app *app.App, id int64) (User, error) {
 | 
			
		||||
// UserById finds a User table row in the database by id and returns a struct representing this row
 | 
			
		||||
func UserById(app *app.App, id int64) (User, error) {
 | 
			
		||||
	user := User{}
 | 
			
		||||
 | 
			
		||||
	err := app.Db.QueryRow(selectUserById, id).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
 | 
			
		||||
@@ -54,8 +56,8 @@ func GetUserById(app *app.App, id int64) (User, error) {
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserByUsername finds a User table row in the database by username and returns a struct representing this row
 | 
			
		||||
func GetUserByUsername(app *app.App, username string) (User, error) {
 | 
			
		||||
// UserByUsername finds a User table row in the database by username and returns a struct representing this row
 | 
			
		||||
func UserByUsername(app *app.App, username string) (User, error) {
 | 
			
		||||
	user := User{}
 | 
			
		||||
 | 
			
		||||
	err := app.Db.QueryRow(selectUserByUsername, username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
 | 
			
		||||
@@ -68,7 +70,12 @@ func GetUserByUsername(app *app.App, username string) (User, error) {
 | 
			
		||||
 | 
			
		||||
// CreateUser creates a User table row in the database
 | 
			
		||||
func CreateUser(app *app.App, username string, password string, createdAt time.Time, updatedAt time.Time) (User, error) {
 | 
			
		||||
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 | 
			
		||||
	// Get sha256 hash of password then get bcrypt hash to store
 | 
			
		||||
	hash256 := sha256.New()
 | 
			
		||||
	hash256.Write([]byte(password))
 | 
			
		||||
	hashSum := hash256.Sum(nil)
 | 
			
		||||
	hashString := hex.EncodeToString(hashSum)
 | 
			
		||||
	hash, err := bcrypt.GenerateFromPassword([]byte(hashString), bcrypt.DefaultCost)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Error("error hashing password: " + err.Error())
 | 
			
		||||
		return User{}, err
 | 
			
		||||
@@ -82,7 +89,7 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T
 | 
			
		||||
		return User{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return GetUserById(app, lastInsertId)
 | 
			
		||||
	return UserById(app, lastInsertId)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AuthenticateUser validates the password for the specified user
 | 
			
		||||
@@ -95,7 +102,12 @@ func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, pass
 | 
			
		||||
		return Session{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
 | 
			
		||||
	// Get sha256 hash of password then check bcrypt
 | 
			
		||||
	hash256 := sha256.New()
 | 
			
		||||
	hash256.Write([]byte(password))
 | 
			
		||||
	hashSum := hash256.Sum(nil)
 | 
			
		||||
	hashString := hex.EncodeToString(hashSum)
 | 
			
		||||
	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(hashString))
 | 
			
		||||
	if err != nil { // Failed to validate password, doesn't match
 | 
			
		||||
		slog.Info("incorrect password:" + username)
 | 
			
		||||
		return Session{}, err
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,11 @@ func Get(app *app.App) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	staticHandler := http.FileServer(http.FS(staticFS))
 | 
			
		||||
	http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
 | 
			
		||||
	http.Handle("GET /static/", http.StripPrefix("/static/", staticHandler))
 | 
			
		||||
	slog.Info("serving static files from embedded file system /static")
 | 
			
		||||
 | 
			
		||||
	// Pages
 | 
			
		||||
	http.HandleFunc("/", getController.ShowHome)
 | 
			
		||||
	http.HandleFunc("/login", getController.ShowLogin)
 | 
			
		||||
	http.HandleFunc("/register", getController.ShowRegister)
 | 
			
		||||
	http.HandleFunc("/logout", getController.Logout)
 | 
			
		||||
	http.HandleFunc("GET /", getController.ShowHome)
 | 
			
		||||
	http.HandleFunc("GET /login", getController.ShowLogin)
 | 
			
		||||
	http.HandleFunc("GET /register", getController.ShowRegister)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ func Post(app *app.App) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// User authentication
 | 
			
		||||
	http.HandleFunc("/register-handle", middleware.Csrf(postController.Register))
 | 
			
		||||
	http.HandleFunc("/login-handle", middleware.Csrf(postController.Login))
 | 
			
		||||
	http.HandleFunc("POST /register-handle", middleware.Csrf(postController.Register))
 | 
			
		||||
	http.HandleFunc("POST /login-handle", middleware.Csrf(postController.Login))
 | 
			
		||||
	http.HandleFunc("POST /logout", middleware.Csrf(postController.Logout))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,24 @@
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <title>SiteName - {{ template "pageTitle" }}</title>
 | 
			
		||||
    <link rel="stylesheet" href="/static/css/style.css">
 | 
			
		||||
    <link href="/static/css/style.css" rel="stylesheet">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div class="navbar">
 | 
			
		||||
    {{ if .IsAuthenticated }}
 | 
			
		||||
    <form action="/logout" method="post">
 | 
			
		||||
        <input name="csrf_token" type="hidden" value="{{ .CsrfToken }}">
 | 
			
		||||
        <input type="submit" value="Logout">
 | 
			
		||||
    </form>
 | 
			
		||||
    {{ else }}
 | 
			
		||||
    <form action="/login" method="get">
 | 
			
		||||
        <input type="submit" value="Login">
 | 
			
		||||
    </form>
 | 
			
		||||
    <form action="/register" method="get">
 | 
			
		||||
        <input type="submit" value="Register">
 | 
			
		||||
    </form>
 | 
			
		||||
    {{ end }}
 | 
			
		||||
</div>
 | 
			
		||||
{{ template "content" . }}
 | 
			
		||||
<div class="footer-container">
 | 
			
		||||
    <footer>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,11 +7,11 @@
 | 
			
		||||
        <input name="csrf_token" type="hidden" value="{{ .CsrfToken }}">
 | 
			
		||||
 | 
			
		||||
        <label for="username">Username:</label><br>
 | 
			
		||||
        <input id="username" name="username" type="text" placeholder="John"><br><br>
 | 
			
		||||
        <input id="username" name="username" placeholder="John" type="text"><br><br>
 | 
			
		||||
        <label for="password">Password:</label><br>
 | 
			
		||||
        <input id="password" name="password" type="password"><br><br>
 | 
			
		||||
        <label for="remember">Remember Me:</label>
 | 
			
		||||
        <input id="remember" type="checkbox" name="remember"><br><br>
 | 
			
		||||
        <input id="remember" name="remember" type="checkbox"><br><br>
 | 
			
		||||
        <input type="submit" value="Submit">
 | 
			
		||||
    </form>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
        <input name="csrf_token" type="hidden" value="{{ .CsrfToken }}">
 | 
			
		||||
 | 
			
		||||
        <label for="username">Username:</label><br>
 | 
			
		||||
        <input id="username" name="username" type="text" placeholder="John"><br><br>
 | 
			
		||||
        <input id="username" name="username" placeholder="John" type="text"><br><br>
 | 
			
		||||
        <label for="password">Password:</label><br>
 | 
			
		||||
        <input id="password" name="password" type="password"><br><br>
 | 
			
		||||
        <input type="submit" value="Submit">
 | 
			
		||||
 
 | 
			
		||||
@@ -2,45 +2,82 @@ package templating
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"GoWeb/app"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RenderTemplate renders and serves a template from the embedded filesystem optionally with given data
 | 
			
		||||
func RenderTemplate(app *app.App, w http.ResponseWriter, contentPath string, data any) {
 | 
			
		||||
	templatePath := app.Config.Template.BaseName
 | 
			
		||||
var templates = make(map[string]*template.Template) // This is only used here, does not need to be in app.App
 | 
			
		||||
 | 
			
		||||
	templateContent, err := app.Res.ReadFile(templatePath)
 | 
			
		||||
func BuildPages(app *app.App) error {
 | 
			
		||||
	basePath := app.Config.Template.BaseName
 | 
			
		||||
 | 
			
		||||
	baseContent, err := app.Res.ReadFile(basePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Error(err.Error())
 | 
			
		||||
		http.Error(w, err.Error(), 500)
 | 
			
		||||
		return
 | 
			
		||||
		return fmt.Errorf("error reading base file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, err := template.New(templatePath).Parse(string(templateContent))
 | 
			
		||||
	base, err := template.New(basePath).Parse(string(baseContent)) // Sets filepath as name and parses content
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Error(err.Error())
 | 
			
		||||
		http.Error(w, err.Error(), 500)
 | 
			
		||||
		return
 | 
			
		||||
		return fmt.Errorf("error parsing base file: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	readFilesRecursively := func(fsys fs.FS, root string) ([]string, error) {
 | 
			
		||||
		var files []string
 | 
			
		||||
		err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("error walking the path %q: %w", path, err)
 | 
			
		||||
			}
 | 
			
		||||
			if !d.IsDir() {
 | 
			
		||||
				files = append(files, path)
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		})
 | 
			
		||||
		return files, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get all file paths in the directory tree
 | 
			
		||||
	filePaths, err := readFilesRecursively(app.Res, app.Config.Template.ContentPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("error reading files recursively: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, contentPath := range filePaths { // Create a new template base + content for each page
 | 
			
		||||
		content, err := app.Res.ReadFile(contentPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error reading content file %s: %w", contentPath, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		t, err := base.Clone()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error cloning base template: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = t.Parse(string(content))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("error parsing content: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		templates[contentPath] = t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RenderTemplate(w http.ResponseWriter, contentPath string, data any) {
 | 
			
		||||
	t, ok := templates[contentPath]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		err := fmt.Errorf("template not found for path: %s", contentPath)
 | 
			
		||||
		slog.Error(err.Error())
 | 
			
		||||
		http.Error(w, err.Error(), 500)
 | 
			
		||||
		http.Error(w, "Template not found", 404)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, err = t.Parse(string(content))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Error(err.Error())
 | 
			
		||||
		http.Error(w, err.Error(), 500)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = t.Execute(w, data)
 | 
			
		||||
	err := t.Execute(w, data) // Execute prebuilt template with dynamic data
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = fmt.Errorf("error executing template: %w", err)
 | 
			
		||||
		slog.Error(err.Error())
 | 
			
		||||
		http.Error(w, err.Error(), 500)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user