30 Commits

Author SHA1 Message Date
max
783ddaf553 Merge branch 'master' into enhanced_routing_and_logout_fixes 2024-02-28 10:06:56 -06:00
max
b8b64968bb Merge remote-tracking branch 'origin/enhanced_routing_and_logout_fixes' into enhanced_routing_and_logout_fixes 2024-02-28 09:52:47 -06: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
max
65be302aa4 Update x/crypto 2024-02-28 09:51:26 -06:00
8ab50cb37c Merge branch 'master' into enhanced_routing_and_logout_fixes 2024-02-18 17:23:46 -06:00
a833823ad6 Fix wording 2024-02-18 17:23:23 -06:00
max
6d6aff50b3 Only show logout (now CSRF protected) if user is authenticated, include relevant authentication logic in GET controllers (this should be moved to middleware) 2024-02-14 13:20:35 -06:00
max
a6be73765a Add GET verb to static handler 2024-02-14 13:16:52 -06:00
max
ddc9e51831 Fix boolean column migration example 2024-02-14 13:16:15 -06:00
max
dc450e26dd Move logout to POST route and controller with CSRF middleware. Add CsrfToken to home for logout 2024-02-12 14:46:26 -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
e497f4d2f0 Ignore fields that are zero value 2024-01-20 16:32:07 -06:00
b30af86e58 Prebuild templates (base.html + content) at startup to avoid a file parse every page load 2023-12-22 21:03:15 -06:00
3ffd548623 Fix ordering for html attributes 2023-12-21 00:14:28 -06:00
cb4f10e0b4 Better alignment for memory 2023-12-19 16:41:31 -06:00
878ce01b29 Get the sha256 hash of password before passing to bcrypt to avoid character limit 2023-12-19 16:06:00 -06:00
c82cdb4f13 Use best naming practices 2023-12-18 23:04:31 -06:00
ce81c36e9f Update x/crypto 2023-12-18 23:01:19 -06:00
ab1b82c680 Update x/crypto 2023-10-10 21:37:54 -05:00
d8b1a5c999 Remove unnecessary comparison 2023-09-26 11:32:39 -05:00
0f59a6eba9 Go mod tidy and update x/crypto 2023-09-17 19:23:57 -05:00
bb69c16cdf Update How to use section 2023-09-03 16:18:48 -05:00
e87f14bcbf Make all slog errors uniform (lowercase) 2023-09-03 15:56:35 -05:00
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
ee4c9f9199 Clean up error handling, begin migration to log/slog 2023-08-17 21:13:17 -05:00
1d400efa19 Update x/crypto and Go version 2023-08-12 14:30:12 -05:00
b36cbf4b9e Comment cleanup 2023-08-12 14:28:21 -05:00
22 changed files with 266 additions and 175 deletions

View File

@ -19,7 +19,7 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
- Config file handling
- Scheduled tasks
- 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>
@ -39,10 +39,13 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
## How to use 🤔
1. Clone
2. Run `go get` to install dependencies
3. Copy env_example.json to env.json and fill in the values
4. Run `go run main.go` to start the server
5. Start building your app!
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.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!
8. When you see useful changes to GoWeb you'd like in your project copy them over
## How to contribute 👨‍💻
@ -56,7 +59,7 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
### License and disclaimer 😤
- 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.
- 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.

View File

@ -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

View File

@ -3,7 +3,7 @@ package config
import (
"encoding/json"
"flag"
"log"
"log/slog"
"os"
)
@ -24,6 +24,7 @@ type Configuration struct {
Template struct {
BaseName string `json:"BaseTemplateName"`
ContentPath string `json:"ContentPath"`
}
}
@ -33,13 +34,13 @@ func LoadConfig() Configuration {
flag.Parse()
file, err := os.Open(*c)
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) {
err := file.Close()
if err != nil {
log.Fatal("Unable to close JSON config file: ", err)
slog.Error("unable to close JSON config file: ", err)
}
}(file)
@ -47,7 +48,7 @@ func LoadConfig() Configuration {
Config := Configuration{}
err = decoder.Decode(&Config)
if err != nil {
log.Fatal("Unable to decode JSON config file: ", err)
panic("unable to decode JSON config file: " + err.Error())
}
return Config

View File

@ -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)
}

View File

