Compare commits

...

56 Commits

Author SHA1 Message Date
max
86ff949eae Update x/crypto 2024-06-25 18:17:21 -05:00
max
8476e37499 Update x/crypto 2024-04-19 11:40:15 -05:00
max
aad9cdfaf5 Merge remote-tracking branch 'origin/master' 2024-02-28 09:52:10 -06:00
max
3738ba689e Update x/crypto 2024-02-28 09:51:56 -06:00
Maximilian
a833823ad6 Fix wording 2024-02-18 17:23:23 -06:00
max
de4a217c5f Update extended crypto library 2024-02-09 14:47:29 -06:00
max
c4e83d06b9 Bump go version to 1.22 2024-02-09 14:21:45 -06:00
max
51da24be9b Small formatting fix 2024-02-09 13:44:21 -06:00
Maximilian
e497f4d2f0 Ignore fields that are zero value 2024-01-20 16:32:07 -06:00
Maximilian
b30af86e58 Prebuild templates (base.html + content) at startup to avoid a file parse every page load 2023-12-22 21:03:15 -06:00
Maximilian
3ffd548623 Fix ordering for html attributes 2023-12-21 00:14:28 -06:00
Maximilian
cb4f10e0b4 Better alignment for memory 2023-12-19 16:41:31 -06:00
Maximilian
878ce01b29 Get the sha256 hash of password before passing to bcrypt to avoid character limit 2023-12-19 16:06:00 -06:00
Maximilian
c82cdb4f13 Use best naming practices 2023-12-18 23:04:31 -06:00
Maximilian
ce81c36e9f Update x/crypto 2023-12-18 23:01:19 -06:00
Maximilian
ab1b82c680 Update x/crypto 2023-10-10 21:37:54 -05:00
Maximilian
d8b1a5c999 Remove unnecessary comparison 2023-09-26 11:32:39 -05:00
Maximilian
0f59a6eba9 Go mod tidy and update x/crypto 2023-09-17 19:23:57 -05:00
Maximilian
bb69c16cdf Update How to use section 2023-09-03 16:18:48 -05:00
Maximilian
e87f14bcbf Make all slog errors uniform (lowercase) 2023-09-03 15:56:35 -05:00
Maximilian
ed712a5344 Clean up error handling, migrate to log/slog, add todo for flash message system in post controller 2023-09-03 15:45:12 -05:00
Maximilian
ee4c9f9199 Clean up error handling, begin migration to log/slog 2023-08-17 21:13:17 -05:00
Maximilian
1d400efa19 Update x/crypto and Go version 2023-08-12 14:30:12 -05:00
Maximilian
b36cbf4b9e Comment cleanup 2023-08-12 14:28:21 -05:00
Maximilian
fa3165d317 Use proper error comparison 2023-08-03 12:13:37 -05:00
Maximilian
7cb36db3c2 Handle errors 2023-08-03 12:13:15 -05:00
Maximilian
eda5344685 Fix spelling 2023-08-03 12:11:01 -05:00
Maximilian
9670b7d717 Better naming of functions and files, delete less than helpful comments 2023-08-03 12:09:40 -05:00
Maximilian
a1438f4fe2 Update README.md 2023-07-31 20:18:11 -05:00
Maximilian
052fa689c7 Merge branch 'middleware' 2023-07-31 18:41:49 -05:00
Maximilian
f1fad7e4e3 Pass in handler to middleware, create definition for MiddlewareFunc 2023-07-31 18:37:54 -05:00
Maximilian
b475da66da Fix nil error for GET requests 2023-07-26 13:09:01 -05:00
Maximilian
d0085ab2c3 Update crypto dependency 2023-07-25 15:36:25 -05:00
Maximilian
58514f4c5f Update client to handle GET (no body) requests, multipart requests, and JSON requests 2023-07-25 15:34:11 -05:00
Maximilian
606f5df45a Refactor name 2023-07-22 23:37:38 -05:00
Maximilian
2a32a1b3ce Add restclient package, and a simple function to send HTTP requests 2023-07-21 16:35:18 -05:00
Maximilian
eb36156c52 Change function name to ProcessGroup 2023-07-21 16:26:43 -05:00
Maximilian
bada24884a Use ungrouped CSRF middleware on register and login POST routes 2023-07-21 11:59:55 -05:00
Maximilian
05397c2b61 Initial middleware implementation for CSRF and update comment 2023-07-21 11:59:01 -05:00
Maximilian
3d80b95f55 Initial wrapper implementation 2023-07-07 18:05:17 -05:00
Maximilian
6da7d408f9 Add .gitattributes to force LF line endings 2023-05-05 12:19:17 -05:00
max
e993bcf317 Update dependency versions 2023-05-04 09:00:35 -05:00
Maximilian
9b231a73d6 Update README.md 2023-04-07 21:32:39 -05:00
Maximilian
34acd0fa8d Remove old session query 2023-04-07 21:27:14 -05:00
Maximilian
71d3bd77d0 Add ability to get session given an AuthToken, fix GetCurrentUser() 2023-04-07 21:23:46 -05:00
Maximilian
1451abcca4 Formatting 2023-04-06 12:01:29 -05:00
max
53a780343f Fix scheduler by adding a wait group 2023-04-06 09:55:56 -05:00
max
8e4c5e3268 Fix wrong query for clearing 6-hour old sessions 2023-04-06 09:35:53 -05:00
max
f18f512fea Properly set the name of the checkbox for parsing 2023-04-06 09:31:12 -05:00
max
58328fe505 Fix some SQL errors 2023-04-06 09:30:53 -05:00
max
10e7830349 Remember me checkbox on login form 2023-04-06 08:57:17 -05:00
max
5f7e674d32 Add remember me functionality, handle both types of sessions appropriately 2023-04-06 08:56:48 -05:00
max
ec9c1a8fb5 Initial clear old sessions implementation 2023-04-04 14:37:36 -05:00
max
242029f2e5 Initial task scheduler implementation 2023-04-04 14:37:23 -05:00
Maximilian
b1c65f2ab1 Remove erroneous SetCookie (leftover from redundant remove) 2023-03-27 15:05:11 -05:00
max
965139ea18 Remove redundant session cookie clear 2023-03-16 08:40:50 -05:00
30 changed files with 599 additions and 349 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -12,11 +12,14 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
- Routing/controllers - Routing/controllers
- Templating - Templating
- Simple database migration system - Simple database migration system
- Built in REST client
- CSRF protection - CSRF protection
- Middleware
- Minimal user login/registration + sessions - Minimal user login/registration + sessions
- Config file handling - Config file handling
- Scheduled tasks
- Entire website compiles into a single binary (~10mb) (excluding env.json) - Entire website compiles into a single binary (~10mb) (excluding env.json)
- Minimal dependencies (just standard library, postgres driver, and experimental package for bcrypt) - Minimal dependencies (just standard library, postgres driver, and x/crypto for bcrypt)
<hr> <hr>
@ -36,10 +39,13 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
## How to use 🤔 ## How to use 🤔
1. Clone 1. Clone
2. Run `go get` to install dependencies 2. Delete the git folder, so you can start tracking in your own repo
3. Copy env_example.json to env.json and fill in the values 3. Run `go get` to install dependencies
4. Run `go run main.go` to start the server 4. Copy env_example.json to env.json and fill in the values
5. Start building your app! 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!
8. When you see useful changes to GoWeb you'd like in your project copy them over
## How to contribute 👨‍💻 ## How to contribute 👨‍💻
@ -53,7 +59,7 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
### License and disclaimer 😤 ### License and disclaimer 😤
- You are free to use this project under the terms of the MIT license. See LICENSE for more details. - You are free to use this project under the terms of the MIT license. See LICENSE for more details.
- You and you alone are responsible for the security and everything else regarding your application. - You are responsible for the security and everything else regarding your application.
- It is not required, but I ask that when you use this project you give me credit by linking to this repository. - It is not required, but I ask that when you use this project you give me credit by linking to this repository.
- I also ask that when releasing self-hosted or other end-user applications that you release it under - I also ask that when releasing self-hosted or other end-user applications that you release it under
the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) license. This too is not required, but I would appreciate it. the [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html) license. This too is not required, but I would appreciate it.

