25 Commits

Author SHA1 Message Date
cf8aea5115 Update README.md 2023-03-06 21:34:12 -06:00
c510646c84 Make username text placeholder instead of value 2023-03-06 21:27:05 -06:00
a4366c7395 Add more to .gitignore 2023-03-06 21:23:56 -06:00
073dfafb28 Change log message 2023-03-06 21:10:09 -06:00
3fa5cf46d2 Update experimental crypto library 2023-03-06 21:08:56 -06:00
bd8b015f44 Update README.md 2023-03-06 21:02:41 -06:00
5a1cd77676 Update README.md 2023-03-06 13:10:50 -06:00
012906eee2 Update README.md 2023-03-06 13:00:11 -06:00
2a705483d9 Add README.md 2023-03-06 12:58:58 -06:00
be2c3ae178 Add default theme and apply to pages 2023-03-06 12:44:20 -06:00
f32223f12c Fix static file handling for the embedded filesystem 2023-03-06 12:43:54 -06:00
eff740072d Decouple SQL queries from logic 2023-03-05 15:46:43 -06:00
75d8996cf9 Fix some queries, comments, and error logging 2023-02-28 15:02:21 -06:00
ac2b5262fd Remove print 2023-02-28 14:57:15 -06:00
b9ac6fbd5f Add session migration 2023-02-28 14:55:09 -06:00
baa8eb2b93 Move to a session based system for AuthTokens 2023-02-28 14:54:55 -06:00
402c514970 Add checks to skip table and column creation if they already exist 2023-02-17 19:01:59 -06:00
89d1b96400 Change CreatedAt and UpdatedAt to type Time and update migrations.go accordingly 2023-02-17 18:55:27 -06:00
2b46385126 Fix time.Time matching to timestamp postgres type (reflection just gives "Time") 2023-02-17 18:52:15 -06:00
0a77813360 Fix postgres type matching 2023-02-17 18:47:29 -06:00
f7eb852c66 Gracefully shut down server when interrupt signal is received and remove panic when creating log directory 2023-02-17 18:25:14 -06:00
5ae84c1995 Remove unneeded comments 2023-02-15 19:13:05 -06:00
3336bd0b3f Remove default condition 2023-02-15 19:10:50 -06:00
max
f2a7336283 Fix user queries and a logical error in GetCurrentUser 2023-02-14 09:43:02 -06:00
max
204971d40a AutoMigrate changed to DbAutoMigrate to match correctly 2023-02-14 08:31:13 -06:00
16 changed files with 450 additions and 197 deletions

22
.gitignore vendored
View File

@ -1,4 +1,26 @@
# GoWeb specific
env.json env.json
logs/ logs/
*.log *.log
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories
vendor/
# Go workspace file
go.work
# IDE files
/.idea /.idea

59
README.md Normal file
View File