@ -3,7 +3,7 @@ package controllers
import (
"GoWeb/app"
"GoWeb/models"
"log"
"log/slog"
"net/http"
"time"
)
@ -19,15 +19,12 @@ func (p *Post) Login(w http.ResponseWriter, r *http.Request) {
remember := r.FormValue("remember") == "on"
if username == "" || password == "" {
log.Println("Tried to login user with empty username or password")
http.Redirect(w, r, "/login", http.StatusFound)
http.Redirect(w, r, "/login", http.StatusUnauthorized)
}
_, err := models.AuthenticateUser(p.App, w, username, password, remember)
if err != nil {
log.Println("Error authenticating user")
log.Println(err)
http.Redirect(w, r, "/login", http.StatusFound)
http.Redirect(w, r, "/login", http.StatusUnauthorized)
return
}
@ -41,16 +38,20 @@ func (p *Post) Register(w http.ResponseWriter, r *http.Request) {
updatedAt := time.Now()
if username == "" || password == "" {
log.Println("Tried to create user with empty username or password")
http.Redirect(w, r, "/register", http.StatusFound)
http.Redirect(w, r, "/register", http.StatusUnauthorized)
}
_, err := models.CreateUser(p.App, username, password, createdAt, updatedAt)
if err != nil {
log.Println("Error creating user")
log.Println(err)
return
// 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)
}
func (p *Post) Logout(w http.ResponseWriter, r *http.Request) {
models.LogoutUser(p.App, w, r)
http.Redirect(w, r, "/", http.StatusFound)
}

View File

@ -5,29 +5,26 @@ import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
"log"
"log/slog"
)
// Connect returns a new database connection
func Connect(app *app.App) *sql.DB {
// Set connection parameters from config
postgresConfig := fmt.Sprintf("host=%s port=%s user=%s "+
"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)
// Create connection
db, err := sql.Open("postgres", postgresConfig)
if err != nil {
panic(err)
}
// Test connection
err = db.Ping()
if err != nil {
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
}

View File

@ -5,11 +5,12 @@ import (
"errors"
"fmt"
"github.com/lib/pq"
"log"
"log/slog"
"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
}
@ -39,23 +45,23 @@ func createTable(app *app.App, tableName string) error {
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)
if err != nil {
log.Println("Error checking if table exists: " + tableName)
slog.Error("error checking if table exists: " + tableName)
return err
}
if tableExists {
log.Println("Table already exists: " + tableName)
slog.Info("table already exists: " + tableName)
return nil
} else {
sanitizedTableQuery := fmt.Sprintf("CREATE TABLE IF NOT EXISTS \"%s\" (\"Id\" serial primary key)", tableName)
_, err := app.Db.Query(sanitizedTableQuery)
if err != nil {
log.Println("Error creating table: " + tableName)
slog.Error("error creating table: " + tableName)
return err
}
log.Println("Table created successfully: " + tableName)
slog.Info("table created successfully: " + tableName)
return nil
}
}
@ -65,17 +71,17 @@ func createColumn(app *app.App, tableName, columnName, columnType string) error
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)
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
}
if columnExists {
log.Println("Column already exists: " + columnName + " in table: " + tableName)
slog.Info("column already exists: " + columnName + " in table: " + tableName)
return nil
} else {
postgresType, err := getPostgresType(columnType)
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
}
@ -84,11 +90,11 @@ func createColumn(app *app.App, tableName, columnName, columnType string) error
_, err = app.Db.Query(query)
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
}
log.Println("Column created successfully:", columnName)
slog.Info("column created successfully:", columnName)
return nil
}

View File

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

4
go.mod
View File

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

4
go.sum
View File

@ -1,4 +1,4 @@
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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=

37
main.go
View File

@ -6,10 +6,11 @@ import (
"GoWeb/database"
"GoWeb/models"
"GoWeb/routes"
"GoWeb/templating"
"context"
"embed"
"errors"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
@ -34,23 +35,26 @@ func main() {
if _, err := os.Stat("logs"); os.IsNotExist(err) {
err := os.Mkdir("logs", 0755)
if err != nil {
log.Println("Failed to create log directory")
log.Println(err)
return
panic("failed to create log directory: " + err.Error())
}
}
// 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)
log.SetOutput(file)
file, err := os.OpenFile("logs/"+time.Now().Format("2006-01-02")+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
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
appLoaded.Db = database.Connect(&appLoaded)
if appLoaded.Config.Db.AutoMigrate {
err = models.RunAllMigrations(&appLoaded)
if err != nil {
log.Println(err)
return
slog.Error("error running migrations: " + err.Error())
os.Exit(1)
}
}
@ -64,13 +68,21 @@ 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() {
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()
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)
}
}()
@ -81,10 +93,11 @@ func main() {
go app.RunScheduledTasks(&appLoaded, 100, stop)
<-interrupt
log.Println("Interrupt signal received. Shutting down server...")
slog.Info("interrupt signal received. Shutting down server...")
err = server.Shutdown(context.Background())
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)
}
}

View File