View File

@ -11,4 +11,5 @@ type App struct {
Config config.Configuration // Configuration file Config config.Configuration // Configuration file
Db *sql.DB // Database connection Db *sql.DB // Database connection
Res *embed.FS // Resources from the embedded filesystem Res *embed.FS // Resources from the embedded filesystem
ScheduledTasks Scheduled // Scheduled contains a struct of all scheduled functions
} }

71
app/schedule.go Normal file
View File

@ -0,0 +1,71 @@
package app
import (
"sync"
"time"
)
type Scheduled struct {
EveryReboot []func(app *App)
EverySecond []func(app *App)
EveryMinute []func(app *App)
EveryHour []func(app *App)
EveryDay []func(app *App)
EveryWeek []func(app *App)
EveryMonth []func(app *App)
EveryYear []func(app *App)
}
type Task struct {
Funcs []func(app *App)
Interval time.Duration
}
func RunScheduledTasks(app *App, poolSize int, stop <-chan struct{}) {
for _, f := range app.ScheduledTasks.EveryReboot {
f(app)
}
tasks := []Task{
{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
runners := make([]chan bool, len(tasks))
for i, task := range tasks {
runner := make(chan bool, poolSize)
runners[i] = runner
wg.Add(1)
go func(task Task, runner chan bool) {
defer wg.Done()
ticker := time.NewTicker(task.Interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
for _, f := range task.Funcs {
runner <- true
go func(f func(app *App)) {
defer func() { <-runner }()
f(app)
}(f)
}
case <-stop:
return
}
}
}(task, runner)
}
wg.Wait()
for _, runner := range runners {
close(runner)
}
}

View File

@ -3,7 +3,7 @@ package config
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"log" "log/slog"
"os" "os"
) )
@ -24,6 +24,7 @@ type Configuration struct {
Template struct { Template struct {
BaseName string `json:"BaseTemplateName"` BaseName string `json:"BaseTemplateName"`
ContentPath string `json:"ContentPath"`
} }
} }
@ -33,22 +34,21 @@ func LoadConfig() Configuration {
flag.Parse() flag.Parse()
file, err := os.Open(*c) file, err := os.Open(*c)
if err != nil { if err != nil {
log.Fatal("Unable to open JSON config file: ", err) panic("unable to open JSON config file: " + err.Error())
} }
defer func(file *os.File) { defer func(file *os.File) {
err := file.Close() err := file.Close()
if err != nil { if err != nil {
log.Fatal("Unable to close JSON config file: ", err) slog.Error("unable to close JSON config file: ", err)
} }
}(file) }(file)
// Decode json config file to Configuration struct named config
decoder := json.NewDecoder(file) decoder := json.NewDecoder(file)
Config := Configuration{} Config := Configuration{}
err = decoder.Decode(&Config) err = decoder.Decode(&Config)
if err != nil { if err != nil {
log.Fatal("Unable to decode JSON config file: ", err) panic("unable to decode JSON config file: " + err.Error())
} }
return Config return Config

65
controllers/get.go Normal file
View File

@ -0,0 +1,65 @@
package controllers
import (
"GoWeb/app"
"GoWeb/models"
"GoWeb/security"
"GoWeb/templating"
"net/http"
)
// Get is a wrapper struct for the App struct
type Get struct {
App *app.App
}
func (g *Get) ShowHome(w http.ResponseWriter, _ *http.Request) {
type dataStruct struct {
Test string
}
data := dataStruct{
Test: "Hello World!",
}
templating.RenderTemplate(w, "templates/pages/home.html", data)
}
func (g *Get) ShowRegister(w http.ResponseWriter, r *http.Request) {
type dataStruct struct {
CsrfToken string
}
CsrfToken, err := security.GenerateCsrfToken(w, r)
if err != nil {
return
}
data := dataStruct{
CsrfToken: CsrfToken,
}
templating.RenderTemplate(w, "templates/pages/register.html", data)
}
func (g *Get) ShowLogin(w http.ResponseWriter, r *http.Request) {
type dataStruct struct {
CsrfToken string
}
CsrfToken, err := security.GenerateCsrfToken(w, r)
if err != nil {
return
}
data := dataStruct{
CsrfToken: CsrfToken,
}
templating.RenderTemplate(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)
}

View File

@ -1,67 +0,0 @@
package controllers
import (
"GoWeb/app"
"GoWeb/models"
"GoWeb/security"
"GoWeb/templating"
"net/http"
)
// GetController is a wrapper struct for the App struct
type GetController struct {
App *app.App
}
func (getController *GetController) ShowHome(w http.ResponseWriter, _ *http.Request) {
type dataStruct struct {
Test string
}
data := dataStruct{
Test: "Hello World!",
}
templating.RenderTemplate(getController.App, w, "templates/pages/home.html", data)
}
func (getController *GetController) ShowRegister(w http.ResponseWriter, r *http.Request) {
type dataStruct struct {
CsrfToken string
}
// Create csrf token
CsrfToken, err := security.GenerateCsrfToken(w, r)
if err != nil {
return
}
data := dataStruct{
CsrfToken: CsrfToken,
}
templating.RenderTemplate(getController.App, w, "templates/pages/register.html", data)
}
func (getController *GetController) ShowLogin(w http.ResponseWriter, r *http.Request) {
type dataStruct struct {
CsrfToken string
}
// Create csrf token
CsrfToken, err := security.GenerateCsrfToken(w, r)
if err != nil {
return
}
data := dataStruct{
CsrfToken: CsrfToken,
}
templating.RenderTemplate(getController.App, w, "templates/pages/login.html", data)
}
func (getController *GetController) Logout(w http.ResponseWriter, r *http.Request) {
models.LogoutUser(getController.App, w, r)
http.Redirect(w, r, "/", http.StatusFound)
}

52
controllers/post.go Normal file
View File

@ -0,0 +1,52 @@
package controllers
import (
"GoWeb/app"
"GoWeb/models"
"log/slog"
"net/http"
"time"
)
// Post is a wrapper struct for the App struct
type Post struct {
App *app.App
}
func (p *Post) Login(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
remember := r.FormValue("remember") == "on"
if username == "" || password == "" {
http.Redirect(w, r, "/login", http.StatusUnauthorized)
}
_, err := models.AuthenticateUser(p.App, w, username, password, remember)
if err != nil {
http.Redirect(w, r, "/login", http.StatusUnauthorized)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (p *Post) Register(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
createdAt := time.Now()
updatedAt := time.Now()
if username == "" || password == "" {
http.Redirect(w, r, "/register", http.StatusUnauthorized)
}
_, err := models.CreateUser(p.App, username, password, createdAt, updatedAt)
if err != nil {
// TODO: if err == bcrypt.ErrPasswordTooLong display error to user, this will require a flash message system with cookies
slog.Error("error creating user: " + err.Error())
http.Redirect(w, r, "/register", http.StatusInternalServerError)
}
http.Redirect(w, r, "/login", http.StatusFound)
}

View File

@ -1,70 +0,0 @@
package controllers
import (
"GoWeb/app"
"GoWeb/models"
"GoWeb/security"
"log"
"net/http"
"time"
)
// PostController is a wrapper struct for the App struct
type PostController struct {
App *app.App
}
func (postController *PostController) Login(w http.ResponseWriter, r *http.Request) {
// Validate csrf token
_, err := security.VerifyCsrfToken(r)
if err != nil {
log.Println("Error verifying csrf token")
return
}
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
log.Println("Tried to login user with empty username or password")
http.Redirect(w, r, "/login", http.StatusFound)
}
_, err = models.AuthenticateUser(postController.App, w, username, password)
if err != nil {
log.Println("Error authenticating user")
log.Println(err)
http.Redirect(w, r, "/login", http.StatusFound)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
func (postController *PostController) Register(w http.ResponseWriter, r *http.Request) {
// Validate csrf token
_, err := security.VerifyCsrfToken(r)
if err != nil {
log.Println("Error verifying csrf token")
return
}
username := r.FormValue("username")
password := r.FormValue("password")
createdAt := time.Now()
updatedAt := time.Now()
if username == "" || password == "" {
log.Println("Tried to create user with empty username or password")
http.Redirect(w, r, "/register", http.StatusFound)
}
_, err = models.CreateUser(postController.App, username, password, createdAt, updatedAt)
if err != nil {
log.Println("Error creating user")
log.Println(err)
return
}
http.Redirect(w, r, "/login", http.StatusFound)
}

View File

@ -5,29 +5,26 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"log" "log/slog"
) )
// ConnectDB returns a new database connection // Connect returns a new database connection
func ConnectDB(app *app.App) *sql.DB { func Connect(app *app.App) *sql.DB {
// Set connection parameters from config
postgresConfig := fmt.Sprintf("host=%s port=%s user=%s "+ postgresConfig := fmt.Sprintf("host=%s port=%s user=%s "+
"password=%s dbname=%s sslmode=disable", "password=%s dbname=%s sslmode=disable",
app.Config.Db.Ip, app.Config.Db.Port, app.Config.Db.User, app.Config.Db.Password, app.Config.Db.Name) app.Config.Db.Ip, app.Config.Db.Port, app.Config.Db.User, app.Config.Db.Password, app.Config.Db.Name)
// Create connection
db, err := sql.Open("postgres", postgresConfig) db, err := sql.Open("postgres", postgresConfig)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Test connection
err = db.Ping() err = db.Ping()
if err != nil { if err != nil {
panic(err) panic(err)
} }
log.Println("Connected to database successfully on " + app.Config.Db.Ip + ":" + app.Config.Db.Port + " using database " + app.Config.Db.Name) slog.Info("connected to database successfully on " + app.Config.Db.Ip + ":" + app.Config.Db.Port + " using database " + app.Config.Db.Name)
return db return db
} }

View File

@ -5,11 +5,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/lib/pq" "github.com/lib/pq"
"log" "log/slog"
"reflect" "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 { func Migrate(app *app.App, anyStruct interface{}) error {
valueOfStruct := reflect.ValueOf(anyStruct) valueOfStruct := reflect.ValueOf(anyStruct)
typeOfStruct := valueOfStruct.Type() typeOfStruct := valueOfStruct.Type()
@ -23,6 +24,10 @@ func Migrate(app *app.App, anyStruct interface{}) error {
for i := 0; i < valueOfStruct.NumField(); i++ { for i := 0; i < valueOfStruct.NumField(); i++ {
fieldType := typeOfStruct.Field(i) fieldType := typeOfStruct.Field(i)
fieldName := fieldType.Name 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" { if fieldName != "Id" && fieldName != "id" {
err := createColumn(app, tableName, fieldName, fieldType.Type.Name()) err := createColumn(app, tableName, fieldName, fieldType.Type.Name())
if err != nil { if err != nil {
@ -30,54 +35,53 @@ func Migrate(app *app.App, anyStruct interface{}) error {
} }
} }
} }
}
return nil return nil
} }
// createTable creates a table with the given name if it doesn't exist, it is assumed that id will be the primary key // createTable creates a table with the given name if it doesn't exist, it is assumed that id will be the primary key
func createTable(app *app.App, tableName string) error { func createTable(app *app.App, tableName string) error {
// Check to see if the table already exists
var tableExists bool var tableExists bool
err := app.Db.QueryRow("SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname ~ $1 AND pg_catalog.pg_table_is_visible(c.oid))", "^"+tableName+"$").Scan(&tableExists) err := app.Db.QueryRow("SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname ~ $1 AND pg_catalog.pg_table_is_visible(c.oid))", "^"+tableName+"$").Scan(&tableExists)
if err != nil { if err != nil {
log.Println("Error checking if table exists: " + tableName) slog.Error("error checking if table exists: " + tableName)
return err return err
} }
if tableExists { if tableExists {
log.Println("Table already exists: " + tableName) slog.Info("table already exists: " + tableName)
return nil return nil
} else { } else {
sanitizedTableQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS \"%s\" (\"Id\" serial primary key)", tableName) sanitizedTableQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS \"%s\" (\"Id\" serial primary key)", tableName)
_, err := app.Db.Query(sanitizedTableQuery) _, err := app.Db.Query(sanitizedTableQuery)
if err != nil { if err != nil {
log.Println("Error creating table: " + tableName) slog.Error("error creating table: " + tableName)
return err return err
} }
log.Println("Table created successfully: " + tableName) slog.Info("table created successfully: " + tableName)
return nil return nil
} }
} }
// createColumn creates a column with the given name and type if it doesn't exist // createColumn creates a column with the given name and type if it doesn't exist
func createColumn(app *app.App, tableName, columnName, columnType string) error { func createColumn(app *app.App, tableName, columnName, columnType string) error {
// Check to see if the column already exists
var columnExists bool var columnExists bool
err := app.Db.QueryRow("SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = $2)", tableName, columnName).Scan(&columnExists) err := app.Db.QueryRow("SELECT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = $1 AND column_name = $2)", tableName, columnName).Scan(&columnExists)
if err != nil { if err != nil {
log.Println("Error checking if column exists: " + columnName + " in table: " + tableName) slog.Error("error checking if column exists: " + columnName + " in table: " + tableName)
return err return err
} }
if columnExists { if columnExists {
log.Println("Column already exists: " + columnName + " in table: " + tableName) slog.Info("column already exists: " + columnName + " in table: " + tableName)
return nil return nil
} else { } else {
postgresType, err := getPostgresType(columnType) postgresType, err := getPostgresType(columnType)
if err != nil { if err != nil {
log.Println("Error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType) slog.Error("error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType)
return err return err
} }
@ -86,11 +90,11 @@ func createColumn(app *app.App, tableName, columnName, columnType string) error
_, err = app.Db.Query(query) _, err = app.Db.Query(query)
if err != nil { if err != nil {
log.Println("Error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType) slog.Error("error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType)
return err return err
} }
log.Println("Column created successfully:", columnName) slog.Info("column created successfully:", columnName)
return nil return nil
} }

View File

@ -12,6 +12,7 @@
"HttpPort": "8090" "HttpPort": "8090"
}, },
"Template": { "Template": {
"BaseTemplateName": "templates/base.html" "BaseTemplateName": "templates/base.html",
"ContentPath": "templates"
} }
} }