@ -0,0 +1,59 @@
# GoWeb 🌐
GoWeb is a simple Go web framework that aims to only use the standard library. The overall file structure and
development flow is inspired by larger frameworks like Laravel. It is partially ready for smaller projects if you are
fine with getting your hands dirty, but I plan on having it ready to go for more serious projects when it hits version
2.0.
<hr>
## Current features 🚀
- Routing/controllers
- Templating
- Simple database migration system
- CSRF protection
- Minimal user login/registration + sessions
- Config file handling
- Entire website compiles into a single binary (~10mb) (excluding env.json)
- Minimal dependencies (just standard library, postgres driver, and experimental package for bcrypt)
<hr>
## When to use 🙂
- You need to build a dynamic web application with persistent data
- You need to build a dynamic website using Go and need a good starting point
- You need to build an API in Go and don't know where to start
- Pretty much any use-case where you would use Laravel, Django, or Flask
## When not to use 🙃
- You need a static website (see [Hugo](https://gohugo.io/))
- You need a simple blog (see [Hugo](https://gohugo.io/))
- You need a simple site for your projects' documentation (see [Hugo](https://gohugo.io/))
## 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!
## How to contribute 👨‍💻
- Open an issue on GitHub if you find a bug or have a feature request.
- [Email](mailto:contact@mpatterson.xyz) me a patch if you want to contribute code.
- Please include a good description of what the patch does and why it is needed, also include how you want to be
credited in the commit message.
<hr>
### 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.
- 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

@ -36,6 +36,18 @@ func Migrate(app *app.App, anyStruct interface{}) error {
// 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
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)
return err
}
if tableExists {
log.Println("Table already exists: " + tableName)
return nil
} 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)
@ -44,12 +56,25 @@ func createTable(app *app.App, tableName string) error {
return err return err
} }
log.Println("Table created successfully (or already exists): " + tableName) log.Println("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
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)
return err
}
if columnExists {
log.Println("Column already exists: " + columnName + " in table: " + tableName)
return nil
} 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) log.Println("Error creating column: " + columnName + " in table: " + tableName + " with type: " + postgresType)
@ -65,27 +90,20 @@ func createColumn(app *app.App, tableName, columnName, columnType string) error
return err return err
} }
log.Println("Column created successfully (or already exists):", columnName) log.Println("Column created successfully:", columnName)
return nil return nil
} }
}
// Given a type in Go, return the corresponding type in Postgres // Given a type in Go, return the corresponding type in Postgres
func getPostgresType(goType string) (string, error) { func getPostgresType(goType string) (string, error) {
switch goType { switch goType {
case "int": case "int", "int32", "uint", "uint32":
case "int32":
case "uint":
case "uint32":
return "integer", nil return "integer", nil
case "int64": case "int64", "uint64":
case "uint64":
return "bigint", nil return "bigint", nil
case "int16": case "int16", "int8", "uint16", "uint8", "byte":
case "int8":
case "uint16":
case "uint8":
case "byte":
return "smallint", nil return "smallint", nil
case "string": case "string":
return "text", nil return "text", nil
@ -93,12 +111,10 @@ func getPostgresType(goType string) (string, error) {
return "double precision", nil return "double precision", nil
case "bool": case "bool":
return "boolean", nil return "boolean", nil
case "time.Time": case "Time":
return "timestamp", nil return "timestamp", nil
case "[]byte": case "[]byte":
return "bytea", nil return "bytea", nil
default:
return "text", nil
} }
return "", errors.New("Unknown type: " + goType) return "", errors.New("Unknown type: " + goType)

View File

@ -5,7 +5,7 @@
"DbName": "database", "DbName": "database",
"DbUser": "user", "DbUser": "user",
"DbPassword": "password", "DbPassword": "password",
"AutoMigrate": true "DbAutoMigrate": true
}, },
"Listen": { "Listen": {
"HttpIp": "127.0.0.1", "HttpIp": "127.0.0.1",

2
go.mod
View File

@ -4,5 +4,5 @@ go 1.20
require ( require (
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.7
golang.org/x/crypto v0.6.0 golang.org/x/crypto v0.7.0
) )

4
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.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=

26
main.go
View File

@ -6,10 +6,13 @@ import (
"GoWeb/database" "GoWeb/database"
"GoWeb/models" "GoWeb/models"
"GoWeb/routes" "GoWeb/routes"
"context"
"embed" "embed"
"log" "log"
"net/http" "net/http"
"os" "os"
"os/signal"
"syscall"
"time" "time"
) )
@ -30,7 +33,9 @@ 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 {
panic("Failed to create log directory") log.Println("Failed to create log directory")
log.Println(err)
return
} }
} }
@ -53,10 +58,23 @@ func main() {
routes.PostRoutes(&appLoaded) routes.PostRoutes(&appLoaded)
// Start server // 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) log.Println("Starting server and listening on " + appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port)
err = http.ListenAndServe(appLoaded.Config.Listen.Ip+":"+appLoaded.Config.Listen.Port, nil) err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not listen on %s: %v\n", appLoaded.Config.Listen.Ip+":"+appLoaded.Config.Listen.Port, err)
}
}()
// Wait for interrupt signal and shut down the server
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
<-interrupt
log.Println("Interrupt signal received. Shutting down server...")
err = server.Shutdown(context.Background())
if err != nil { if err != nil {
log.Println(err) log.Fatalf("Could not gracefully shutdown the server: %v\n", err)
return
} }
} }

