first working server

This commit is contained in:
$(pass /github/name)
2024-07-03 09:40:45 +02:00
parent 9bd8d48243
commit 6d34d99835
20 changed files with 340 additions and 128 deletions

3
.gitignore vendored
View File

@@ -41,8 +41,9 @@ go.work
!README.md !README.md
!LICENSE !LICENSE
# all template files:
!*.template*
# !Makefile # !Makefile
# ...even if they are in subdirectories # ...even if they are in subdirectories
!*/ !*/

View File

@@ -0,0 +1,13 @@
{
"db":
{
"DBPath": "data/db.sqlite3"
},
"smtp": {
"server": "mail.server.com",
"user": "username",
"password": "password",
"port": 465,
"mailtype": "html"
}
}

4
go.mod
View File

@@ -3,10 +3,8 @@ module GoMembership
go 1.22.2 go 1.22.2
require ( require (
github.com/go-sql-driver/mysql v1.8.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
golang.org/x/crypto v0.24.0 golang.org/x/crypto v0.24.0
) )
require filippo.io/edwards25519 v1.1.0 // indirect

6
go.sum
View File

@@ -1,7 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=

View File

@@ -1,38 +1,69 @@
package config package config
import ( import (
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
"encoding/json" "encoding/json"
"log"
"os" "os"
"path/filepath" "path/filepath"
"sync"
) )
type DatabaseConfig struct { type DatabaseConfig struct {
DBPath string `json:"DBPath"` DBPath string `json:"DBPath"`
} }
type AuthenticationConfig struct {
JWTSecret string
CSRFSecret string
}
type Config struct { type Config struct {
DB DatabaseConfig `json:"db"` DB DatabaseConfig `json:"db"`
Auth AuthenticationConfig
} }
var (
pConfig Config
once sync.Once
loaded bool
)
func LoadConfig() *Config { func LoadConfig() *Config {
path, err := os.Getwd() path, err := os.Getwd()
if err != nil { if err != nil {
log.Fatalf("could not get working directory: %v", err) logger.Error.Fatalf("could not get working directory: %v", err)
} }
configFile, err := os.Open(filepath.Join(path, "configs", "config.json")) configFile, err := os.Open(filepath.Join(path, "configs", "config.json"))
if err != nil { if err != nil {
log.Fatalf("could not open config file: %v", err) logger.Error.Fatalf("could not open config file: %v", err)
} }
defer configFile.Close() defer configFile.Close()
decoder := json.NewDecoder(configFile) decoder := json.NewDecoder(configFile)
config := &Config{} // pConfig = &Config{}
err = decoder.Decode(config) err = decoder.Decode(&pConfig)
if err != nil { if err != nil {
log.Fatalf("could not decode config file: %v", err) logger.Error.Fatalf("could not decode config file: %v", err)
}
if !loaded {
once.Do(
func() {
csrfSecret, err := utils.GenerateRandomString(32)
if err != nil {
logger.Error.Fatalf("could not generate CSRF secret: %v", err)
} }
return config jwtSecret, err := utils.GenerateRandomString(32)
if err != nil {
logger.Error.Fatalf("could not generate JWT secret: %v", err)
}
pConfig.Auth.JWTSecret = jwtSecret
pConfig.Auth.CSRFSecret = csrfSecret
loaded = true
})
}
return &pConfig
} }

View File

