18 Commits

Author SHA1 Message Date
ce03926ce6 Rename app->internal 2024-07-10 17:45:04 -05:00
ce85d6b77b Begin refactoring structure 2024-07-01 21:19:48 -05:00
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
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
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
30 changed files with 305 additions and 235 deletions

View File

@ -18,8 +18,8 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
- Minimal user login/registration + sessions - Minimal user login/registration + sessions
- Config file handling - Config file handling
- Scheduled tasks - Scheduled tasks
- Entire website compiles into a single binary (~10mb) (excluding env.toml) - Entire website compiles into a single binary (~10mb) (excluding env.json)
- Minimal dependencies (just standard library, postgres driver, and experimental package for bcrypt) - Minimal dependencies (just standard library, postgres driver, and x/crypto for bcrypt)
<hr> <hr>
@ -41,7 +41,7 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
1. Clone 1. Clone
2. Delete the git folder, so you can start tracking in your own repo 2. Delete the git folder, so you can start tracking in your own repo
3. Run `go get` to install dependencies 3. Run `go get` to install dependencies
4. Copy env_example.toml to env.toml and fill in the values 4. Copy env_example.json to env.json and fill in the values
5. Run `go run main.go` to start the server 5. Run `go run main.go` to start the server
6. Rename the occurences of "GoWeb" to your app name 6. Rename the occurences of "GoWeb" to your app name
7. Start building your app! 7. Start building your app!
@ -59,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

@ -1,71 +0,0 @@
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 {
Interval time.Duration
Funcs []func(app *App)
}
func RunScheduledTasks(app *App, poolSize int, stop <-chan struct{}) {
for _, f := range app.ScheduledTasks.EveryReboot {
f(app)
}
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},
}
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

@ -1,44 +1,54 @@
package config package config
import ( import (
"encoding/json"
"flag" "flag"
"github.com/BurntSushi/toml" "log/slog"
"os" "os"
) )
type Configuration struct { type Configuration struct {
Db struct { Db struct {
Ip string `toml:"DbIp"` Ip string `json:"DbIp"`
Port string `toml:"DbPort"` Port string `json:"DbPort"`
Name string `toml:"DbName"` Name string `json:"DbName"`
User string `toml:"DbUser"` User string `json:"DbUser"`
Password string `toml:"DbPassword"` Password string `json:"DbPassword"`
AutoMigrate bool `toml:"DbAutoMigrate"` AutoMigrate bool `json:"DbAutoMigrate"`
} }
Listen struct { Listen struct {
Ip string `toml:"HttpIp"` Ip string `json:"HttpIp"`
Port string `toml:"HttpPort"` Port string `json:"HttpPort"`
} }
Template struct { Template struct {
BaseName string `toml:"BaseTemplateName"` BaseName string `json:"BaseTemplateName"`
ContentPath string `json:"ContentPath"`
} }
} }
// LoadConfig loads and returns a configuration struct // LoadConfig loads and returns a configuration struct
func LoadConfig() Configuration { func LoadConfig() Configuration {
c := flag.String("c", "env.toml", "Path to the toml configuration file") c := flag.String("c", "env.json", "Path to the json configuration file")
flag.Parse() flag.Parse()
file, err := os.ReadFile(*c) file, err := os.Open(*c)
if err != nil { if err != nil {
panic("Unable to read TOML config file: " + err.Error()) panic("unable to open JSON config file: " + err.Error())
} }
var Config Configuration defer func(file *os.File) {
_, err = toml.Decode(string(file), &Config) err := file.Close()
if err != nil { if err != nil {
panic("Unable to decode TOML config file: " + err.Error()) slog.Error("unable to close JSON config file: ", err)
}
}(file)
decoder := json.NewDecoder(file)
Config := Configuration{}
err = decoder.Decode(&Config)
if err != nil {
panic("unable to decode JSON config file: " + err.Error())
} }
return Config return Config

View File

@ -1,7 +1,7 @@
package database package database
import ( import (
"GoWeb/app" "GoWeb/internal"
"database/sql" "database/sql"
"fmt" "fmt"
_ "github.com/lib/pq" _ "github.com/lib/pq"
@ -9,7 +9,7 @@ import (
) )
// Connect returns a new database connection // Connect returns a new database connection
func Connect(app *app.App) *sql.DB { func Connect(app *app.Deps) *sql.DB {
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)

View File

@ -1,7 +1,7 @@
package database package database
import ( import (
"GoWeb/app" "GoWeb/internal"
"errors" "errors"
"fmt" "fmt"
"github.com/lib/pq" "github.com/lib/pq"
@ -9,8 +9,9 @@ import (
"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
func Migrate(app *app.App, anyStruct interface{}) error { // as the type and create columns with the same name as the fields of the object
func Migrate(app *app.Deps, 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,12 +35,13 @@ 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.Deps, tableName string) error {
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 {
@ -61,7 +67,7 @@ func createTable(app *app.App, tableName string) error {
} }
// 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.Deps, tableName, columnName, columnType string) error {
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 {

18
env_example.json Normal file
View File

@ -0,0 +1,18 @@
{
"Db": {
"DbIp": "127.0.0.1",
"DbPort": "5432",
"DbName": "database",
"DbUser": "user",
"DbPassword": "password",
"DbAutoMigrate": true
},
"Listen": {
"HttpIp": "127.0.0.1",
"HttpPort": "8090"
},
"Template": {
"BaseTemplateName": "internal/frontend/templates/base.html",
"ContentPath": "internal/frontend/templates"
}
}

View File

@ -1,14 +0,0 @@
[Db]
DbIp = "127.0.0.1"
DbPort = "5432"
DbName = "test"
DbUser = "postgres"
DbPassword = "postgres"
DbAutoMigrate = true
[Listen]
HttpIp = "127.0.0.1"
HttpPort = "8090"
[Template]
BaseTemplateName = "templates/base.html"

6
go.mod
View File

@ -1,10 +1,8 @@
module GoWeb module GoWeb
go 1.21 go 1.22
require ( require (
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
golang.org/x/crypto v0.13.0 golang.org/x/crypto v0.24.0
) )
require github.com/BurntSushi/toml v1.3.2

6
go.sum
View File

@ -1,6 +1,4 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=

View File

@ -1,8 +1,8 @@
package controllers package controllers
import ( import (
"GoWeb/app" "GoWeb/internal"
"GoWeb/models" "GoWeb/internal/models"
"GoWeb/security" "GoWeb/security"
"GoWeb/templating" "GoWeb/templating"
"net/http" "net/http"
@ -10,7 +10,7 @@ import (
// Get is a wrapper struct for the App struct // Get is a wrapper struct for the App struct
type Get struct { type Get struct {
App *app.App App *app.Deps
} }
func (g *Get) ShowHome(w http.ResponseWriter, _ *http.Request) { func (g *Get) ShowHome(w http.ResponseWriter, _ *http.Request) {
@ -22,7 +22,7 @@ func (g *Get) ShowHome(w http.ResponseWriter, _ *http.Request) {
Test: "Hello World!", Test: "Hello World!",
} }
templating.RenderTemplate(g.App, w, "templates/pages/home.html", data) templating.RenderTemplate(w, "templates/pages/home.html", data)
} }
func (g *Get) ShowRegister(w http.ResponseWriter, r *http.Request) { func (g *Get) ShowRegister(w http.ResponseWriter, r *http.Request) {
@ -39,7 +39,7 @@ func (g *Get) ShowRegister(w http.ResponseWriter, r *http.Request) {
CsrfToken: CsrfToken, CsrfToken: CsrfToken,
} }
templating.RenderTemplate(g.App, w, "templates/pages/register.html", data) templating.RenderTemplate(w, "templates/pages/register.html", data)
} }
func (g *Get) ShowLogin(w http.ResponseWriter, r *http.Request) { func (g *Get) ShowLogin(w http.ResponseWriter, r *http.Request) {
@ -56,7 +56,7 @@ func (g *Get) ShowLogin(w http.ResponseWriter, r *http.Request) {
CsrfToken: CsrfToken, CsrfToken: CsrfToken,
} }
templating.RenderTemplate(g.App, w, "templates/pages/login.html", data) templating.RenderTemplate(w, "templates/pages/login.html", data)
} }
func (g *Get) Logout(w http.ResponseWriter, r *http.Request) { func (g *Get) Logout(w http.ResponseWriter, r *http.Request) {

View File

@ -1,8 +1,8 @@
package controllers package controllers
import ( import (
"GoWeb/app" "GoWeb/internal"
"GoWeb/models" "GoWeb/internal/models"
"log/slog" "log/slog"
"net/http" "net/http"
"time" "time"
@ -10,7 +10,7 @@ import (
// Post is a wrapper struct for the App struct // Post is a wrapper struct for the App struct
type Post struct { type Post struct {
App *app.App App *app.Deps
} }
func (p *Post) Login(w http.ResponseWriter, r *http.Request) { func (p *Post) Login(w http.ResponseWriter, r *http.Request) {

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="/app/frontend/staticyle.css" rel="stylesheet">
</head> </head>
<body> <body>
{{ template "content" . }} {{ template "content" . }}

View File

@ -7,11 +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> <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"> <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

@ -6,8 +6,8 @@ import (
"embed" "embed"
) )
// App contains and supplies available configurations and connections // Deps contains and supplies available configurations and connections
type App struct { type Deps 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

View File

@ -2,6 +2,8 @@ package middleware
import "net/http" import "net/http"
type MiddlewareFunc func(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request)
// ProcessGroup is a wrapper function for the http.HandleFunc function // ProcessGroup is a wrapper function for the http.HandleFunc function
// that takes the function you want to execute (f) and the middleware you want // 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 // to execute (m) this should be used when processing multiple groups of middleware at a time

View File

@ -1,13 +1,13 @@
package models package models
import ( import (
"GoWeb/app"
"GoWeb/database" "GoWeb/database"
"GoWeb/internal"
"time" "time"
) )
// RunAllMigrations defines the structs that should be represented in the database // RunAllMigrations defines the structs that should be represented in the database
func RunAllMigrations(app *app.App) error { func RunAllMigrations(app *app.Deps) error {
// Declare new dummy user for reflection // Declare new dummy user for reflection
user := User{ user := User{
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

View File

@ -1,7 +1,7 @@
package models package models
import ( import (
"GoWeb/app" "GoWeb/internal"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"log/slog" "log/slog"
@ -31,7 +31,7 @@ const (
) )
// 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, remember bool) (Session, error) { func CreateSession(app *app.Deps, 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)
@ -62,7 +62,7 @@ func CreateSession(app *app.App, w http.ResponseWriter, userId int64, remember b
return session, nil return session, nil
} }
func GetSessionByAuthToken(app *app.App, authToken string) (Session, error) { func SessionByAuthToken(app *app.Deps, authToken string) (Session, error) {
session := Session{} session := Session{}
err := app.Db.QueryRow(selectSessionByAuthToken, authToken).Scan(&session.Id, &session.UserId, &session.AuthToken, &session.RememberMe, &session.CreatedAt) err := app.Db.QueryRow(selectSessionByAuthToken, authToken).Scan(&session.Id, &session.UserId, &session.AuthToken, &session.RememberMe, &session.CreatedAt)
@ -74,7 +74,7 @@ func GetSessionByAuthToken(app *app.App, authToken string) (Session, error) {
} }
// generateAuthToken generates a random 64-byte string // generateAuthToken generates a random 64-byte string
func generateAuthToken(app *app.App) string { func generateAuthToken(app *app.Deps) string {
b := make([]byte, 64) b := make([]byte, 64)
_, err := rand.Read(b) _, err := rand.Read(b)
if err != nil { if err != nil {
@ -85,7 +85,7 @@ func generateAuthToken(app *app.App) string {
} }
// 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.Deps, w http.ResponseWriter, session Session) {
cookie := &http.Cookie{} cookie := &http.Cookie{}
if session.RememberMe { if session.RememberMe {
cookie = &http.Cookie{ cookie = &http.Cookie{
@ -111,7 +111,7 @@ func createSessionCookie(app *app.App, w http.ResponseWriter, session Session) {
} }
// deleteSessionCookie deletes the session cookie // deleteSessionCookie deletes the session cookie
func deleteSessionCookie(app *app.App, w http.ResponseWriter) { func deleteSessionCookie(app *app.Deps, w http.ResponseWriter) {
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: "session", Name: "session",
Value: "", Value: "",
@ -123,7 +123,7 @@ 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.Deps, w http.ResponseWriter, authToken string) error {
_, err := app.Db.Exec(deleteSessionByAuthToken, authToken) _, err := app.Db.Exec(deleteSessionByAuthToken, authToken)
if err != nil { if err != nil {
slog.Error("error deleting session from database") slog.Error("error deleting session from database")
@ -136,7 +136,7 @@ func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken str
} }
// ScheduledSessionCleanup deletes expired sessions from the database // ScheduledSessionCleanup deletes expired sessions from the database
func ScheduledSessionCleanup(app *app.App) { func ScheduledSessionCleanup(app *app.Deps) {
// Delete sessions older than 30 days (remember me sessions) // Delete sessions older than 30 days (remember me sessions)
_, err := app.Db.Exec(deleteSessionsOlderThan30Days) _, err := app.Db.Exec(deleteSessionsOlderThan30Days)
if err != nil { if err != nil {

View File

@ -1,7 +1,9 @@
package models package models
import ( import (
"GoWeb/app" "GoWeb/internal"
"crypto/sha256"
"encoding/hex"
"log/slog" "log/slog"
"net/http" "net/http"
"time" "time"
@ -27,23 +29,23 @@ const (
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.Deps, r *http.Request) (User, error) {
cookie, err := r.Cookie("session") cookie, err := r.Cookie("session")
if err != nil { if err != nil {
return User{}, err return User{}, err
} }
session, err := GetSessionByAuthToken(app, cookie.Value) session, err := SessionByAuthToken(app, cookie.Value)
if err != nil { if err != nil {
return User{}, err 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 // 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.Deps, id int64) (User, error) {
user := User{} user := User{}
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)
@ -54,8 +56,8 @@ func GetUserById(app *app.App, id int64) (User, error) {
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.Deps, username string) (User, error) {
user := User{} user := User{}
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)
@ -67,8 +69,13 @@ 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.Deps, 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 { if err != nil {
slog.Error("error hashing password: " + err.Error()) slog.Error("error hashing password: " + err.Error())
return User{}, err return User{}, err
@ -82,11 +89,11 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T
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, remember bool) (Session, error) { func AuthenticateUser(app *app.Deps, w http.ResponseWriter, username string, password string, remember bool) (Session, error) {
var user User var user User
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)
@ -95,7 +102,12 @@ func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, pass
return Session{}, err 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 if err != nil { // Failed to validate password, doesn't match
slog.Info("incorrect password:" + username) slog.Info("incorrect password:" + username)
return Session{}, err return Session{}, err
@ -105,7 +117,7 @@ func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, pass
} }
// 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.Deps, w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session") cookie, err := r.Cookie("session")
if err != nil { if err != nil {
return return

View File

@ -1,15 +1,15 @@
package routes package routes
import ( import (
"GoWeb/app" "GoWeb/internal"
"GoWeb/controllers" "GoWeb/internal/controllers"
"io/fs" "io/fs"
"log/slog" "log/slog"
"net/http" "net/http"
) )
// Get defines all project get routes // Get defines all project get routes
func Get(app *app.App) { func Get(app *app.Deps) {
// Get controller struct initialize // Get controller struct initialize
getController := controllers.Get{ getController := controllers.Get{
App: app, App: app,

View File

@ -1,14 +1,14 @@
package routes package routes
import ( import (
"GoWeb/app" "GoWeb/internal"
"GoWeb/controllers" "GoWeb/internal/controllers"
"GoWeb/middleware" "GoWeb/internal/middleware"
"net/http" "net/http"
) )
// Post defines all project post routes // Post defines all project post routes
func Post(app *app.App) { func Post(app *app.Deps) {
// Post controller struct initialize // Post controller struct initialize
postController := controllers.Post{ postController := controllers.Post{
App: app, App: app,

71
internal/scheduler.go Normal file
View File

@ -0,0 +1,71 @@
package app
import (
"sync"
"time"
)
type Scheduled struct {
EveryReboot []func(app *Deps)
EverySecond []func(app *Deps)
EveryMinute []func(app *Deps)
EveryHour []func(app *Deps)
EveryDay []func(app *Deps)
EveryWeek []func(app *Deps)
EveryMonth []func(app *Deps)
EveryYear []func(app *Deps)
}
type Task struct {
Funcs []func(app *Deps)
Interval time.Duration
}
func RunScheduledTasks(app *Deps, 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 *Deps)) {
defer func() { <-runner }()
f(app)
}(f)
}
case <-stop:
return
}
}
}(task, runner)
}
wg.Wait()
for _, runner := range runners {
close(runner)
}
}

24
main.go
View File

@ -1,11 +1,12 @@
package main package main
import ( import (
"GoWeb/app"
"GoWeb/config" "GoWeb/config"
"GoWeb/database" "GoWeb/database"
"GoWeb/models" "GoWeb/internal"
"GoWeb/routes" "GoWeb/internal/models"
"GoWeb/internal/routes"
"GoWeb/templating"
"context" "context"
"embed" "embed"
"errors" "errors"
@ -17,12 +18,12 @@ import (
"time" "time"
) )
//go:embed templates static //go:embed internal/frontend/templates internal/frontend/static
var res embed.FS var res embed.FS
func main() { func main() {
// Create instance of App // Create instance of Deps
appLoaded := app.App{} appLoaded := app.Deps{}
// Load config file to application // Load config file to application
appLoaded.Config = config.LoadConfig() appLoaded.Config = config.LoadConfig()
@ -59,14 +60,21 @@ func main() {
// Assign and run scheduled tasks // Assign and run scheduled tasks
appLoaded.ScheduledTasks = app.Scheduled{ appLoaded.ScheduledTasks = app.Scheduled{
EveryReboot: []func(app *app.App){models.ScheduledSessionCleanup}, EveryReboot: []func(app *app.Deps){models.ScheduledSessionCleanup},
EveryMinute: []func(app *app.App){models.ScheduledSessionCleanup}, EveryMinute: []func(app *app.Deps){models.ScheduledSessionCleanup},
} }
// Define Routes // Define Routes
routes.Get(&appLoaded) routes.Get(&appLoaded)
routes.Post(&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() {

View File

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

View File

@ -1,4 +1,4 @@
package restclient package rest
import ( import (
"bytes" "bytes"

85
templating/builder.go Normal file
View File

@ -0,0 +1,85 @@
package templating
import (
"GoWeb/internal"
"fmt"
"html/template"
"io/fs"
"log/slog"
"net/http"
)
var templates = make(map[string]*template.Template) // This is only used here, does not need to be in internal.Deps
func BuildPages(app *app.Deps) error {
basePath := app.Config.Template.BaseName
baseContent, err := app.Res.ReadFile(basePath)
if err != nil {
return fmt.Errorf("error reading base file: %w", err)
}
base, err := template.New(basePath).Parse(string(baseContent)) // Sets filepath as name and parses content
if err != nil {
return fmt.Errorf("error parsing base file: %w", err)
}
readFilesRecursively := func(fsys fs.FS, root string) ([]string, error) {
var files []string
err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("error walking the path %q: %w", path, err)
}
if !d.IsDir() {
files = append(files, path)
}
return nil
})
return files, err
}
// Get all file paths in the directory tree
filePaths, err := readFilesRecursively(app.Res, app.Config.Template.ContentPath)
if err != nil {
return fmt.Errorf("error reading files recursively: %w", err)
}
for _, contentPath := range filePaths { // Create a new template base + content for each page
content, err := app.Res.ReadFile(contentPath)
if err != nil {
return fmt.Errorf("error reading content file %s: %w", contentPath, err)
}
t, err := base.Clone()
if err != nil {
return fmt.Errorf("error cloning base template: %w", err)
}
_, err = t.Parse(string(content))
if err != nil {
return fmt.Errorf("error parsing content: %w", err)
}
templates[contentPath] = t
}
return nil
}
func RenderTemplate(w http.ResponseWriter, contentPath string, data any) {
t, ok := templates[contentPath]
if !ok {
err := fmt.Errorf("template not found for path: %s", contentPath)
slog.Error(err.Error())
http.Error(w, "Template not found", 404)
return
}
err := t.Execute(w, data) // Execute prebuilt template with dynamic data
if err != nil {
err = fmt.Errorf("error executing template: %w", err)
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}
}

View File

@ -1,48 +0,0 @@
package templating
import (
"GoWeb/app"
"html/template"
"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
templateContent, err := app.Res.ReadFile(templatePath)
if err != nil {
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}
t, err := template.New(templatePath).Parse(string(templateContent))
if err != nil {
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}
content, err := app.Res.ReadFile(contentPath)
if err != nil {
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}
t, err = t.Parse(string(content))
if err != nil {
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}
err = t.Execute(w, data)
if err != nil {
slog.Error(err.Error())
http.Error(w, err.Error(), 500)
return
}
}