View File

@ -3,6 +3,7 @@ package models
import ( import (
"GoWeb/app" "GoWeb/app"
"GoWeb/database" "GoWeb/database"
"time"
) )
// RunAllMigrations defines the structs that should be represented in the database // RunAllMigrations defines the structs that should be represented in the database
@ -12,10 +13,24 @@ func RunAllMigrations(app *app.App) error {
Id: 1, // Id is handled automatically, but it is added here to show it will be skipped during column creation Id: 1, // Id is handled automatically, but it is added here to show it will be skipped during column creation
Username: "migrate", Username: "migrate",
Password: "migrate", Password: "migrate",
AuthToken: "migrate", CreatedAt: time.Now(),
CreatedAt: "2021-01-01 00:00:00", UpdatedAt: time.Now(),
UpdatedAt: "2021-01-01 00:00:00", }
err := database.Migrate(app, user)
if err != nil {
return err
} }
return database.Migrate(app, user) session := Session{
Id: 1,
UserId: 1,
AuthToken: "migrate",
CreatedAt: time.Now(),
}
err = database.Migrate(app, session)
if err != nil {
return err
}
return nil
} }

114
models/session.go Normal file
View File

@ -0,0 +1,114 @@
package models
import (
"GoWeb/app"
"crypto/rand"
"encoding/hex"
"log"
"net/http"
"time"
)
type Session struct {
Id int64
UserId int64
AuthToken string
CreatedAt time.Time
}
const sessionColumnsNoId = "\"UserId\", \"AuthToken\", \"CreatedAt\""
const sessionColumns = "\"Id\", " + sessionColumnsNoId
const sessionTable = "public.\"Session\""
const (
selectSessionByAuthToken = "SELECT " + sessionColumns + " 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\""
deleteSessionByAuthToken = "DELETE FROM " + sessionTable + " WHERE \"AuthToken\" = $1"
)
// CreateSession creates a new session for a user
func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session, error) {
session := Session{}
session.UserId = userId
session.AuthToken = generateAuthToken(app)
session.CreatedAt = time.Now()
// If the AuthToken column for any user matches the token, set existingAuthToken to true
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)
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...")
return CreateSession(app, w, userId)
}
// Insert session into database
err = app.Db.QueryRow(insertSession, session.UserId, session.AuthToken, session.CreatedAt).Scan(&session.Id)
if err != nil {
log.Println("Error inserting session into database")
return Session{}, err
}
createSessionCookie(app, w, session)
return session, nil
}
// Generates a random 64-byte string
func generateAuthToken(app *app.App) string {
// Generate random bytes
b := make([]byte, 64)
_, err := rand.Read(b)
if err != nil {
log.Println("Error generating random bytes")
}
// Convert random bytes to hex string
return hex.EncodeToString(b)
}
// createSessionCookie creates a new session cookie
func createSessionCookie(app *app.App, w http.ResponseWriter, session Session) {
cookie := &http.Cookie{
Name: "session",
Value: session.AuthToken,
Path: "/",
MaxAge: 86400,
HttpOnly: true,
Secure: true,
}
http.SetCookie(w, cookie)
}
// deleteSessionCookie deletes the session cookie
func deleteSessionCookie(app *app.App, w http.ResponseWriter) {
cookie := &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
}
// DeleteSessionByAuthToken deletes a session from the database by AuthToken
func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken string) error {
// Delete session from database
_, err := app.Db.Exec(deleteSessionByAuthToken, authToken)
if err != nil {
log.Println("Error deleting session from database")
return err
}
deleteSessionCookie(app, w)
return nil
}