@@ -7,6 +7,7 @@ import (
// "github.com/gorilla/mux" // "github.com/gorilla/mux"
"net/http" "net/http"
// "strconv" // "strconv"
"GoMembership/pkg/logger"
) )
type UserController struct { type UserController struct {
@@ -18,13 +19,17 @@ func NewUserController(service services.UserService) *UserController {
} }
func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) { func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
logger.Info.Println("registering user")
var user models.User var user models.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil { if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
logger.Error.Printf("Couldn't decode Userdata: %v", err)
return return
} }
if err := uc.service.RegisterUser(&user); err != nil { if err := uc.service.RegisterUser(&user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error.Printf("Couldn't register User: %v", err)
return return
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
@@ -41,8 +46,8 @@ func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
return return
} }
json.NewEncoder(w).Encode(user) json.NewEncoder(w).Encode(user)
} */ }
*/
/* func (uc *UserController) GetUserID(w http.ResponseWriter, r *http.Request) { /* func (uc *UserController) GetUserID(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"]) id, err := strconv.Atoi(vars["id"])

View File

@@ -2,8 +2,8 @@ package database
import ( import (
"GoMembership/internal/config" "GoMembership/internal/config"
"GoMembership/pkg/logger"
"database/sql" "database/sql"
"log"
"os" "os"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
@@ -29,13 +29,21 @@ func initializeDB(dbPath string, schemaPath string) error {
func Connect() *sql.DB { func Connect() *sql.DB {
cfg := config.LoadConfig() cfg := config.LoadConfig()
dsn := cfg.DB.DBPath _, err := os.Stat(cfg.DB.DBPath)
db, err := sql.Open("sqlite3", dsn) if os.IsNotExist(err) {
initErr := initializeDB(cfg.DB.DBPath, "internal/database/schema.sql")
if initErr != nil {
logger.Error.Fatalf("Couldn't create database: %v", initErr)
}
logger.Info.Println("Created new database")
}
db, err := sql.Open("sqlite3", cfg.DB.DBPath)
if err != nil { if err != nil {
log.Fatal(err) logger.Error.Fatal(err)
} }
if err := db.Ping(); err != nil { if err := db.Ping(); err != nil {
log.Fatal(err) logger.Error.Fatal(err)
} }
return db return db
} }

View File

@@ -5,9 +5,13 @@ import (
) )
func AuthMiddleware(next http.Handler) http.Handler { func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(
// Authentication logic here func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "your-secret-token" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }

View File

@@ -0,0 +1,114 @@
package middlewares
import (
"GoMembership/internal/config"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"net/http"
"strings"
)
// GenerateCSRFToken generates HMAC-signed CSRF token
func GenerateCSRFToken(sessionID string, secretKey string) string {
// Create message to be signed (e.g., combining sessionID with some random value)
randomString, err := utils.GenerateRandomString(8)
if err != nil {
logger.Error.Fatalf("Could not create random string: %v", err)
return ""
}
message := sessionID + "!" + randomString
// Create HMAC hash using SHA-256
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(message))
signature := h.Sum(nil)
// Encode signature and message into a CSRF token
csrfToken := base64.StdEncoding.EncodeToString(signature) + "." + message
return csrfToken
}
func ComputeHMAC(message string, secretKey string) []byte {
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(message))
return h.Sum(nil)
}
// CSRFMiddleware verifies HMAC-signed CSRF token
func CSRFMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
next.ServeHTTP(w, r)
return
}
csrfSecret := config.LoadConfig().Auth.CSRFSecret
// Retrieve CSRF token from request (e.g., from cookie, header, or form data)
csrfToken := r.Header.Get("X-CSRF-Token")
// Extract signature and message from CSRF token
parts := strings.SplitN(csrfToken, ".", 2)
if len(parts) != 2 {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
receivedSignature := parts[0]
receivedMessage := parts[1]
// Compute HMAC using the received message and the CSRF secret key
computedSignature := ComputeHMAC(receivedMessage, csrfSecret)
// Compare computed HMAC with received signature
if !hmac.Equal([]byte(receivedSignature), computedSignature) {
http.Error(w, "CSRF Token validation failed", http.StatusForbidden)
return
}
// CSRF token is valid, proceed to the next handler
next.ServeHTTP(w, r)
})
}
func GenerateCSRFTokenHandler(w http.ResponseWriter, r *http.Request) {
// Simulate getting session ID from authenticated session
sessionID := "exampleSessionID123"
// Generate HMAC-signed CSRF token
csrfToken := GenerateCSRFToken(sessionID, config.LoadConfig().Auth.CSRFSecret)
// Set CSRF token in a cookie (example)
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: csrfToken,
Path: "/",
HttpOnly: true,
Secure: true,
})
}
/* func GenerateCSRFTokenHandler() http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
token, err := GenerateCSRFToken()
if err != nil {
http.Error(w, "Could not generate CSRF token", http.StatusInternalServerError)
return
}
// Set CSRF token in cookie
http.SetCookie(w, &http.Cookie{
Name: "csrf_token",
Value: token,
Path: "/",
})
logger.Info.Printf("generated token: %v", token)
// Return CSRF token in response
w.Header().Set("X-CSRF-Token", token)
w.WriteHeader(http.StatusOK)
})
} */

View File

@@ -0,0 +1,17 @@
package middlewares
import (
"GoMembership/pkg/logger"
"net/http"
"time"
)
// LoggerMiddleware logs each incoming HTTP request
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
logger.Info.Printf("%s %s %s", r.Method, r.RequestURI, time.Since(start))
})
}

View File