6
go.mod
View File

@ -1,8 +1,8 @@
module GoWeb module GoWeb
go 1.20 go 1.22
require ( require (
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.9
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.24.0
) )

8
go.sum
View File

@ -1,4 +1,4 @@
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=

55
main.go
View File

@ -6,9 +6,11 @@ import (
"GoWeb/database" "GoWeb/database"
"GoWeb/models" "GoWeb/models"
"GoWeb/routes" "GoWeb/routes"
"GoWeb/templating"
"context" "context"
"embed" "embed"
"log" "errors"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -33,48 +35,69 @@ func main() {
if _, err := os.Stat("logs"); os.IsNotExist(err) { if _, err := os.Stat("logs"); os.IsNotExist(err) {
err := os.Mkdir("logs", 0755) err := os.Mkdir("logs", 0755)
if err != nil { if err != nil {
log.Println("Failed to create log directory") panic("failed to create log directory: " + err.Error())
log.Println(err)
return
} }
} }
// Create log file and set output // Create log file and set output
file, err := os.OpenFile("logs/"+time.Now().Format("2006-01-02")+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) file, err := os.OpenFile("logs/"+time.Now().Format("2006-01-02")+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
log.SetOutput(file) if err != nil {
panic("error creating log file: " + err.Error())
}
logger := slog.New(slog.NewTextHandler(file, nil))
slog.SetDefault(logger) // Set structured logger globally
// Connect to database and run migrations // Connect to database and run migrations
appLoaded.Db = database.ConnectDB(&appLoaded) appLoaded.Db = database.Connect(&appLoaded)
if appLoaded.Config.Db.AutoMigrate { if appLoaded.Config.Db.AutoMigrate {
err = models.RunAllMigrations(&appLoaded) err = models.RunAllMigrations(&appLoaded)
if err != nil { if err != nil {
log.Println(err) slog.Error("error running migrations: " + err.Error())
return os.Exit(1)
} }
} }
// Assign and run scheduled tasks
appLoaded.ScheduledTasks = app.Scheduled{
EveryReboot: []func(app *app.App){models.ScheduledSessionCleanup},
EveryMinute: []func(app *app.App){models.ScheduledSessionCleanup},
}
// Define Routes // Define Routes
routes.GetRoutes(&appLoaded) routes.Get(&appLoaded)
routes.PostRoutes(&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 // Start server
server := &http.Server{Addr: appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port} server := &http.Server{Addr: appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port}
go func() { go func() {
log.Println("Starting server and listening on " + appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port) slog.Info("starting server and listening on " + appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port)
err := server.ListenAndServe() err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("Could not listen on %s: %v\n", appLoaded.Config.Listen.Ip+":"+appLoaded.Config.Listen.Port, err) slog.Error("could not listen on %s: %v\n", appLoaded.Config.Listen.Ip+":"+appLoaded.Config.Listen.Port, err)
os.Exit(1)
} }
}() }()
// Wait for interrupt signal and shut down the server // Wait for interrupt signal and shut down the server
interrupt := make(chan os.Signal, 1) interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
stop := make(chan struct{})
go app.RunScheduledTasks(&appLoaded, 100, stop)
<-interrupt <-interrupt
log.Println("Interrupt signal received. Shutting down server...") slog.Info("interrupt signal received. Shutting down server...")
err = server.Shutdown(context.Background()) err = server.Shutdown(context.Background())
if err != nil { if err != nil {
log.Fatalf("Could not gracefully shutdown the server: %v\n", err) slog.Error("could not gracefully shutdown the server: %v\n", err)
os.Exit(1)
} }
} }