View File

@ -2,11 +2,7 @@ package models
import ( import (
"GoWeb/app" "GoWeb/app"
"crypto/rand"
"database/sql"
"encoding/hex"
"log" "log"
"math"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -18,24 +14,33 @@ type User struct {
Id int64 Id int64
Username string Username string
Password string Password string
AuthToken string CreatedAt time.Time
CreatedAt string UpdatedAt time.Time
UpdatedAt string
} }
const userColumnsNoId = "\"Username\", \"Password\", \"CreatedAt\", \"UpdatedAt\""
const userColumns = "\"Id\", " + userColumnsNoId
const userTable = "public.\"User\""
const (
selectSessionIdByAuthToken = "SELECT \"Id\" FROM public.\"Session\" WHERE \"AuthToken\" = $1"
selectUserById = "SELECT " + userColumns + " FROM " + userTable + " WHERE \"Id\" = $1"
selectUserByUsername = "SELECT " + userColumns + " FROM " + userTable + " WHERE \"Username\" = $1"
insertUser = "INSERT INTO " + userTable + " (" + userColumnsNoId + ") VALUES ($1, $2, $3, $4) RETURNING \"Id\""
)
// GetCurrentUser finds the currently logged-in user by session cookie // GetCurrentUser finds the currently logged-in user by session cookie
func GetCurrentUser(app *app.App, r *http.Request) (User, error) { func GetCurrentUser(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") log.Println("Error getting session cookie")
log.Println(err)
return User{}, err return User{}, err
} }
var userId int64 var userId int64
// Query row by session cookie // Query row by AuthToken
err = app.Db.QueryRow("SELECT Id FROM User WHERE session = $1", cookie.Value).Scan(&userId) 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) log.Println("Error querying session row with session: " + cookie.Value)
return User{}, err return User{}, err
@ -44,38 +49,35 @@ func GetCurrentUser(app *app.App, r *http.Request) (User, error) {
return GetUserById(app, userId) return GetUserById(app, userId)
} }
// GetUserById finds a users table row in the database by id and returns a struct representing this row // 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) { func GetUserById(app *app.App, id int64) (User, error) {
user := User{} user := User{}
// Query row by id // Query row by id
row, err := app.Db.Query("SELECT Id, Username, Password, AuthToken, CreatedAt, UpdatedAt FROM User WHERE Id = $1", id) 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("Error querying user row with id: " + strconv.FormatInt(id, 10)) log.Println("Get user error (user not found) for user id:" + strconv.FormatInt(id, 10))
return User{}, err
}
defer func(row *sql.Rows) {
err := row.Close()
if err != nil {
log.Println("Error closing database row")
log.Println(err)
}
}(row)
// Feed row data into user struct
row.Next()
err = row.Scan(&user.Id, &user.Username, &user.Password, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
log.Println("Error reading queried row from database")
log.Println(err)
return User{}, err return User{}, err
} }
return user, nil return user, nil
} }
// CreateUser creates a users table row in the database // 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) {
user := User{}
// Query row by username
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
}
return user, nil
}
// 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 // Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
@ -86,129 +88,49 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T
var lastInsertId int64 var lastInsertId int64
sqlStatement := "INSERT INTO User (Username, Password, CreatedAt, UpdatedAt) VALUES ($1, $2, $3, $4) RETURNING Id" err = app.Db.QueryRow(insertUser, username, string(hash), createdAt, updatedAt).Scan(&lastInsertId)
err = app.Db.QueryRow(sqlStatement, username, string(hash), createdAt, updatedAt).Scan(&lastInsertId)
if err != nil { if err != nil {
log.Println("Error creating user row") log.Println("Error creating user row")
log.Println(err)
return User{}, err return User{}, err
} }
return GetUserById(app, lastInsertId) return GetUserById(app, lastInsertId)
} }
// AuthenticateUser validates the password for the specified user if it matches a session cookie is created and returned // AuthenticateUser validates the password for the specified user
func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string) (string, error) { func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string) (Session, error) {
var hashedPassword []byte var user User
// Query row by username, scan password column // Query row by username
err := app.Db.QueryRow("SELECT Password FROM User WHERE Username = $1", username).Scan(&hashedPassword) 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("Unable to find row with username: " + username) log.Println("Authentication error (user not found) for user:" + username)
log.Println(err) return Session{}, err
return "", err
} }
// Validate password // Validate password
err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)) err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
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) log.Println("Authentication error (incorrect password) for user:" + username)
log.Println(err) return Session{}, err
return "", err
} else { } else {
return createSessionCookie(app, w, username) return CreateSession(app, w, user.Id)
} }
} }
// createSessionCookie creates a new session token and cookie and returns the token value // LogoutUser deletes the session cookie and AuthToken from the database
func createSessionCookie(app *app.App, w http.ResponseWriter, username string) (string, error) {
// Generate random 64 character string (alpha-numeric)
buff := make([]byte, int(math.Ceil(float64(64)/2)))
_, err := rand.Read(buff)
if err != nil {
log.Println("Error creating random buffer for session token value")
log.Println(err)
return "", err
}
str := hex.EncodeToString(buff)
token := str[:64]
// If the auth_token column for any user matches the token, set existingAuthToken to true
var existingAuthToken bool
err = app.Db.QueryRow("SELECT EXISTS(SELECT 1 FROM User WHERE AuthToken = $1)", token).Scan(&existingAuthToken)
if err != nil {
log.Println("Error checking for existing auth token")
log.Println(err)
return "", err
}
// If duplicate token found, recursively call function until unique token is generated
if existingAuthToken == true {
log.Println("Duplicate token found in sessions table")
return createSessionCookie(app, w, username)
}
// Store token in auth_token column of the users table
_, err = app.Db.Exec("UPDATE User SET AuthToken = $1 WHERE Username = $2", token, username)
if err != nil {
log.Println("Error setting auth_token column in users table")
log.Println(err)
return "", err
}
// Create session cookie, containing token
cookie := &http.Cookie{
Name: "session",
Value: token,
Path: "/",
MaxAge: 86400,
HttpOnly: true,
Secure: true,
}
http.SetCookie(w, cookie)
return token, nil
}
// ValidateSessionCookie validates the session cookie and returns the username of the user if valid
func ValidateSessionCookie(app *app.App, r *http.Request) (string, error) {
// Get cookie from request
cookie, err := r.Cookie("session")
if err != nil {
log.Println("Error getting cookie from request")
log.Println(err)
return "", err
}
// Query row by token
var username string
err = app.Db.QueryRow("SELECT Username FROM User WHERE AuthToken = $1", cookie.Value).Scan(&username)
if err != nil {
log.Println("Error querying row by token")
log.Println(err)
return "", err
}
return username, nil
}
// LogoutUser deletes the session cookie and token 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 // 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") log.Println("Error getting cookie from request")
log.Println(err)
return return
} }
// Set token to empty string // Set token to empty string
sqlStatement := "UPDATE User SET AuthToken = $1 WHERE AuthToken = $2" err = DeleteSessionByAuthToken(app, w, cookie.Value)
_, err = app.Db.Exec(sqlStatement, "", cookie.Value)
if err != nil { if err != nil {
log.Println("Error setting auth_token column in users table") log.Println("Error deleting session by AuthToken")
log.Println(err)
return return
} }