@@ -9,7 +9,8 @@ type User struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"-"`
Salt string `json:"-"`
IBAN string `json:"iban"` IBAN string `json:"iban"`
BIC string `json:"bic"` BIC string `json:"bic"`
MandateReference string `json:"mandate_reference"` MandateReference string `json:"mandate_reference"`

View File

@@ -9,7 +9,7 @@ import (
type UserRepository interface { type UserRepository interface {
CreateUser(user *models.User) error CreateUser(user *models.User) error
FindUserByID(id int) (*models.User, error) FindUserByID(id int) (*models.User, error)
// FindUserByEmail(email string) (*models.User, error) FindUserByEmail(email string) (*models.User, error)
} }
type userRepository struct { type userRepository struct {
@@ -20,16 +20,29 @@ func NewUserRepository(db *sql.DB) UserRepository {
return &userRepository{db} return &userRepository{db}
} }
func (r *userRepository) CreateUser(user *models.User) error { func (repo *userRepository) CreateUser(user *models.User) error {
query := "INSERT INTO users (first_name, last_name, email, password, iban, bic, mandate_reference, mandate_date_signed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" query := "INSERT INTO users (first_name, last_name, email, password, salt, iban, bic, mandate_reference, mandate_date_signed, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
_, err := r.db.Exec(query, user.FirstName, user.LastName, user.Email, user.Password, user.CreatedAt, user.UpdatedAt) _, err := repo.db.Exec(query, user.FirstName, user.LastName, user.Email, user.Password, user.Salt, user.IBAN, user.BIC, user.MandateReference, user.MandateDateSigned, user.CreatedAt, user.UpdatedAt)
return err return err
} }
func (r *userRepository) FindUserByID(id int) (*models.User, error) { func (repo *userRepository) FindUserByID(id int) (*models.User, error) {
var user models.User var user models.User
query := "SELECT id, first_name, last_name, email, iban, bic, mandate_reference FROM users WHERE id = ?" query := "SELECT id, first_name, last_name, email, iban, bic, mandate_reference FROM users WHERE id = ?"
err := r.db.QueryRow(query, id).Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email, &user.IBAN, &user.BIC, &user.MandateReference) err := repo.db.QueryRow(query, id).Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email, &user.IBAN, &user.BIC, &user.MandateReference)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.ErrUserNotFound
}
return nil, err
}
return &user, nil
}
func (repo *userRepository) FindUserByEmail(email string) (*models.User, error) {
var user models.User
query := "SELECT id, first_name, last_name, email, iban, bic, mandate_reference FROM users WHERE email = ?"
err := repo.db.QueryRow(query, email).Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email, &user.IBAN, &user.BIC, &user.MandateReference)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, errors.ErrUserNotFound return nil, errors.ErrUserNotFound

View File

@@ -1,13 +1,17 @@
package routes package routes
import ( import (
"net/http"
"GoMembership/internal/controllers" "GoMembership/internal/controllers"
// "GoMembership/internal/middlewares"
"GoMembership/pkg/logger"
// "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func RegisterRoutes(router *mux.Router, userController *controllers.UserController) { func RegisterRoutes(router *mux.Router, userController *controllers.UserController) {
logger.Info.Println("Registering /register route")
router.HandleFunc("/register", userController.RegisterUser).Methods("POST") router.HandleFunc("/register", userController.RegisterUser).Methods("POST")
} // router.HandleFunc("/login", userController.LoginUser).Methods("POST")
}

View File

@@ -1,20 +1,19 @@
package server package server
import ( import (
// "GoMembership/internal/config"
"GoMembership/internal/controllers" "GoMembership/internal/controllers"
"GoMembership/internal/database" "GoMembership/internal/database"
"GoMembership/internal/middlewares"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/internal/routes" "GoMembership/internal/routes"
"GoMembership/internal/services" "GoMembership/internal/services"
"log" "GoMembership/pkg/logger"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
func Run() { func Run() {
// cfg := config.LoadConfig()
db := database.Connect() db := database.Connect()
defer db.Close() defer db.Close()
@@ -23,10 +22,20 @@ func Run() {
userController := controllers.NewUserController(userService) userController := controllers.NewUserController(userService)
router := mux.NewRouter() router := mux.NewRouter()
routes.RegisterRoutes(router, userController) // router.Handle("/csrf-token", middlewares.GenerateCSRFTokenHandler()).Methods("GET")
log.Println("Starting server on :8080") // Apply CSRF middleware
// router.Use(middlewares.CSRFMiddleware)
router.Use(middlewares.LoggerMiddleware)
routes.RegisterRoutes(router, userController)
// create subrouter for teh authenticated area /account
// also pthprefix matches everything below /account
// accountRouter := router.PathPrefix("/account").Subrouter()
// accountRouter.Use(middlewares.AuthMiddleware)
logger.Info.Println("Starting server on :8080")
if err := http.ListenAndServe(":8080", router); err != nil { if err := http.ListenAndServe(":8080", router); err != nil {
log.Fatalf("could not start server: %v", err) logger.Error.Fatalf("could not start server: %v", err)
} }
} }

View File

@@ -3,12 +3,16 @@ package services
import ( import (
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
// "GoMembership/pkg/errors"
"crypto/rand"
"encoding/base64"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"time" "time"
) )
type UserService interface { type UserService interface {
RegisterUser(user *models.User) error RegisterUser(user *models.User) error
// AuthenticateUser(email, password string) (*models.User, error)
} }
type userService struct { type userService struct {
@@ -19,8 +23,14 @@ func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo} return &userService{repo}
} }
func (s *userService) RegisterUser(user *models.User) error { func (service *userService) RegisterUser(user *models.User) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return err
}
user.Salt = base64.StdEncoding.EncodeToString(salt)
hashedPassword, err := HashPassword(user.Password, user.Salt)
if err != nil { if err != nil {
return err return err
} }
@@ -28,5 +38,38 @@ func (s *userService) RegisterUser(user *models.User) error {
user.CreatedAt = time.Now() user.CreatedAt = time.Now()
user.UpdatedAt = time.Now() user.UpdatedAt = time.Now()
user.MandateDateSigned = time.Now() user.MandateDateSigned = time.Now()
return s.repo.CreateUser(user) return service.repo.CreateUser(user)
} }
func HashPassword(password string, salt string) (string, error) {
saltedPassword := password + salt
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(hashedPassword), nil
}
/* func (s *userService) AuthenticateUser(email, password string) (*models.User, error) {
user, err := s.repo.FindUserByEmail(email)
if err != nil {
return nil, errors.ErrUserNotFound
}
if !verifyPassword(password, user.Password, user.Salt) {
return nil, errors.ErrInvalidCredentials
}
return user, nil
}
*/
/* func verifyPassword(password string, storedPassword string, salt string) bool {
saltedPassword := password + salt
decodedStoredPassword, err := base64.StdEncoding.DecodeString(storedPassword)
if err != nil {
return false
}
err = bcrypt.CompareHashAndPassword([]byte(decodedStoredPassword), []byte(saltedPassword))
return err == nil
} */

15
internal/utils/crypto.go Normal file
View File

@@ -0,0 +1,15 @@
package utils
import (
"crypto/rand"
"encoding/base64"
)
func GenerateRandomString(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}

View File

@@ -5,6 +5,5 @@ import "errors"
var ( var (
ErrUserNotFound = errors.New("user not found") ErrUserNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email") ErrInvalidEmail = errors.New("invalid email")
// Add other custom errors here ErrInvalidCredentials = errors.New("invalid credentials: unauthorized")
) )

View File

@@ -12,7 +12,7 @@ var (
) )
func init() { func init() {
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) file, err := os.OpenFile("gomember.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -21,4 +21,3 @@ func init() {
Warning = log.New(file, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile) Warning = log.New(file, "WARNING: ", log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) Error = log.New(file, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
} }

View File

@@ -1,3 +0,0 @@
module git.stoelti.land/Alex/GoMembership
go 1.22.4

View File

@@ -1,57 +0,0 @@
// main.go
package main
import (
"strconv"
"github.com/astaxie/beego"
)
func main() {
/* This would match routes like the following:
/sum/3/5
/product/6/23
...
*/
beego.Router("/:operation/:num1:int/:num2:int", &mainController{})
beego.Run()
}
type mainController struct {
beego.Controller
}
func (c *mainController) Get() {
//Obtain the values of the route parameters defined in the route above
operation := c.Ctx.Input.Param(":operation")
num1, _ := strconv.Atoi(c.Ctx.Input.Param(":num1"))
num2, _ := strconv.Atoi(c.Ctx.Input.Param(":num2"))
//Set the values for use in the template
c.Data["operation"] = operation
c.Data["num1"] = num1
c.Data["num2"] = num2
c.TplName = "result.html"
// Perform the calculation depending on the 'operation' route parameter
switch operation {
case "sum":
c.Data["result"] = add(num1, num2)
case "product":
c.Data["result"] = multiply(num1, num2)
default:
c.TplName = "invalid-route.html"
}
}
func add(n1, n2 int) int {
return n1 + n2
}
func multiply(n1, n2 int) int {
return n1 * n2
}