21
middleware/csrf.go Normal file
View File

@ -0,0 +1,21 @@
package middleware
import (
"GoWeb/security"
"log/slog"
"net/http"
)
// Csrf validates the CSRF token and returns the handler function if it succeeded
func Csrf(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
_, err := security.VerifyCsrfToken(r)
if err != nil {
slog.Info("error verifying csrf token")
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
f(w, r)
}
}

5
middleware/groups.go Normal file
View File

@ -0,0 +1,5 @@
package middleware
import "net/http"
type MiddlewareFunc func(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request)

14
middleware/wrapper.go Normal file
View File

@ -0,0 +1,14 @@
package middleware
import "net/http"
// ProcessGroup is a wrapper function for the http.HandleFunc function
// that takes the function you want to execute (f) and the middleware you want
// to execute (m) this should be used when processing multiple groups of middleware at a time
func ProcessGroup(f func(w http.ResponseWriter, r *http.Request), m []MiddlewareFunc) func(w http.ResponseWriter, r *http.Request) {
for _, middleware := range m {
_ = middleware(f)
}
return f
}

View File

@ -25,6 +25,7 @@ func RunAllMigrations(app *app.App) error {
Id: 1, Id: 1,
UserId: 1, UserId: 1,
AuthToken: "migrate", AuthToken: "migrate",
RememberMe: false,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
err = database.Migrate(app, session) err = database.Migrate(app, session)

View File

@ -4,7 +4,7 @@ import (
"GoWeb/app" "GoWeb/app"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"log" "log/slog"
"net/http" "net/http"
"time" "time"
) )
@ -13,46 +13,48 @@ type Session struct {
Id int64 Id int64
UserId int64 UserId int64
AuthToken string AuthToken string
RememberMe bool
CreatedAt time.Time CreatedAt time.Time
} }
const sessionColumnsNoId = "\"UserId\", \"AuthToken\", \"CreatedAt\"" const sessionColumnsNoId = "\"UserId\", \"AuthToken\", \"RememberMe\", \"CreatedAt\""
const sessionColumns = "\"Id\", " + sessionColumnsNoId const sessionColumns = "\"Id\", " + sessionColumnsNoId
const sessionTable = "public.\"Session\"" const sessionTable = "public.\"Session\""
const ( const (
selectSessionByAuthToken = "SELECT " + sessionColumns + " FROM " + sessionTable + " WHERE \"AuthToken\" = $1" selectSessionByAuthToken = "SELECT " + sessionColumns + " FROM " + sessionTable + " WHERE \"AuthToken\" = $1"
selectAuthTokenIfExists = "SELECT EXISTS(SELECT 1 FROM " + sessionTable + " WHERE \"AuthToken\" = $1)" selectAuthTokenIfExists = "SELECT EXISTS(SELECT 1 FROM " + sessionTable + " WHERE \"AuthToken\" = $1)"
insertSession = "INSERT INTO " + sessionTable + " (" + sessionColumnsNoId + ") VALUES ($1, $2, $3) RETURNING \"Id\"" insertSession = "INSERT INTO " + sessionTable + " (" + sessionColumnsNoId + ") VALUES ($1, $2, $3, $4) RETURNING \"Id\""
deleteSessionByAuthToken = "DELETE FROM " + sessionTable + " WHERE \"AuthToken\" = $1" deleteSessionByAuthToken = "DELETE FROM " + sessionTable + " WHERE \"AuthToken\" = $1"
deleteSessionsOlderThan30Days = "DELETE FROM " + sessionTable + " WHERE \"CreatedAt\" < NOW() - INTERVAL '30 days'"
deleteSessionsOlderThan6Hours = "DELETE FROM " + sessionTable + " WHERE \"CreatedAt\" < NOW() - INTERVAL '6 hours' AND \"RememberMe\" = false"
) )
// CreateSession creates a new session for a user // CreateSession creates a new session for a user
func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session, error) { func CreateSession(app *app.App, w http.ResponseWriter, userId int64, remember bool) (Session, error) {
session := Session{} session := Session{}
session.UserId = userId session.UserId = userId
session.AuthToken = generateAuthToken(app) session.AuthToken = generateAuthToken(app)
session.RememberMe = remember
session.CreatedAt = time.Now() session.CreatedAt = time.Now()
// If the AuthToken column for any user matches the token, set existingAuthToken to true // If the AuthToken column for any user matches the token, set existingAuthToken to true
var existingAuthToken bool var existingAuthToken bool
err := app.Db.QueryRow(selectAuthTokenIfExists, session.AuthToken).Scan(&existingAuthToken) err := app.Db.QueryRow(selectAuthTokenIfExists, session.AuthToken).Scan(&existingAuthToken)
if err != nil { if err != nil {
log.Println("Error checking for existing auth token") slog.Error("error checking for existing auth token" + err.Error())
log.Println(err)
return Session{}, err return Session{}, err
} }
// If duplicate token found, recursively call function until unique token is generated // If duplicate token found, recursively call function until unique token is generated
if existingAuthToken == true { if existingAuthToken {
log.Println("Duplicate token found in sessions table, generating new token...") slog.Warn("duplicate token found in sessions table, generating new token...")
return CreateSession(app, w, userId) return CreateSession(app, w, userId, remember)
} }
// Insert session into database err = app.Db.QueryRow(insertSession, session.UserId, session.AuthToken, session.RememberMe, session.CreatedAt).Scan(&session.Id)
err = app.Db.QueryRow(insertSession, session.UserId, session.AuthToken, session.CreatedAt).Scan(&session.Id)
if err != nil { if err != nil {
log.Println("Error inserting session into database") slog.Error("error inserting session into database")
return Session{}, err return Session{}, err
} }
@ -60,29 +62,50 @@ func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session,
return session, nil return session, nil
} }
// Generates a random 64-byte string 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)
if err != nil {
return Session{}, err
}
return session, nil
}
// generateAuthToken generates a random 64-byte string
func generateAuthToken(app *app.App) string { func generateAuthToken(app *app.App) string {
// Generate random bytes
b := make([]byte, 64) b := make([]byte, 64)
_, err := rand.Read(b) _, err := rand.Read(b)
if err != nil { if err != nil {
log.Println("Error generating random bytes") slog.Error("error generating random bytes for auth token")
} }
// Convert random bytes to hex string
return hex.EncodeToString(b) return hex.EncodeToString(b)
} }
// createSessionCookie creates a new session cookie // createSessionCookie creates a new session cookie
func createSessionCookie(app *app.App, w http.ResponseWriter, session Session) { func createSessionCookie(app *app.App, w http.ResponseWriter, session Session) {
cookie := &http.Cookie{ cookie := &http.Cookie{}
if session.RememberMe {
cookie = &http.Cookie{
Name: "session", Name: "session",
Value: session.AuthToken, Value: session.AuthToken,
Path: "/", Path: "/",
MaxAge: 86400, MaxAge: 2592000 * 1000, // 30 days in ms
HttpOnly: true, HttpOnly: true,
Secure: true, Secure: true,
} }
} else {
cookie = &http.Cookie{
Name: "session",
Value: session.AuthToken,
Path: "/",
MaxAge: 21600 * 1000, // 6 hours in ms
HttpOnly: true,
Secure: true,
}
}
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
} }
@ -101,10 +124,9 @@ func deleteSessionCookie(app *app.App, w http.ResponseWriter) {
// DeleteSessionByAuthToken deletes a session from the database by AuthToken // DeleteSessionByAuthToken deletes a session from the database by AuthToken
func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken string) error { func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken string) error {
// Delete session from database
_, err := app.Db.Exec(deleteSessionByAuthToken, authToken) _, err := app.Db.Exec(deleteSessionByAuthToken, authToken)
if err != nil { if err != nil {
log.Println("Error deleting session from database") slog.Error("error deleting session from database")
return err return err
} }
@ -112,3 +134,20 @@ func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken str
return nil return nil
} }
// ScheduledSessionCleanup deletes expired sessions from the database
func ScheduledSessionCleanup(app *app.App) {
// Delete sessions older than 30 days (remember me sessions)
_, err := app.Db.Exec(deleteSessionsOlderThan30Days)
if err != nil {
slog.Error("error deleting 30 day expired sessions from database" + err.Error())
}
// Delete sessions older than 6 hours
_, err = app.Db.Exec(deleteSessionsOlderThan6Hours)
if err != nil {
slog.Error("error deleting 6 hour expired sessions from database" + err.Error())
}
slog.Info("deleted expired sessions from database")
}