View File

@ -3,6 +3,7 @@ package routes
import ( import (
"GoWeb/app" "GoWeb/app"
"GoWeb/controllers" "GoWeb/controllers"
"io/fs"
"log" "log"
"net/http" "net/http"
) )
@ -15,8 +16,14 @@ func GetRoutes(app *app.App) {
} }
// Serve static files // Serve static files
http.Handle("/file/", http.FileServer(http.Dir("./static"))) staticFS, err := fs.Sub(app.Res, "static")
log.Println("Serving static files from: ./static") if err != nil {
log.Println(err)
return
}
staticHandler := http.FileServer(http.FS(staticFS))
http.Handle("/static/", http.StripPrefix("/static/", staticHandler))
log.Println("Serving static files from embedded file system /static")
// Pages // Pages
http.HandleFunc("/", getController.ShowHome) http.HandleFunc("/", getController.ShowHome)

View File

@ -21,7 +21,6 @@ func GenerateCsrfToken(w http.ResponseWriter, _ *http.Request) (string, error) {
str := hex.EncodeToString(buff) str := hex.EncodeToString(buff)
token := str[:64] token := str[:64]
// Create session cookie, containing token
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: "csrf_token", Name: "csrf_token",
Value: token, Value: token,
@ -38,7 +37,6 @@ func GenerateCsrfToken(w http.ResponseWriter, _ *http.Request) (string, error) {
// VerifyCsrfToken verifies the csrf token // VerifyCsrfToken verifies the csrf token
func VerifyCsrfToken(r *http.Request) (bool, error) { func VerifyCsrfToken(r *http.Request) (bool, error) {
// Get csrf cookie
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") log.Println("Error getting csrf_token cookie")
@ -46,10 +44,8 @@ func VerifyCsrfToken(r *http.Request) (bool, error) {
return false, err return false, err
} }
// Get csrf token from form
token := r.FormValue("csrf_token") token := r.FormValue("csrf_token")
// Compare csrf cookie and csrf token
if cookie.Value == token { if cookie.Value == token {
return true, nil return true, nil
} }

75
static/css/style.css Normal file
View File

@ -0,0 +1,75 @@
body {
font-family: Arial, sans-serif;
background-color: lightblue;
color: #333;
margin: 0;
}
.container {
display: flex;
justify-content: center;
align-items: center;
width: 80%;
padding: 20px;
margin: 0 auto;
}
.footer-container {
display: flex;
justify-content: center;
align-items: center;
height: 80px;
background-color: lightblue;
}
footer {
color: #0077be;
font-size: 14px;
}
form label {
display: block;
font-weight: bold;
margin-bottom: 5px;
}
form input[type="text"],
form input[type="password"] {
padding: 10px;
font-size: 16px;
border-radius: 5px;
border: none;
margin-bottom: 10px;
width: 100%;
box-sizing: border-box;
}
form input[type="submit"] {
display: inline-block;
padding: 10px 20px;
background-color: #0077be;
color: #fff;
border-radius: 5px;
text-decoration: none;
border: none;
cursor: pointer;
}
form input[type="submit"]:hover {
background-color: #005fa3;
}
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
color: #333;
text-align: center;
}
a {
color: #0077be;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}

View File

@ -3,11 +3,14 @@
<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">
</head> </head>
<body> <body>
{{ template "content" . }} {{ template "content" . }}
</body> <div class="footer-container">
<footer> <footer>
<p>SiteName - Powered by Go!</p> <p>SiteName - Powered by GoWeb!</p>
</footer> </footer>
</div>
</body>
</html> </html>

View File

@ -1,13 +1,16 @@
{{ define "pageTitle" }}Login{{ end }} {{ define "pageTitle" }}Login{{ end }}
{{ define "content" }} {{ define "content" }}
<h1>Login</h1>
<div class="container">
<form action="/login-handle" method="post"> <form action="/login-handle" method="post">
<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" value="John"><br><br> <input id="username" name="username" type="text" placeholder="John"><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">
</form> </form>
</div>
{{ end }} {{ end }}

View File

@ -1,13 +1,16 @@
{{ define "pageTitle" }}Register{{ end }} {{ define "pageTitle" }}Register{{ end }}
{{ define "content" }} {{ define "content" }}
<h1>Register</h1>
<div class="container">
<form action="/register-handle" method="post"> <form action="/register-handle" method="post">
<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" value="John"><br><br> <input id="username" name="username" type="text" placeholder="John"><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">
</form> </form>
</div>
{{ end }} {{ end }}