@ -2,7 +2,7 @@ package middleware
import (
"GoWeb/security"
"log"
"log/slog"
"net/http"
)
@ -11,7 +11,7 @@ func Csrf(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWr
return func(w http.ResponseWriter, r *http.Request) {
_, err := security.VerifyCsrfToken(r)
if err != nil {
log.Println("Error verifying csrf token")
slog.Info("error verifying csrf token")
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

View File

@ -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)

View File

@ -4,7 +4,7 @@ import (
"GoWeb/app"
"crypto/rand"
"encoding/hex"
"log"
"log/slog"
"net/http"
"time"
)
@ -42,21 +42,19 @@ func CreateSession(app *app.App, w http.ResponseWriter, userId int64, remember b
var existingAuthToken bool
err := app.Db.QueryRow(selectAuthTokenIfExists, session.AuthToken).Scan(&existingAuthToken)
if err != nil {
log.Println("Error checking for existing auth token")
log.Println(err)
slog.Error("error checking for existing auth token" + err.Error())
return Session{}, err
}
// If duplicate token found, recursively call function until unique token is generated
if existingAuthToken == true {
log.Println("Duplicate token found in sessions table, generating new token...")
if existingAuthToken {
slog.Warn("duplicate token found in sessions table, generating new token...")
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)
if err != nil {
log.Println("Error inserting session into database")
slog.Error("error inserting session into database")
return Session{}, err
}
@ -64,24 +62,23 @@ 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)
if err != nil {
log.Println("Error getting session by auth token")
return Session{}, err
}
return session, nil
}
// Generates a random 64-byte string
// generateAuthToken generates a random 64-byte string
func generateAuthToken(app *app.App) string {
b := make([]byte, 64)
_, err := rand.Read(b)
if err != nil {
log.Println("Error generating random bytes")
slog.Error("error generating random bytes for auth token")
}
return hex.EncodeToString(b)
@ -129,7 +126,7 @@ func deleteSessionCookie(app *app.App, w http.ResponseWriter) {
func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken string) error {
_, err := app.Db.Exec(deleteSessionByAuthToken, authToken)
if err != nil {
log.Println("Error deleting session from database")
slog.Error("error deleting session from database")
return err
}
@ -143,16 +140,14 @@ func ScheduledSessionCleanup(app *app.App) {
// Delete sessions older than 30 days (remember me sessions)
_, err := app.Db.Exec(deleteSessionsOlderThan30Days)
if err != nil {
log.Println("Error deleting 30 day expired sessions from database")
log.Println(err)
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 {
log.Println("Error deleting 6 hour expired sessions from database")
log.Println(err)
slog.Error("error deleting 6 hour expired sessions from database" + err.Error())
}
log.Println("Deleted expired sessions from database")
slog.Info("deleted expired sessions from database")
}

View File

@ -2,9 +2,10 @@ package models
import (
"GoWeb/app"
"log"
"crypto/sha256"
"encoding/hex"
"log/slog"
"net/http"
"strconv"
"time"
"golang.org/x/crypto/bcrypt"
@ -28,43 +29,39 @@ 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 {
log.Println("Error getting session cookie")
return User{}, err
}
session, err := GetSessionByAuthToken(app, cookie.Value)
session, err := SessionByAuthToken(app, cookie.Value)
if err != nil {
log.Println("Error getting session by auth token")
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)
if err != nil {
log.Println("Get user error (user not found) for user id:" + strconv.FormatInt(id, 10))
return User{}, err
}
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)
if err != nil {
log.Println("Get user error (user not found) for user:" + username)
return User{}, err
}
@ -73,9 +70,14 @@ 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 {
log.Println("Error hashing password when creating user")
slog.Error("error hashing password: " + err.Error())
return User{}, err
}
@ -83,11 +85,11 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T
err = app.Db.QueryRow(insertUser, username, string(hash), createdAt, updatedAt).Scan(&lastInsertId)
if err != nil {
log.Println("Error creating user row")
slog.Error("error creating user row: " + err.Error())
return User{}, err
}
return GetUserById(app, lastInsertId)
return UserById(app, lastInsertId)
}
// AuthenticateUser validates the password for the specified user
@ -96,13 +98,18 @@ func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, pass
err := app.Db.QueryRow(selectUserByUsername, username).Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
log.Println("Authentication error (user not found) for user:" + username)
slog.Info("user not found: " + username)
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
log.Println("Authentication error (incorrect password) for user:" + username)
slog.Info("incorrect password:" + username)
return Session{}, err
} else {
return CreateSession(app, w, user.Id, remember)
@ -113,13 +120,11 @@ func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, pass
func LogoutUser(app *app.App, w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
log.Println("Error getting cookie from request")
return
}
err = DeleteSessionByAuthToken(app, w, cookie.Value)
if err != nil {
log.Println("Error deleting session by AuthToken")
return
}
}

View File

@ -4,7 +4,7 @@ import (
"GoWeb/app"
"GoWeb/controllers"
"io/fs"
"log"
"log/slog"
"net/http"
)
@ -18,16 +18,15 @@ func Get(app *app.App) {
// Serve static files
staticFS, err := fs.Sub(app.Res, "static")
if err != nil {
log.Println(err)
slog.Error(err.Error())
return
}
staticHandler := http.FileServer(http.FS(staticFS))
http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
log.Println("Serving static files from embedded file system /static")
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)
}

View File

@ -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))
}

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -2,46 +2,83 @@ package templating
import (
"GoWeb/app"
"fmt"
"html/template"
"log"
"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 {
log.Println(err)
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 {
log.Println(err)
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 {
log.Println(err)
http.Error(w, err.Error(), 500)
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, "Template not found", 404)
return
}
t, err = t.Parse(string(content))
err := t.Execute(w, data) // Execute prebuilt template with dynamic data
if err != nil {
log.Println(err)
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
log.Println(err)
err = fmt.Errorf("error executing template: %w", err)
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}