View File

@ -2,9 +2,10 @@ package models
import ( import (
"GoWeb/app" "GoWeb/app"
"log" "crypto/sha256"
"encoding/hex"
"log/slog"
"net/http" "net/http"
"strconv"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -23,54 +24,44 @@ const userColumns = "\"Id\", " + userColumnsNoId
const userTable = "public.\"User\"" const userTable = "public.\"User\""
const ( const (
selectSessionIdByAuthToken = "SELECT \"Id\" FROM public.\"Session\" WHERE \"AuthToken\" = $1"
selectUserById = "SELECT " + userColumns + " FROM " + userTable + " WHERE \"Id\" = $1" selectUserById = "SELECT " + userColumns + " FROM " + userTable + " WHERE \"Id\" = $1"
selectUserByUsername = "SELECT " + userColumns + " FROM " + userTable + " WHERE \"Username\" = $1" selectUserByUsername = "SELECT " + userColumns + " FROM " + userTable + " WHERE \"Username\" = $1"
insertUser = "INSERT INTO " + userTable + " (" + userColumnsNoId + ") VALUES ($1, $2, $3, $4) RETURNING \"Id\"" insertUser = "INSERT INTO " + userTable + " (" + userColumnsNoId + ") VALUES ($1, $2, $3, $4) RETURNING \"Id\""
) )
// GetCurrentUser finds the currently logged-in user by session cookie // CurrentUser finds the currently logged-in user by session cookie
func GetCurrentUser(app *app.App, r *http.Request) (User, error) { func CurrentUser(app *app.App, r *http.Request) (User, error) {
cookie, err := r.Cookie("session") cookie, err := r.Cookie("session")
if err != nil { if err != nil {
log.Println("Error getting session cookie")
return User{}, err return User{}, err
} }
var userId int64 session, err := SessionByAuthToken(app, cookie.Value)
// Query row by AuthToken
err = app.Db.QueryRow(selectSessionIdByAuthToken, cookie.Value).Scan(&userId)
if err != nil { if err != nil {
log.Println("Error querying session row with session: " + cookie.Value)
return User{}, err return User{}, err
} }
return GetUserById(app, userId) return UserById(app, session.UserId)
} }
// GetUserById finds a User table row in the database by id and returns a struct representing this row // UserById 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) { func UserById(app *app.App, id int64) (User, error) {
user := User{} user := User{}
// Query row by id
err := app.Db.QueryRow(selectUserById, id).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt) err := app.Db.QueryRow(selectUserById, id).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
log.Println("Get user error (user not found) for user id:" + strconv.FormatInt(id, 10))
return User{}, err return User{}, err
} }
return user, nil return user, nil
} }
// GetUserByUsername finds a User table row in the database by username and returns a struct representing this row // UserByUsername 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) { func UserByUsername(app *app.App, username string) (User, error) {
user := User{} user := User{}
// Query row by username
err := app.Db.QueryRow(selectUserByUsername, username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt) err := app.Db.QueryRow(selectUserByUsername, username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
log.Println("Get user error (user not found) for user:" + username)
return User{}, err return User{}, err
} }
@ -79,10 +70,14 @@ func GetUserByUsername(app *app.App, username string) (User, error) {
// CreateUser creates a User table row in the database // 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) { func CreateUser(app *app.App, username string, password string, createdAt time.Time, updatedAt time.Time) (User, error) {
// Hash password // Get sha256 hash of password then get bcrypt hash to store
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 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 { if err != nil {
log.Println("Error hashing password when creating user") slog.Error("error hashing password: " + err.Error())
return User{}, err return User{}, err
} }
@ -90,57 +85,46 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T
err = app.Db.QueryRow(insertUser, username, string(hash), createdAt, updatedAt).Scan(&lastInsertId) err = app.Db.QueryRow(insertUser, username, string(hash), createdAt, updatedAt).Scan(&lastInsertId)
if err != nil { if err != nil {
log.Println("Error creating user row") slog.Error("error creating user row: " + err.Error())
return User{}, err return User{}, err
} }
return GetUserById(app, lastInsertId) return UserById(app, lastInsertId)
} }
// AuthenticateUser validates the password for the specified user // AuthenticateUser validates the password for the specified user
func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string) (Session, error) { func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string, remember bool) (Session, error) {
var user User var user User
// Query row by username
err := app.Db.QueryRow(selectUserByUsername, username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt) err := app.Db.QueryRow(selectUserByUsername, username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err != nil { if err != nil {
log.Println("Authentication error (user not found) for user:" + username) slog.Info("user not found: " + username)
return Session{}, err return Session{}, err
} }
// Validate password // Get sha256 hash of password then check bcrypt
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) 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 if err != nil { // Failed to validate password, doesn't match
log.Println("Authentication error (incorrect password) for user:" + username) slog.Info("incorrect password:" + username)
return Session{}, err return Session{}, err
} else { } else {
return CreateSession(app, w, user.Id) return CreateSession(app, w, user.Id, remember)
} }
} }
// LogoutUser deletes the session cookie and AuthToken from the database // LogoutUser deletes the session cookie and AuthToken from the database
func LogoutUser(app *app.App, w http.ResponseWriter, r *http.Request) { func LogoutUser(app *app.App, w http.ResponseWriter, r *http.Request) {
// Get cookie from request
cookie, err := r.Cookie("session") cookie, err := r.Cookie("session")
if err != nil { if err != nil {
log.Println("Error getting cookie from request")
return return
} }
// Set token to empty string
err = DeleteSessionByAuthToken(app, w, cookie.Value) err = DeleteSessionByAuthToken(app, w, cookie.Value)
if err != nil { if err != nil {
log.Println("Error deleting session by AuthToken")
return return
} }
// Delete cookie
cookie = &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
} }

