started "real work"

This commit is contained in:
z1glr
2025-01-10 14:06:53 +00:00
parent e2aa65b416
commit 45f600268f
30 changed files with 9811 additions and 8584 deletions

View File

@@ -4,18 +4,18 @@ import (
"github.com/johannesbuehl/golunteer/backend/pkg/db"
)
type assignments map[string]string
type assignments map[string]*string
type assignemntDB struct {
TaskName string `db:"taskName"`
UserName string `db:"userName"`
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 {
if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, TASKS.text AS taskName FROM USER_ASSIGNMENTS LEFT 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

View File

@@ -3,46 +3,70 @@ package events
import (
"github.com/johannesbuehl/golunteer/backend/pkg/db"
"github.com/johannesbuehl/golunteer/backend/pkg/db/assignments"
"github.com/johannesbuehl/golunteer/backend/pkg/logger"
)
type Event struct {
type EventWithAssignment struct {
eventDataDB
Tasks []string
Assignments map[string]string
Tasks map[string]*string `json:"tasks"`
}
type eventDataDB struct {
Id int `db:"id"`
Date string `db:"date"`
Description string `db:"description"`
Id int `db:"id" json:"id"`
Date string `db:"date" json:"date"`
Description string `db:"description" json:"description"`
}
// transform the database-entry to an Event
func (e *eventDataDB) Event() (Event, error) {
func (e *eventDataDB) Event() (EventWithAssignment, error) {
// get the availabilites associated with the event
if assignemnts, err := assignments.Event(e.Id); err != nil {
return Event{}, err
return EventWithAssignment{}, err
} else {
return Event{
return EventWithAssignment{
eventDataDB: *e,
Assignments: assignemnts,
Tasks: assignemnts,
}, nil
}
}
// get all the event ids
func All() (map[int]eventDataDB, error) {
func All() ([]eventDataDB, error) {
var dbRows []eventDataDB
if err := db.DB.Select(&dbRows, "SELECT * FROM EVENTS"); err != nil {
if err := db.DB.Select(&dbRows, "SELECT *, DATE_FORMAT(date, '%Y-%m-%dT%H:%i:%s') as date FROM EVENTS"); err != nil {
return nil, err
} else {
eventsMap := map[int]eventDataDB{}
for _, idRow := range dbRows {
eventsMap[idRow.Id] = idRow
}
return eventsMap, nil
return dbRows, nil
}
}
func WithAssignments() ([]EventWithAssignment, error) {
// get all events
if eventsDB, err := All(); err != nil {
return nil, err
} else {
events := make([]EventWithAssignment, len(eventsDB))
for ii, e := range eventsDB {
if ev, err := e.Event(); err != nil {
logger.Logger.Error().Msgf("can't get assignments for event with id = %d: %v", e.Id, err)
} else {
events[ii] = ev
}
}
return events, nil
}
}
func UserPending(userName string) (int, error) {
var result struct {
Count int `db:"count(*)"`
}
if err := db.DB.QueryRowx("SELECT count(*) FROM USERS WHERE name = ? AND name NOT IN (SELECT userName FROM USER_AVAILABILITIES)", userName).StructScan(&result); err != nil {
return 0, err
} else {
return result.Count, nil
}
}

View File

@@ -5,28 +5,29 @@ import (
"github.com/johannesbuehl/golunteer/backend/pkg/db/events"
)
func getEvents(c *fiber.Ctx) responseMessage {
func getEventsAssignments(args HandlerArgs) responseMessage {
response := responseMessage{}
// get all eventRows
if eventRows, err := events.All(); err != nil {
if events, err := events.WithAssignments(); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Error().Msgf("events retrieving failed: %v", err)
logger.Error().Msgf("can't retrieve events with assignments: %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
}
response.Data = events
}
return response
}
func getEventsUserPending(args HandlerArgs) responseMessage {
response := responseMessage{}
if count, err := events.UserPending(args.User.UserName); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Warn().Msgf("can't query database for users %q pending events: %v", args.User.UserName, err)
} else {
response.Data = count
}
return response

View File

@@ -1,67 +1,98 @@
package router
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/johannesbuehl/golunteer/backend/pkg/db"
"golang.org/x/crypto/bcrypt"
)
type UserLogin struct {
UserName string `json:"userName"`
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,
response.Data = UserChecked{
Admin: false,
}
if ok, err := checkUser(c); err != nil {
if user, err := checkUser(c); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Warn().Msgf("can't check user: %v", err)
} else if !ok {
} else if user == nil {
response.Status = fiber.StatusNoContent
logger.Debug().Msgf("user not authorized")
} 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{
UserName: user.UserName,
LoggedIn: true,
}
}
logger.Debug().Msgf("welcomed user with uid = %v", uid)
}
response.Data = UserChecked{
UserName: user.UserName,
Admin: user.Admin,
}
logger.Debug().Msgf("welcomed user %q", user.UserName)
}
return response.send(c)
}
const messageWrongLogin = "Unkown user or wrong password"
func handleLogin(c *fiber.Ctx) error {
panic("not implemented yet")
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
// extract username and password from the request
requestBody := struct {
Username string `json:"userName" validate:"required"`
Password string `json:"password" validate:"required"`
}{}
var response responseMessage
if err := c.BodyParser(&requestBody); err != nil {
logger.Debug().Msgf("can't parse login-body: %v", err)
response.Status = fiber.StatusBadRequest
// validate the body
} else if err := validate.Struct(requestBody); err != nil {
logger.Warn().Msgf("can't parse login-body: %v", err)
} else {
// query the database for the user
var result userDB
if err := db.DB.QueryRowx("SELECT password, admin, tokenID FROM USERS WHERE name = ?", requestBody.Username).StructScan(&result); err != nil {
response.Status = fiber.StatusForbidden
response.Message = messageWrongLogin
logger.Info().Msgf("can't get user with name = %q from database", requestBody.Username)
} else {
// hash the password
if bcrypt.CompareHashAndPassword(result.Password, []byte(requestBody.Password)) != nil {
response.Status = fiber.StatusForbidden
logger.Info().Msgf("login denied: wrong password for user with name = %q", requestBody.Username)
} else {
// password is correct -> generate the JWT
if jwt, err := config.SignJWT(JWTPayload{
UserID: requestBody.Username,
TokenID: result.TokenID,
}); err != nil {
response.Status = fiber.StatusInternalServerError
logger.Error().Msgf("can't create JWT: %v", err)
} else {
setSessionCookie(c, &jwt)
response.Data = UserChecked{
UserName: requestBody.Username,
Admin: true,
}
logger.Debug().Msgf("user %q logged in", requestBody.Username)
}
}
}
}
return response.send(c)
}
// handles logout-requests
@@ -70,9 +101,5 @@ func handleLogout(c *fiber.Ctx) error {
removeSessionCookie(c)
return responseMessage{
Data: UserLogin{
LoggedIn: false,
},
}.send(c)
return responseMessage{}.send(c)
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
@@ -11,6 +12,8 @@ import (
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
)
var validate *validator.Validate
var logger = _logger.Logger
var config = _config.Config
@@ -47,7 +50,14 @@ func (result responseMessage) send(c *fiber.Ctx) error {
}
}
type HandlerArgs struct {
C *fiber.Ctx
User UserChecked
}
func init() {
validate = validator.New()
// setup fiber
app = fiber.New(fiber.Config{
AppName: "johannes-pv",
@@ -63,8 +73,8 @@ func init() {
}
// map with the individual registered endpoints
endpoints := map[string]map[string]func(*fiber.Ctx) responseMessage{
"GET": {"events": getEvents},
endpoints := map[string]map[string]func(HandlerArgs) responseMessage{
"GET": {"events/assignments": getEventsAssignments, "events/user/pending": getEventsUserPending},
"POST": {},
"PATCH": {},
"DELETE": {},
@@ -81,7 +91,28 @@ func init() {
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)
var response responseMessage
if user, err := checkUser(c); err != nil {
response = responseMessage{
Status: fiber.StatusBadRequest,
}
logger.Error().Msgf("can't check user: %v", err)
} else if user == nil {
response = responseMessage{
Status: fiber.StatusNoContent,
}
logger.Log().Msgf("user not authorized")
} else {
response = handler(HandlerArgs{
C: c,
User: *user,
})
}
return response.send(c)
})
}
}
@@ -125,7 +156,7 @@ func removeSessionCookie(c *fiber.Ctx) {
// payload of the JSON webtoken
type JWTPayload struct {
UserID int `json:"userID"`
UserID string `json:"userID"`
TokenID string `json:"tokenID"`
}
@@ -138,7 +169,7 @@ type JWT struct {
// extracts the json webtoken from the request
//
// @returns (userID, tokenID, error)
func extractJWT(c *fiber.Ctx) (int, string, error) {
func extractJWT(c *fiber.Ctx) (string, string, error) {
// get the session-cookie
cookie := c.Cookies("session")
@@ -151,70 +182,57 @@ func extractJWT(c *fiber.Ctx) (int, string, error) {
})
if err != nil {
return -1, "", err
return "", "", 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")
return "", "", fmt.Errorf("invalid JWT")
}
}
// user-entry in the database
type UserDB struct {
UserName string `json:"userName"`
Password []byte `json:"password"`
Admin bool `json:"admin"`
TokenID string `json:"tokenID"`
type userDB struct {
UserName string `db:"userName"`
Password []byte `db:"password"`
Admin bool `db:"admin"`
TokenID string `db:"tokenID"`
}
type UserChecked struct {
UserName string `json:"userName" db:"userName"`
Admin bool `json:"admin" db:"admin"`
}
// checks wether the request is from a valid user
func checkUser(c *fiber.Ctx) (bool, error) {
uid, tid, err := extractJWT(c)
func checkUser(c *fiber.Ctx) (*UserChecked, error) {
userName, tokenID, err := extractJWT(c)
if err != nil {
return false, nil
return nil, nil
}
var dbResult struct {
TokenID string `db:"tokenID"`
Admin bool `db:"admin"`
}
// retrieve the user from the database
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
if err := db.DB.QueryRowx("SELECT tokenID, admin FROM USERS WHERE name = ?", userName).StructScan(&dbResult); err != nil {
return nil, err
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 {
// if the tokenID is valid, the user is authorized
} else if dbResult.TokenID != tokenID {
return nil, err
} else {
// 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].UserName == "admin" && response[0].TokenID == tokenID, err
return &UserChecked{
UserName: userName,
Admin: dbResult.Admin,
}, err
}
}