started working on the backend
This commit is contained in:
95
backend/pkg/config/config.go
Normal file
95
backend/pkg/config/config.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/lib"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ReservationConfig struct {
|
||||
Expiration time.Duration
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
ConfigYaml
|
||||
LogLevel zerolog.Level
|
||||
SessionExpire time.Duration
|
||||
}
|
||||
|
||||
var Config ConfigStruct
|
||||
|
||||
type Payload struct {
|
||||
jwt.RegisteredClaims
|
||||
CustomClaims map[string]any
|
||||
}
|
||||
|
||||
func (config ConfigStruct) SignJWT(val any) (string, error) {
|
||||
valMap, err := lib.StrucToMap(val)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload := Payload{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.SessionExpire)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
CustomClaims: valMap,
|
||||
}
|
||||
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
|
||||
|
||||
return t.SignedString([]byte(config.ClientSession.JwtSignature))
|
||||
}
|
||||
|
||||
func loadConfig() ConfigStruct {
|
||||
Config := ConfigYaml{}
|
||||
|
||||
yamlFile, err := os.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error opening config-file: %q", err))
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(yamlFile)
|
||||
|
||||
dec := yaml.NewDecoder(reader)
|
||||
dec.KnownFields(true)
|
||||
err = dec.Decode(&Config)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing config-file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if logLevel, err := zerolog.ParseLevel(Config.LogLevel); err != nil {
|
||||
panic(fmt.Errorf("can't parse log-level: %v", err))
|
||||
} else {
|
||||
var configStruct ConfigStruct
|
||||
|
||||
// parse the durations
|
||||
if session_expire, err := time.ParseDuration(Config.ClientSession.Expire); err != nil {
|
||||
log.Fatalf(`Error parsing "client_session.expire": %v`, err)
|
||||
|
||||
// parse the templates
|
||||
} else {
|
||||
configStruct = ConfigStruct{
|
||||
ConfigYaml: Config,
|
||||
LogLevel: logLevel,
|
||||
SessionExpire: session_expire,
|
||||
}
|
||||
}
|
||||
|
||||
return configStruct
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
Config = loadConfig()
|
||||
}
|
||||
75
backend/pkg/config/configYAML.go
Normal file
75
backend/pkg/config/configYAML.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var CONFIG_PATH = "config.yaml"
|
||||
|
||||
type ConfigYaml struct {
|
||||
LogLevel string `yaml:"log_level"`
|
||||
Database struct {
|
||||
Host string `yaml:"host"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
Database string `yaml:"database"`
|
||||
} `yaml:"database"`
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"server"`
|
||||
ClientSession struct {
|
||||
JwtSignature string `yaml:"jwt_signature"`
|
||||
Expire string `yaml:"expire"`
|
||||
} `yaml:"client_session"`
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
Expiration time.Duration
|
||||
Purge time.Duration
|
||||
}
|
||||
|
||||
var YamlConfig ConfigYaml
|
||||
|
||||
func _loadConfig() ConfigYaml {
|
||||
config := ConfigYaml{}
|
||||
|
||||
yamlFile, err := os.ReadFile(CONFIG_PATH)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error opening config-file: %v", err))
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(yamlFile)
|
||||
|
||||
dec := yaml.NewDecoder(reader)
|
||||
dec.KnownFields(true)
|
||||
err = dec.Decode(&config)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing config-file: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func WriteConfig() {
|
||||
buf := bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
// Can set default indent here on the encoder
|
||||
if err := enc.Encode(&YamlConfig); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
if err := os.WriteFile(CONFIG_PATH, buf.Bytes(), 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
YamlConfig = _loadConfig()
|
||||
}
|
||||
30
backend/pkg/db/assignments/assignments.go
Normal file
30
backend/pkg/db/assignments/assignments.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package assignments
|
||||
|
||||
import (
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
)
|
||||
|
||||
type assignments map[string]string
|
||||
|
||||
type assignemntDB struct {
|
||||
TaskName string `db:"taskName"`
|
||||
UserName string `db:"userName"`
|
||||
}
|
||||
|
||||
func Event(eventID int) (assignments, error) {
|
||||
// get the assignments from the database
|
||||
var assignmentRows []assignemntDB
|
||||
|
||||
if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, TASKS.text AS taskName FROM USER_ASSIGNMENTS JOIN USERS ON USER_ASSIGNMENTS.userName = USERS.name LEFT JOIN TASKS ON USER_ASSIGNMENTS.taskID = TASKS.id WHERE USER_ASSIGNMENTS.eventID = ?", eventID); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// transform the rows into the returned map
|
||||
eventAssignments := assignments{}
|
||||
|
||||
for _, assignment := range assignmentRows {
|
||||
eventAssignments[assignment.TaskName] = assignment.UserName
|
||||
}
|
||||
|
||||
return eventAssignments, nil
|
||||
}
|
||||
}
|
||||
59
backend/pkg/db/availabilites/availabilities.go
Normal file
59
backend/pkg/db/availabilites/availabilities.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package availabilites
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cache "github.com/jfarleyx/go-simple-cache"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
)
|
||||
|
||||
type availabilitesDB struct {
|
||||
Id int `db:"id"`
|
||||
Text string `db:"text"`
|
||||
Disabled bool `db:"disabled"`
|
||||
}
|
||||
|
||||
type Availability struct {
|
||||
Text string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
var c *cache.Cache
|
||||
|
||||
func Keys() (map[int]Availability, error) {
|
||||
if availabilities, hit := c.Get("availabilites"); !hit {
|
||||
refresh()
|
||||
|
||||
return nil, fmt.Errorf("availabilites not stored cached")
|
||||
} else {
|
||||
return availabilities.(map[int]Availability), nil
|
||||
}
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
// get the availabilitesRaw from the database
|
||||
var availabilitesRaw []availabilitesDB
|
||||
|
||||
if err := db.DB.Select(&availabilitesRaw, "SELECT * FROM AVAILABILITIES"); err == nil {
|
||||
// convert the result in a map
|
||||
availabilites := map[int]Availability{}
|
||||
|
||||
for _, a := range availabilitesRaw {
|
||||
availabilites[a.Id] = Availability{
|
||||
Text: a.Text,
|
||||
Disabled: a.Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("availabilites", availabilites)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
c = cache.New(24 * time.Hour)
|
||||
|
||||
c.OnExpired(refresh)
|
||||
|
||||
refresh()
|
||||
}
|
||||
36
backend/pkg/db/availabilites/userAvailabilities.go
Normal file
36
backend/pkg/db/availabilites/userAvailabilities.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package availabilites
|
||||
|
||||
import (
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db/users"
|
||||
)
|
||||
|
||||
type eventAvailabilites struct {
|
||||
userName string `db:"userName"`
|
||||
AvailabilityID int `db:"availabilityID"`
|
||||
}
|
||||
|
||||
func Event(eventID int) (map[string]string, error) {
|
||||
// get the availabilites for the event
|
||||
var availabilitesRows []eventAvailabilites
|
||||
|
||||
if err := db.DB.Select(&availabilitesRows, "SELECT (userID, availabilityID) FROM USER_AVAILABILITES WHERE eventID = ?", eventID); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// transform the result into a map
|
||||
eventAvailabilities := map[string]string{}
|
||||
|
||||
// get the availabilites
|
||||
if availabilitesMap, err := Keys(); err != nil {
|
||||
return nil, err
|
||||
} else if usersMap, err := users.Get(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
for _, a := range availabilitesRows {
|
||||
eventAvailabilities[usersMap[a.userName].Name] = availabilitesMap[a.AvailabilityID].Text
|
||||
}
|
||||
|
||||
return eventAvailabilities, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
225
backend/pkg/db/db.go
Normal file
225
backend/pkg/db/db.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
||||
)
|
||||
|
||||
var logger = _logger.Logger
|
||||
var config = _config.Config
|
||||
|
||||
// connection to database
|
||||
var DB *sqlx.DB
|
||||
|
||||
func init() {
|
||||
// setup the database-connection
|
||||
sqlConfig := mysql.Config{
|
||||
AllowNativePasswords: true,
|
||||
Net: "tcp",
|
||||
User: config.Database.User,
|
||||
Passwd: config.Database.Password,
|
||||
Addr: config.Database.Host,
|
||||
DBName: config.Database.Database,
|
||||
}
|
||||
|
||||
// connect to the database
|
||||
DB = sqlx.MustOpen("mysql", sqlConfig.FormatDSN())
|
||||
DB.SetMaxIdleConns(10)
|
||||
DB.SetMaxIdleConns(100)
|
||||
DB.SetConnMaxLifetime(time.Minute)
|
||||
|
||||
}
|
||||
|
||||
// query the database
|
||||
func SelectOld[T any](table string, where string, args ...any) ([]T, error) {
|
||||
// validate columns against struct T
|
||||
tType := reflect.TypeOf(new(T)).Elem()
|
||||
columns := make([]string, tType.NumField())
|
||||
|
||||
validColumns := make(map[string]any)
|
||||
for ii := 0; ii < tType.NumField(); ii++ {
|
||||
field := tType.Field(ii)
|
||||
validColumns[strings.ToLower(field.Name)] = struct{}{}
|
||||
columns[ii] = strings.ToLower(field.Name)
|
||||
}
|
||||
|
||||
for _, col := range columns {
|
||||
if _, ok := validColumns[strings.ToLower(col)]; !ok {
|
||||
return nil, fmt.Errorf("invalid column: %s for struct type %T", col, new(T))
|
||||
}
|
||||
}
|
||||
|
||||
// create the query
|
||||
completeQuery := fmt.Sprintf("SELECT %s FROM %s", strings.Join(columns, ", "), table)
|
||||
|
||||
if where != "" && where != "*" {
|
||||
completeQuery = fmt.Sprintf("%s WHERE %s", completeQuery, where)
|
||||
}
|
||||
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
|
||||
if len(args) > 0 {
|
||||
DB.Ping()
|
||||
|
||||
rows, err = DB.Query(completeQuery, args...)
|
||||
} else {
|
||||
DB.Ping()
|
||||
|
||||
rows, err = DB.Query(completeQuery)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error().Msgf("database access failed with error %v", err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
results := []T{}
|
||||
|
||||
for rows.Next() {
|
||||
var lineResult T
|
||||
|
||||
scanArgs := make([]any, len(columns))
|
||||
v := reflect.ValueOf(&lineResult).Elem()
|
||||
|
||||
for ii, col := range columns {
|
||||
field := v.FieldByName(col)
|
||||
|
||||
if field.IsValid() && field.CanSet() {
|
||||
scanArgs[ii] = field.Addr().Interface()
|
||||
} else {
|
||||
logger.Warn().Msgf("Field %s not found in struct %T", col, lineResult)
|
||||
scanArgs[ii] = new(any) // save dummy value
|
||||
}
|
||||
}
|
||||
|
||||
// scan the row into the struct
|
||||
if err := rows.Scan(scanArgs...); err != nil {
|
||||
logger.Warn().Msgf("Scan-error: %v", err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results = append(results, lineResult)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
logger.Error().Msgf("rows-error: %v", err)
|
||||
return nil, err
|
||||
} else {
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
|
||||
// insert data intot the databse
|
||||
func Insert(table string, vals any) error {
|
||||
// extract columns from vals
|
||||
v := reflect.ValueOf(vals)
|
||||
t := v.Type()
|
||||
|
||||
columns := make([]string, t.NumField())
|
||||
values := make([]any, t.NumField())
|
||||
|
||||
for ii := 0; ii < t.NumField(); ii++ {
|
||||
fieldValue := v.Field(ii)
|
||||
|
||||
field := t.Field(ii)
|
||||
|
||||
columns[ii] = strings.ToLower(field.Name)
|
||||
values[ii] = fieldValue.Interface()
|
||||
}
|
||||
|
||||
placeholders := strings.Repeat(("?, "), len(columns))
|
||||
placeholders = strings.TrimSuffix(placeholders, ", ")
|
||||
|
||||
completeQuery := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", table, strings.Join(columns, ", "), placeholders)
|
||||
|
||||
_, err := DB.Exec(completeQuery, values...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// update data in the database
|
||||
func Update(table string, set, where any) error {
|
||||
setV := reflect.ValueOf(set)
|
||||
setT := setV.Type()
|
||||
|
||||
setColumns := make([]string, setT.NumField())
|
||||
setValues := make([]any, setT.NumField())
|
||||
|
||||
for ii := 0; ii < setT.NumField(); ii++ {
|
||||
fieldValue := setV.Field(ii)
|
||||
|
||||
field := setT.Field(ii)
|
||||
|
||||
setColumns[ii] = strings.ToLower(field.Name) + " = ?"
|
||||
setValues[ii] = fieldValue.Interface()
|
||||
}
|
||||
|
||||
whereV := reflect.ValueOf(where)
|
||||
whereT := whereV.Type()
|
||||
|
||||
whereColumns := make([]string, whereT.NumField())
|
||||
whereValues := make([]any, whereT.NumField())
|
||||
|
||||
for ii := 0; ii < whereT.NumField(); ii++ {
|
||||
fieldValue := whereV.Field(ii)
|
||||
|
||||
// skip empty (zero) values
|
||||
if !fieldValue.IsZero() {
|
||||
field := whereT.Field(ii)
|
||||
|
||||
whereColumns[ii] = strings.ToLower(field.Name) + " = ?"
|
||||
whereValues[ii] = fmt.Sprint(fieldValue.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
sets := strings.Join(setColumns, ", ")
|
||||
wheres := strings.Join(whereColumns, " AND ")
|
||||
|
||||
placeholderValues := append(setValues, whereValues...)
|
||||
|
||||
completeQuery := fmt.Sprintf("UPDATE %s SET %s WHERE %s", table, sets, wheres)
|
||||
|
||||
_, err := DB.Exec(completeQuery, placeholderValues...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// remove data from the database
|
||||
func Delete(table string, vals any) error {
|
||||
// extract columns from vals
|
||||
v := reflect.ValueOf(vals)
|
||||
t := v.Type()
|
||||
|
||||
columns := make([]string, t.NumField())
|
||||
values := make([]any, t.NumField())
|
||||
|
||||
for ii := 0; ii < t.NumField(); ii++ {
|
||||
fieldValue := v.Field(ii)
|
||||
|
||||
// skip empty (zero) values
|
||||
if !fieldValue.IsZero() {
|
||||
field := t.Field(ii)
|
||||
|
||||
columns[ii] = strings.ToLower(field.Name) + " = ?"
|
||||
values[ii] = fmt.Sprint(fieldValue.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
completeQuery := fmt.Sprintf("DELETE FROM %s WHERE %s", table, strings.Join(columns, ", "))
|
||||
|
||||
_, err := DB.Exec(completeQuery, values...)
|
||||
|
||||
return err
|
||||
}
|
||||
48
backend/pkg/db/events/events.go
Normal file
48
backend/pkg/db/events/events.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db/assignments"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
eventDataDB
|
||||
Tasks []string
|
||||
Assignments map[string]string
|
||||
}
|
||||
|
||||
type eventDataDB struct {
|
||||
Id int `db:"id"`
|
||||
Date string `db:"date"`
|
||||
Description string `db:"description"`
|
||||
}
|
||||
|
||||
// transform the database-entry to an Event
|
||||
func (e *eventDataDB) Event() (Event, error) {
|
||||
// get the availabilites associated with the event
|
||||
if assignemnts, err := assignments.Event(e.Id); err != nil {
|
||||
return Event{}, err
|
||||
} else {
|
||||
return Event{
|
||||
eventDataDB: *e,
|
||||
Assignments: assignemnts,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// get all the event ids
|
||||
func All() (map[int]eventDataDB, error) {
|
||||
var dbRows []eventDataDB
|
||||
|
||||
if err := db.DB.Select(&dbRows, "SELECT * FROM EVENTS"); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
eventsMap := map[int]eventDataDB{}
|
||||
|
||||
for _, idRow := range dbRows {
|
||||
eventsMap[idRow.Id] = idRow
|
||||
}
|
||||
|
||||
return eventsMap, nil
|
||||
}
|
||||
}
|
||||
59
backend/pkg/db/tasks/tasks.go
Normal file
59
backend/pkg/db/tasks/tasks.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cache "github.com/jfarleyx/go-simple-cache"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
)
|
||||
|
||||
type tasksDB struct {
|
||||
Id int `db:"id"`
|
||||
Text string `db:"text"`
|
||||
Disabled bool `db:"disabled"`
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Text string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
var c *cache.Cache
|
||||
|
||||
func Get() (map[int]Task, error) {
|
||||
if tasks, hit := c.Get("tasks"); !hit {
|
||||
refresh()
|
||||
|
||||
return nil, fmt.Errorf("tasks not stored cached")
|
||||
} else {
|
||||
return tasks.(map[int]Task), nil
|
||||
}
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
// get the tasksRaw from the database
|
||||
var tasksRaw []tasksDB
|
||||
|
||||
if err := db.DB.Select(&tasksRaw, "SELECT * FROM TASKS"); err == nil {
|
||||
// convert the result in a map
|
||||
tasks := map[int]Task{}
|
||||
|
||||
for _, a := range tasksRaw {
|
||||
tasks[a.Id] = Task{
|
||||
Text: a.Text,
|
||||
Disabled: a.Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("tasks", tasks)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
c = cache.New(24 * time.Hour)
|
||||
|
||||
c.OnExpired(refresh)
|
||||
|
||||
refresh()
|
||||
}
|
||||
52
backend/pkg/db/users/users.go
Normal file
52
backend/pkg/db/users/users.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cache "github.com/jfarleyx/go-simple-cache"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string `db:"text"`
|
||||
Password []byte `db:"password"`
|
||||
TokenID string `db:"tokenID"`
|
||||
Admin bool `db:"disabled"`
|
||||
}
|
||||
|
||||
var c *cache.Cache
|
||||
|
||||
func Get() (map[string]User, error) {
|
||||
if users, hit := c.Get("users"); !hit {
|
||||
refresh()
|
||||
|
||||
return nil, fmt.Errorf("users not stored cached")
|
||||
} else {
|
||||
return users.(map[string]User), nil
|
||||
}
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
// get the usersRaw from the database
|
||||
var usersRaw []User
|
||||
|
||||
if err := db.DB.Select(&usersRaw, "SELECT * FROM USERS"); err == nil {
|
||||
// convert the result in a map
|
||||
users := map[string]User{}
|
||||
|
||||
for _, user := range usersRaw {
|
||||
users[user.Name] = user
|
||||
}
|
||||
|
||||
c.Set("users", users)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
c = cache.New(24 * time.Hour)
|
||||
|
||||
c.OnExpired(refresh)
|
||||
|
||||
refresh()
|
||||
}
|
||||
30
backend/pkg/lib/lib.go
Normal file
30
backend/pkg/lib/lib.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func StrucToMap(data any) (map[string]any, error) {
|
||||
result := make(map[string]any)
|
||||
|
||||
v := reflect.ValueOf(data)
|
||||
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("expected a struct but got %T", data)
|
||||
}
|
||||
|
||||
for ii := 0; ii < v.NumField(); ii++ {
|
||||
field := v.Type().Field(ii)
|
||||
value := v.Field(ii)
|
||||
|
||||
// skip unexported fields
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
result[field.Tag.Get("json")] = value.Interface()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
70
backend/pkg/logger/logger.go
Normal file
70
backend/pkg/logger/logger.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||
"github.com/rs/zerolog"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var Logger zerolog.Logger
|
||||
|
||||
type specificLevelWriter struct {
|
||||
io.Writer
|
||||
Level zerolog.Level
|
||||
}
|
||||
|
||||
func (w specificLevelWriter) WriteLevel(l zerolog.Level, p []byte) (int, error) {
|
||||
if l >= w.Level {
|
||||
return w.Write(p)
|
||||
} else {
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// try to set the log-level
|
||||
zerolog.SetGlobalLevel(config.Config.LogLevel)
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
// create the console output
|
||||
outputConsole := zerolog.ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: time.DateTime,
|
||||
FormatLevel: func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||
},
|
||||
FormatFieldName: func(i interface{}) string {
|
||||
return fmt.Sprintf("%s", i)
|
||||
},
|
||||
NoColor: true,
|
||||
}
|
||||
|
||||
// create the logfile output
|
||||
outputLog := &lumberjack.Logger{
|
||||
Filename: "logs/backend.log",
|
||||
MaxAge: 7,
|
||||
LocalTime: true,
|
||||
}
|
||||
|
||||
// create a multi-output-writer
|
||||
multi := zerolog.MultiLevelWriter(
|
||||
specificLevelWriter{
|
||||
Writer: outputConsole,
|
||||
Level: config.Config.LogLevel,
|
||||
},
|
||||
specificLevelWriter{
|
||||
Writer: outputLog,
|
||||
Level: config.Config.LogLevel,
|
||||
},
|
||||
)
|
||||
|
||||
// create a logger-instance
|
||||
Logger = zerolog.New(multi).With().Timestamp().Logger()
|
||||
|
||||
}
|
||||
33
backend/pkg/router/events.go
Normal file
33
backend/pkg/router/events.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db/events"
|
||||
)
|
||||
|
||||
func getEvents(c *fiber.Ctx) responseMessage {
|
||||
response := responseMessage{}
|
||||
|
||||
// get all eventRows
|
||||
if eventRows, err := events.All(); err != nil {
|
||||
response.Status = fiber.StatusInternalServerError
|
||||
|
||||
logger.Error().Msgf("events retrieving failed: %v", err)
|
||||
} else {
|
||||
// get the data for all the allEvents
|
||||
allEvents := []events.Event{}
|
||||
|
||||
for _, eventRow := range eventRows {
|
||||
if e, err := eventRow.Event(); err != nil {
|
||||
logger.Error().Msgf("error while populating event with id = %d: %v", eventRow.Id, err)
|
||||
} else {
|
||||
allEvents = append(allEvents, e)
|
||||
}
|
||||
|
||||
// response.Data = struct{ Events []events.Event }{Events: allEvents}
|
||||
response.Data = allEvents
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
80
backend/pkg/router/login.go
Normal file
80
backend/pkg/router/login.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
)
|
||||
|
||||
type UserLogin struct {
|
||||
UserID int `json:"userID"`
|
||||
Name string `json:"name"`
|
||||
LoggedIn bool `json:"loggedIn"`
|
||||
}
|
||||
|
||||
// handle welcome-messages from clients
|
||||
func handleWelcome(c *fiber.Ctx) error {
|
||||
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||
|
||||
response := responseMessage{}
|
||||
response.Data = UserLogin{
|
||||
LoggedIn: false,
|
||||
}
|
||||
|
||||
if ok, err := checkUser(c); err != nil {
|
||||
response.Status = fiber.StatusInternalServerError
|
||||
|
||||
logger.Warn().Msgf("can't check user: %v", err)
|
||||
} else if !ok {
|
||||
response.Status = fiber.StatusNoContent
|
||||
} else {
|
||||
if uid, _, err := extractJWT(c); err != nil {
|
||||
response.Status = fiber.StatusBadRequest
|
||||
|
||||
logger.Error().Msgf("can't extract JWT: %v", err)
|
||||
} else {
|
||||
if users, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", strconv.Itoa(uid)); err != nil {
|
||||
response.Status = fiber.StatusInternalServerError
|
||||
|
||||
logger.Error().Msgf("can't get users from database: %v", err)
|
||||
} else {
|
||||
if len(users) != 1 {
|
||||
response.Status = fiber.StatusForbidden
|
||||
response.Message = "unknown user"
|
||||
|
||||
removeSessionCookie(c)
|
||||
} else {
|
||||
user := users[0]
|
||||
|
||||
response.Data = UserLogin{
|
||||
UserID: user.UserID,
|
||||
Name: user.Name,
|
||||
LoggedIn: true,
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug().Msgf("welcomed user with uid = %v", uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.send(c)
|
||||
}
|
||||
|
||||
func handleLogin(c *fiber.Ctx) error {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
// handles logout-requests
|
||||
func handleLogout(c *fiber.Ctx) error {
|
||||
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||
|
||||
removeSessionCookie(c)
|
||||
|
||||
return responseMessage{
|
||||
Data: UserLogin{
|
||||
LoggedIn: false,
|
||||
},
|
||||
}.send(c)
|
||||
}
|
||||
221
backend/pkg/router/router.go
Normal file
221
backend/pkg/router/router.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
||||
)
|
||||
|
||||
var logger = _logger.Logger
|
||||
var config = _config.Config
|
||||
|
||||
var app *fiber.App
|
||||
|
||||
// general message for REST-responses
|
||||
type responseMessage struct {
|
||||
Status int
|
||||
Message string
|
||||
Data any
|
||||
}
|
||||
|
||||
// answer the client request with the response-message
|
||||
func (result responseMessage) send(c *fiber.Ctx) error {
|
||||
// if the status-code is in the error-region, return an error
|
||||
if result.Status >= 400 {
|
||||
// if available, include the message
|
||||
if result.Message != "" {
|
||||
return fiber.NewError(result.Status, result.Message)
|
||||
} else {
|
||||
return fiber.NewError(result.Status)
|
||||
}
|
||||
} else {
|
||||
// if there is data, send it as JSON
|
||||
if result.Data != nil {
|
||||
c.JSON(result.Data)
|
||||
|
||||
// if there is a message, send it instead
|
||||
} else if result.Message != "" {
|
||||
c.SendString(result.Message)
|
||||
}
|
||||
|
||||
return c.SendStatus(result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// setup fiber
|
||||
app = fiber.New(fiber.Config{
|
||||
AppName: "johannes-pv",
|
||||
DisableStartupMessage: true,
|
||||
})
|
||||
|
||||
// map with the individual methods
|
||||
handleMethods := map[string]func(path string, handlers ...func(*fiber.Ctx) error) fiber.Router{
|
||||
"GET": app.Get,
|
||||
"POST": app.Post,
|
||||
"PATCH": app.Patch,
|
||||
"DELETE": app.Delete,
|
||||
}
|
||||
|
||||
// map with the individual registered endpoints
|
||||
endpoints := map[string]map[string]func(*fiber.Ctx) responseMessage{
|
||||
"GET": {"events": getEvents},
|
||||
"POST": {},
|
||||
"PATCH": {},
|
||||
"DELETE": {},
|
||||
}
|
||||
|
||||
// handle specific requests special
|
||||
app.Get("/api/welcome", handleWelcome)
|
||||
app.Post("/api/login", handleLogin)
|
||||
app.Get("/api/logout", handleLogout)
|
||||
|
||||
// register the registered endpoints
|
||||
for method, handlers := range endpoints {
|
||||
for address, handler := range handlers {
|
||||
handleMethods[method]("/api/"+address, func(c *fiber.Ctx) error {
|
||||
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||
|
||||
return handler(c).send(c)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Listen() {
|
||||
// start the server
|
||||
err := app.Listen(fmt.Sprintf(":%d", config.Server.Port))
|
||||
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
func setSessionCookie(c *fiber.Ctx, jwt *string) {
|
||||
var value string
|
||||
|
||||
if jwt == nil {
|
||||
value = c.Cookies("session")
|
||||
} else {
|
||||
value = *jwt
|
||||
}
|
||||
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session",
|
||||
Value: value,
|
||||
HTTPOnly: true,
|
||||
SameSite: "strict",
|
||||
MaxAge: int(config.SessionExpire.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
// removes the session-coockie from a request
|
||||
func removeSessionCookie(c *fiber.Ctx) {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: "session",
|
||||
Value: "",
|
||||
HTTPOnly: true,
|
||||
SameSite: "strict",
|
||||
Expires: time.Unix(0, 0),
|
||||
})
|
||||
}
|
||||
|
||||
// payload of the JSON webtoken
|
||||
type JWTPayload struct {
|
||||
UserID int `json:"userID"`
|
||||
TokenID string `json:"tokenID"`
|
||||
}
|
||||
|
||||
// complete JSON webtoken
|
||||
type JWT struct {
|
||||
_config.Payload
|
||||
CustomClaims JWTPayload
|
||||
}
|
||||
|
||||
// extracts the json webtoken from the request
|
||||
//
|
||||
// @returns (userID, tokenID, error)
|
||||
func extractJWT(c *fiber.Ctx) (int, string, error) {
|
||||
// get the session-cookie
|
||||
cookie := c.Cookies("session")
|
||||
|
||||
token, err := jwt.ParseWithClaims(cookie, &JWT{}, func(token *jwt.Token) (any, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected JWT signing method: %v", token.Header["alg"])
|
||||
}
|
||||
|
||||
return []byte(config.ClientSession.JwtSignature), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return -1, "", err
|
||||
}
|
||||
|
||||
// extract the claims from the JWT
|
||||
if claims, ok := token.Claims.(*JWT); ok && token.Valid {
|
||||
return claims.CustomClaims.UserID, claims.CustomClaims.TokenID, nil
|
||||
} else {
|
||||
return -1, "", fmt.Errorf("invalid JWT")
|
||||
}
|
||||
}
|
||||
|
||||
// user-entry in the database
|
||||
type UserDB struct {
|
||||
UserID int `json:"userID"`
|
||||
Name string `json:"name"`
|
||||
Password []byte `json:"password"`
|
||||
Admin bool `json:"admin"`
|
||||
TokenID string `json:"tokenID"`
|
||||
}
|
||||
|
||||
// checks wether the request is from a valid user
|
||||
func checkUser(c *fiber.Ctx) (bool, error) {
|
||||
uid, tid, err := extractJWT(c)
|
||||
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// retrieve the user from the database
|
||||
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if exactly one user came back and the tID is valid, the user is authorized
|
||||
if len(response) == 1 && response[0].TokenID == tid {
|
||||
// reset the expiration of the cookie
|
||||
setSessionCookie(c, nil)
|
||||
|
||||
return true, err
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// checks wether the request is from the admin
|
||||
func checkAdmin(c *fiber.Ctx) (bool, error) {
|
||||
uid, tokenID, err := extractJWT(c)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// retrieve the user from the database
|
||||
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if exactly one user came back and its name is "admin", the user is the admin
|
||||
if len(response) != 1 {
|
||||
return false, fmt.Errorf("user doesn't exist")
|
||||
} else {
|
||||
return response[0].Name == "admin" && response[0].TokenID == tokenID, err
|
||||
}
|
||||
}
|
||||
13
backend/pkg/router/user.go
Normal file
13
backend/pkg/router/user.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package router
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// hashes a password
|
||||
func hashPassword(password string) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
}
|
||||
|
||||
// validates a password against the password-rules
|
||||
func validatePassword(password string) bool {
|
||||
return len(password) >= 12 && len(password) <= 64
|
||||
}
|
||||
Reference in New Issue
Block a user