65
restclient/client.go Normal file
View File

@ -0,0 +1,65 @@
package restclient
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
)
// SendRequest sends an HTTP request to a URL and includes the specified headers and body.
// A body can be nil for GET requests, a map[string]string for multipart/form-data requests,
// or a struct for JSON requests
func SendRequest(url string, method string, headers map[string]string, body interface{}) (http.Response, error) {
var reqBody *bytes.Buffer
var contentType string
switch v := body.(type) {
case nil:
reqBody = bytes.NewBuffer([]byte(""))
case map[string]string:
reqBody = &bytes.Buffer{}
writer := multipart.NewWriter(reqBody)
for key, value := range v {
err := writer.WriteField(key, value)
if err != nil {
return http.Response{}, err
}
}
err := writer.Close()
if err != nil {
return http.Response{}, err
}
contentType = writer.FormDataContentType()
default:
jsonBody, err := json.Marshal(body)
if err != nil {
return http.Response{}, err
}
reqBody = bytes.NewBuffer(jsonBody)
contentType = "application/json"
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
return http.Response{}, err
}
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
for key, value := range headers {
req.Header.Add(key, value)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return http.Response{}, err
}
return *resp, nil
}

View File

@ -4,26 +4,26 @@ import (
"GoWeb/app" "GoWeb/app"
"GoWeb/controllers" "GoWeb/controllers"
"io/fs" "io/fs"
"log" "log/slog"
"net/http" "net/http"
) )
// GetRoutes defines all project get routes // Get defines all project get routes
func GetRoutes(app *app.App) { func Get(app *app.App) {
// Get controller struct initialize // Get controller struct initialize
getController := controllers.GetController{ getController := controllers.Get{
App: app, App: app,
} }
// Serve static files // Serve static files
staticFS, err := fs.Sub(app.Res, "static") staticFS, err := fs.Sub(app.Res, "static")
if err != nil { if err != nil {
log.Println(err) slog.Error(err.Error())
return return
} }
staticHandler := http.FileServer(http.FS(staticFS)) staticHandler := http.FileServer(http.FS(staticFS))
http.Handle("/static/", http.StripPrefix("/static/", staticHandler)) http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
log.Println("Serving static files from embedded file system /static") slog.Info("serving static files from embedded file system /static")
// Pages // Pages
http.HandleFunc("/", getController.ShowHome) http.HandleFunc("/", getController.ShowHome)

20
routes/post.go Normal file
View File

@ -0,0 +1,20 @@
package routes
import (
"GoWeb/app"
"GoWeb/controllers"
"GoWeb/middleware"
"net/http"
)
// Post defines all project post routes
func Post(app *app.App) {
// Post controller struct initialize
postController := controllers.Post{
App: app,
}
// User authentication
http.HandleFunc("/register-handle", middleware.Csrf(postController.Register))
http.HandleFunc("/login-handle", middleware.Csrf(postController.Login))
}

View File

@ -1,19 +0,0 @@
package routes
import (
"GoWeb/app"
"GoWeb/controllers"
"net/http"
)
// PostRoutes defines all project post routes
func PostRoutes(app *app.App) {
// Post controller struct initialize
postController := controllers.PostController{
App: app,
}
// User authentication
http.HandleFunc("/register-handle", postController.Register)
http.HandleFunc("/login-handle", postController.Login)
}

View File

@ -3,19 +3,17 @@ package security
import ( import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"log" "log/slog"
"math" "math"
"net/http" "net/http"
) )
// GenerateCsrfToken generates a csrf token and assigns it to a cookie for double submit cookie csrf protection // GenerateCsrfToken generates a csrf token and assigns it to a cookie for double submit cookie csrf protection
func GenerateCsrfToken(w http.ResponseWriter, _ *http.Request) (string, error) { func GenerateCsrfToken(w http.ResponseWriter, _ *http.Request) (string, error) {
// Generate random 64 character string (alpha-numeric)
buff := make([]byte, int(math.Ceil(float64(64)/2))) buff := make([]byte, int(math.Ceil(float64(64)/2)))
_, err := rand.Read(buff) _, err := rand.Read(buff)
if err != nil { if err != nil {
log.Println("Error creating random buffer for csrf token value") slog.Error("error creating random buffer for csrf token value" + err.Error())
log.Println(err)
return "", err return "", err
} }
str := hex.EncodeToString(buff) str := hex.EncodeToString(buff)
@ -39,8 +37,7 @@ func GenerateCsrfToken(w http.ResponseWriter, _ *http.Request) (string, error) {
func VerifyCsrfToken(r *http.Request) (bool, error) { func VerifyCsrfToken(r *http.Request) (bool, error) {
cookie, err := r.Cookie("csrf_token") cookie, err := r.Cookie("csrf_token")
if err != nil { if err != nil {
log.Println("Error getting csrf_token cookie") slog.Info("unable to get csrf_token cookie" + err.Error())
log.Println(err)
return false, err return false, err
} }

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SiteName - {{ template "pageTitle" }}</title> <title>SiteName - {{ template "pageTitle" }}</title>
<link rel="stylesheet" href="/static/css/style.css"> <link href="/static/css/style.css" rel="stylesheet">
</head> </head>
<body> <body>
{{ template "content" . }} {{ template "content" . }}

View File

@ -7,9 +7,11 @@
<input name="csrf_token" type="hidden" value="{{ .CsrfToken }}"> <input name="csrf_token" type="hidden" value="{{ .CsrfToken }}">
<label for="username">Username:</label><br> <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> <label for="password">Password:</label><br>
<input id="password" name="password" type="password"><br><br> <input id="password" name="password" type="password"><br><br>
<label for="remember">Remember Me:</label>
<input id="remember" name="remember" type="checkbox"><br><br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">
</form> </form>
</div> </div>

View File

@ -7,7 +7,7 @@
<input name="csrf_token" type="hidden" value="{{ .CsrfToken }}"> <input name="csrf_token" type="hidden" value="{{ .CsrfToken }}">
<label for="username">Username:</label><br> <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> <label for="password">Password:</label><br>
<input id="password" name="password" type="password"><br><br> <input id="password" name="password" type="password"><br><br>
<input type="submit" value="Submit"> <input type="submit" value="Submit">

View File

@ -2,46 +2,83 @@ package templating
import ( import (
"GoWeb/app" "GoWeb/app"
"fmt"
"html/template" "html/template"
"log" "io/fs"
"log/slog"
"net/http" "net/http"
) )
// RenderTemplate renders and serves a template from the embedded filesystem optionally with given data var templates = make(map[string]*template.Template) // This is only used here, does not need to be in app.App
func RenderTemplate(app *app.App, w http.ResponseWriter, contentPath string, data any) {
templatePath := app.Config.Template.BaseName
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 { if err != nil {
log.Println(err) return fmt.Errorf("error reading base file: %w", err)
http.Error(w, err.Error(), 500)
return
} }
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 { if err != nil {
log.Println(err) return fmt.Errorf("error parsing base file: %w", err)
http.Error(w, err.Error(), 500)
return
} }
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) content, err := app.Res.ReadFile(contentPath)
if err != nil { if err != nil {
log.Println(err) return fmt.Errorf("error reading content file %s: %w", contentPath, err)
http.Error(w, err.Error(), 500) }
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, "Template not found", 404)
return return
} }
t, err = t.Parse(string(content)) err := t.Execute(w, data) // Execute prebuilt template with dynamic data
if err != nil { if err != nil {
log.Println(err) err = fmt.Errorf("error executing template: %w", err)
http.Error(w, err.Error(), 500) slog.Error(err.Error())
return
}
err = t.Execute(w, data)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }