29 Commits

Author SHA1 Message Date
9670b7d717 Better naming of functions and files, delete less than helpful comments 2023-08-03 12:09:40 -05:00
a1438f4fe2 Update README.md 2023-07-31 20:18:11 -05:00
052fa689c7 Merge branch 'middleware' 2023-07-31 18:41:49 -05:00
f1fad7e4e3 Pass in handler to middleware, create definition for MiddlewareFunc 2023-07-31 18:37:54 -05:00
b475da66da Fix nil error for GET requests 2023-07-26 13:09:01 -05:00
d0085ab2c3 Update crypto dependency 2023-07-25 15:36:25 -05:00
58514f4c5f Update client to handle GET (no body) requests, multipart requests, and JSON requests 2023-07-25 15:34:11 -05:00
606f5df45a Refactor name 2023-07-22 23:37:38 -05:00
2a32a1b3ce Add restclient package, and a simple function to send HTTP requests 2023-07-21 16:35:18 -05:00
eb36156c52 Change function name to ProcessGroup 2023-07-21 16:26:43 -05:00
bada24884a Use ungrouped CSRF middleware on register and login POST routes 2023-07-21 11:59:55 -05:00
05397c2b61 Initial middleware implementation for CSRF and update comment 2023-07-21 11:59:01 -05:00
3d80b95f55 Initial wrapper implementation 2023-07-07 18:05:17 -05:00
6da7d408f9 Add .gitattributes to force LF line endings 2023-05-05 12:19:17 -05:00
max
e993bcf317 Update dependency versions 2023-05-04 09:00:35 -05:00
9b231a73d6 Update README.md 2023-04-07 21:32:39 -05:00
34acd0fa8d Remove old session query 2023-04-07 21:27:14 -05:00
71d3bd77d0 Add ability to get session given an AuthToken, fix GetCurrentUser() 2023-04-07 21:23:46 -05:00
1451abcca4 Formatting 2023-04-06 12:01:29 -05:00
max
53a780343f Fix scheduler by adding a wait group 2023-04-06 09:55:56 -05:00
max
8e4c5e3268 Fix wrong query for clearing 6-hour old sessions 2023-04-06 09:35:53 -05:00
max
f18f512fea Properly set the name of the checkbox for parsing 2023-04-06 09:31:12 -05:00
max
58328fe505 Fix some SQL errors 2023-04-06 09:30:53 -05:00
max
10e7830349 Remember me checkbox on login form 2023-04-06 08:57:17 -05:00
max
5f7e674d32 Add remember me functionality, handle both types of sessions appropriately 2023-04-06 08:56:48 -05:00
max
ec9c1a8fb5 Initial clear old sessions implementation 2023-04-04 14:37:36 -05:00
max
242029f2e5 Initial task scheduler implementation 2023-04-04 14:37:23 -05:00
b1c65f2ab1 Remove erroneous SetCookie (leftover from redundant remove) 2023-03-27 15:05:11 -05:00
max
965139ea18 Remove redundant session cookie clear 2023-03-16 08:40:50 -05:00
25 changed files with 372 additions and 183 deletions

1
.gitattributes vendored Normal file
View File

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

View File

@ -12,9 +12,12 @@ fine with getting your hands dirty, but I plan on having it ready to go for more
- Routing/controllers
- Templating
- Simple database migration system
- Built in REST client
- CSRF protection
- Middleware
- Minimal user login/registration + sessions
- 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)

View File

@ -8,7 +8,8 @@ import (
// App contains and supplies available configurations and connections
type App struct {
Config config.Configuration // Configuration file
Db *sql.DB // Database connection
Res *embed.FS // Resources from the embedded filesystem
Config config.Configuration // Configuration file
Db *sql.DB // Database connection
Res *embed.FS // Resources from the embedded filesystem
ScheduledTasks Scheduled // Scheduled contains a struct of all scheduled functions
}

71
app/schedule.go Normal file
View File

@ -0,0 +1,71 @@
package app
import (
"sync"
"time"
)
type Scheduled struct {
EveryReboot []func(app *App)
EverySecond []func(app *App)
EveryMinute []func(app *App)
EveryHour []func(app *App)
EveryDay []func(app *App)
EveryWeek []func(app *App)
EveryMonth []func(app *App)
EveryYear []func(app *App)
}
type Task struct {
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

@ -43,7 +43,6 @@ func LoadConfig() Configuration {
}
}(file)
// Decode json config file to Configuration struct named config
decoder := json.NewDecoder(file)
Config := Configuration{}
err = decoder.Decode(&Config)

65
controllers/get.go Normal file
View File

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

View File

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

View File

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

View File

@ -8,8 +8,8 @@ import (
"log"
)
// ConnectDB returns a new database connection
func ConnectDB(app *app.App) *sql.DB {
// 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",

View File

@ -36,7 +36,6 @@ 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
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 {
@ -63,7 +62,6 @@ func createTable(app *app.App, tableName string) error {
// 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 {
// 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 {

4
go.mod
View File

@ -3,6 +3,6 @@ module GoWeb
go 1.20
require (
github.com/lib/pq v1.10.7
golang.org/x/crypto v0.7.0
github.com/lib/pq v1.10.9
golang.org/x/crypto v0.11.0
)

8
go.sum
View File

@ -1,4 +1,4 @@
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
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=

15
main.go
View File

@ -44,7 +44,7 @@ func main() {
log.SetOutput(file)
// Connect to database and run migrations
appLoaded.Db = database.ConnectDB(&appLoaded)
appLoaded.Db = database.Connect(&appLoaded)
if appLoaded.Config.Db.AutoMigrate {
err = models.RunAllMigrations(&appLoaded)
if err != nil {
@ -53,9 +53,15 @@ func main() {
}
}
// Assign and run scheduled tasks
appLoaded.ScheduledTasks = app.Scheduled{
EveryReboot: []func(app *app.App){models.ScheduledSessionCleanup},
EveryMinute: []func(app *app.App){models.ScheduledSessionCleanup},
}
// Define Routes
routes.GetRoutes(&appLoaded)
routes.PostRoutes(&appLoaded)
routes.Get(&appLoaded)
routes.Post(&appLoaded)
// Start server
server := &http.Server{Addr: appLoaded.Config.Listen.Ip + ":" + appLoaded.Config.Listen.Port}
@ -70,6 +76,9 @@ func main() {
// Wait for interrupt signal and shut down the server
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)
stop := make(chan struct{})
go app.RunScheduledTasks(&appLoaded, 100, stop)
<-interrupt
log.Println("Interrupt signal received. Shutting down server...")

21
middleware/csrf.go Normal file
View File

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

5
middleware/groups.go Normal file
View File

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

14
middleware/wrapper.go Normal file
View File

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

View File

@ -22,10 +22,11 @@ func RunAllMigrations(app *app.App) error {
}
session := Session{
Id: 1,
UserId: 1,
AuthToken: "migrate",
CreatedAt: time.Now(),
Id: 1,
UserId: 1,
AuthToken: "migrate",
RememberMe: false,
CreatedAt: time.Now(),
}
err = database.Migrate(app, session)
if err != nil {

View File

@ -10,28 +10,32 @@ import (
)
type Session struct {
Id int64
UserId int64
AuthToken string
CreatedAt time.Time
Id int64
UserId int64
AuthToken string
RememberMe bool
CreatedAt time.Time
}
const sessionColumnsNoId = "\"UserId\", \"AuthToken\", \"CreatedAt\""
const sessionColumnsNoId = "\"UserId\", \"AuthToken\",\"RememberMe\", \"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"
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, $4) RETURNING \"Id\""
deleteSessionByAuthToken = "DELETE FROM " + sessionTable + " WHERE \"AuthToken\" = $1"
deleteSessionsOlderThan30Days = "DELETE FROM " + sessionTable + " WHERE \"CreatedAt\" < NOW() - INTERVAL '30 days'"
deleteSessionsOlderThan6Hours = "DELETE FROM " + sessionTable + " WHERE \"CreatedAt\" < NOW() - INTERVAL '6 hours' AND \"RememberMe\" = false"
)
// CreateSession creates a new session for a user
func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session, error) {
func CreateSession(app *app.App, w http.ResponseWriter, userId int64, remember bool) (Session, error) {
session := Session{}
session.UserId = userId
session.AuthToken = generateAuthToken(app)
session.RememberMe = remember
session.CreatedAt = time.Now()
// If the AuthToken column for any user matches the token, set existingAuthToken to true
@ -46,11 +50,11 @@ func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session,
// 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)
return CreateSession(app, w, userId, remember)
}
// Insert session into database
err = app.Db.QueryRow(insertSession, session.UserId, session.AuthToken, session.CreatedAt).Scan(&session.Id)
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")
return Session{}, err
@ -60,28 +64,50 @@ func CreateSession(app *app.App, w http.ResponseWriter, userId int64) (Session,
return session, nil
}
func GetSessionByAuthToken(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
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,
cookie := &http.Cookie{}
if session.RememberMe {
cookie = &http.Cookie{
Name: "session",
Value: session.AuthToken,
Path: "/",
MaxAge: 2592000 * 1000, // 30 days in ms
HttpOnly: true,
Secure: true,
}
} else {
cookie = &http.Cookie{
Name: "session",
Value: session.AuthToken,
Path: "/",
MaxAge: 21600 * 1000, // 6 hours in ms
HttpOnly: true,
Secure: true,
}
}
http.SetCookie(w, cookie)
@ -101,7 +127,6 @@ func deleteSessionCookie(app *app.App, w http.ResponseWriter) {
// 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")
@ -112,3 +137,22 @@ func DeleteSessionByAuthToken(app *app.App, w http.ResponseWriter, authToken str
return nil
}
// ScheduledSessionCleanup deletes expired sessions from the database
func ScheduledSessionCleanup(app *app.App) {
// Delete sessions older than 30 days (remember me sessions)
_, err := app.Db.Exec(deleteSessionsOlderThan30Days)
if err != nil {
log.Println("Error deleting 30 day expired sessions from database")
log.Println(err)
}
// 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)
}
log.Println("Deleted expired sessions from database")
}

View File

@ -23,10 +23,9 @@ 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\""
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
@ -37,23 +36,19 @@ func GetCurrentUser(app *app.App, r *http.Request) (User, error) {
return User{}, err
}
var userId int64
// Query row by AuthToken
err = app.Db.QueryRow(selectSessionIdByAuthToken, cookie.Value).Scan(&userId)
session, err := GetSessionByAuthToken(app, cookie.Value)
if err != nil {
log.Println("Error querying session row with session: " + cookie.Value)
log.Println("Error getting session by auth token")
return User{}, err
}
return GetUserById(app, userId)
return GetUserById(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) {
user := User{}
// Query row by id
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))
@ -67,7 +62,6 @@ func GetUserById(app *app.App, id int64) (User, error) {
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)
@ -79,7 +73,6 @@ 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 password
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Println("Error hashing password when creating user")
@ -98,49 +91,35 @@ func CreateUser(app *app.App, username string, password string, createdAt time.T
}
// AuthenticateUser validates the password for the specified user
func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string) (Session, error) {
func AuthenticateUser(app *app.App, w http.ResponseWriter, username string, password string, remember bool) (Session, error) {
var user User
// 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("Authentication error (user not found) for user:" + username)
return Session{}, err
}
// Validate password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil { // Failed to validate password, doesn't match
log.Println("Authentication error (incorrect password) for user:" + username)
return Session{}, err
} else {
return CreateSession(app, w, user.Id)
return CreateSession(app, w, user.Id, remember)
}
}
// LogoutUser deletes the session cookie and AuthToken from the database
func LogoutUser(app *app.App, w http.ResponseWriter, r *http.Request) {
// Get cookie from request
cookie, err := r.Cookie("session")
if err != nil {
log.Println("Error getting cookie from request")
return
}
// Set token to empty string
err = DeleteSessionByAuthToken(app, w, cookie.Value)
if err != nil {
log.Println("Error deleting session by AuthToken")
return
}
// Delete cookie
cookie = &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
}
http.SetCookie(w, cookie)
}

57
restclient/client.go Normal file
View File

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

View File

@ -8,10 +8,10 @@ import (
"net/http"
)
// GetRoutes defines all project get routes
func GetRoutes(app *app.App) {
// Get defines all project get routes
func Get(app *app.App) {
// Get controller struct initialize
getController := controllers.GetController{
getController := controllers.Get{
App: app,
}

20
routes/post.go Normal file
View File

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

View File

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

View File

@ -10,7 +10,6 @@ import (
// GenerateCsrfToken generates a csrf token and assigns it to a cookie for double submit cookie csrf protection
func GenerateCsrfToken(w http.ResponseWriter, _ *http.Request) (string, error) {
// Generate random 64 character string (alpha-numeric)
buff := make([]byte, int(math.Ceil(float64(64)/2)))
_, err := rand.Read(buff)
if err != nil {

View File

@ -10,6 +10,8 @@
<input id="username" name="username" type="text" placeholder="John"><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 type="submit" value="Submit">
</form>
</div>