diff --git a/.dockerignore b/.dockerignore index fb58393..f4ea1e8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,7 @@ backend/.vscode backend/.gitignore backend/logs backend/config.yaml +backend/html client/node_modules client/.gitignore diff --git a/.gitignore b/.gitignore index e69de29..cf0767a 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,2 @@ +data +logs diff --git a/Dockerfile b/Dockerfile index a6cae21..2dd6719 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN go mod download && go mod verify # build the source-code COPY backend . -RUN go build -ldflags "-s -w" -o dist/backend +RUN go build -ldflags "-s -w" -o dist/golunteer # build the frontend FROM node:current AS client-builder @@ -29,19 +29,27 @@ FROM alpine:latest WORKDIR /usr/bin/app # copy the backend -COPY --from=backend-builder /usr/src/backend/dist/backend backend +COPY --from=backend-builder /usr/src/backend/dist/golunteer golunteer +COPY backend/setup.sql backend/tables.sql ./ # copy the client-html COPY --from=client-builder /usr/src/client/out html +EXPOSE 80 + # Create a group and user RUN addgroup -S golunteer && adduser -S golunteer -G golunteer -EXPOSE 61016 -EXPOSE 80 +# create an empty config-file so that a bind doesn't create a directory +RUN touch config.yaml && chown -R golunteer:golunteer config.yaml +RUN mkdir data && chown -R golunteer:golunteer data +RUN mkdir logs && chown -R golunteer:golunteer logs + +# copy the config-file +COPY backend/config-default.yaml . # Tell docker that all future commands should run as the appuser user USER golunteer # run the app -CMD ["app"] \ No newline at end of file +CMD ["./golunteer", "data/config.yaml"] \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index d6fec14..7f7e38b 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,4 +1,5 @@ config.yaml logs __debug_bin* -database.db \ No newline at end of file +database.db +html \ No newline at end of file diff --git a/backend/config-default.yaml b/backend/config-default.yaml new file mode 100644 index 0000000..c744999 --- /dev/null +++ b/backend/config-default.yaml @@ -0,0 +1,7 @@ +log_level: INFO +database: data/database.db +server: + port: 80 +client_session: + jwt_signature: JWT_SIGNATURE + expire: 168h diff --git a/backend/pkg/config/config.go b/backend/pkg/config/config.go index d7f127b..24a1262 100644 --- a/backend/pkg/config/config.go +++ b/backend/pkg/config/config.go @@ -2,7 +2,9 @@ package config import ( "bytes" + "errors" "fmt" + "io" "log" "os" "time" @@ -51,6 +53,8 @@ func (config ConfigStruct) SignJWT(val any) (string, error) { } func LoadConfig() ConfigStruct { + ensureConfigExists() + Config := ConfigYaml{} yamlFile, err := os.ReadFile(CONFIG_PATH) @@ -90,6 +94,32 @@ func LoadConfig() ConfigStruct { } } +func ensureConfigExists() { + // if the config path doesn't exist, copy the example config there + if _, err := os.Stat(CONFIG_PATH); errors.Is(err, os.ErrNotExist) { + source, err := os.Open("config-default.yaml") + + if err == nil { + defer source.Close() + + destination, err := os.Create(CONFIG_PATH) + + if err == nil { + defer destination.Close() + + io.Copy(destination, source) + } + } + } +} + func init() { + // check for the config passed as an argument + if len(os.Args) == 2 { + CONFIG_PATH = os.Args[1] + } else { + CONFIG_PATH = "config.yaml" + } + Config = LoadConfig() } diff --git a/backend/pkg/config/configYAML.go b/backend/pkg/config/configYAML.go index a4966cc..6534df1 100644 --- a/backend/pkg/config/configYAML.go +++ b/backend/pkg/config/configYAML.go @@ -9,7 +9,7 @@ import ( "gopkg.in/yaml.v3" ) -var CONFIG_PATH = "config.yaml" +var CONFIG_PATH string type ConfigYaml struct { LogLevel string `yaml:"log_level"` diff --git a/backend/pkg/logger/logger.go b/backend/pkg/logger/logger.go index a253ef5..106e584 100644 --- a/backend/pkg/logger/logger.go +++ b/backend/pkg/logger/logger.go @@ -37,11 +37,7 @@ func init() { Out: os.Stdout, TimeFormat: time.DateTime, FormatLevel: func(i interface{}) string { - if i == nil { - return "| LOG |" - } else { - return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) - } + return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) }, FormatFieldName: func(i interface{}) string { return fmt.Sprintf("%s", i) diff --git a/backend/pkg/router/availabilities.go b/backend/pkg/router/availabilities.go index 924e1c7..7cfc993 100644 --- a/backend/pkg/router/availabilities.go +++ b/backend/pkg/router/availabilities.go @@ -30,13 +30,13 @@ func (a *Handler) postAvailability() { if err := a.C.BodyParser(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) // validate the body } else if err := validate.Struct(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("invalid body: %v", err) + logger.Info().Msgf("invalid body: %v", err) } else if err := availabilities.Add(body); err != nil { a.Status = fiber.StatusInternalServerError @@ -60,13 +60,13 @@ func (a *Handler) patchAvailabilitiy() { if err := a.C.BodyParser(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) // validate the body } else if err := validate.Struct(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("invalid body: %v", err) + logger.Info().Msgf("invalid body: %v", err) } else if err := availabilities.Update(body); err != nil { a.Status = fiber.StatusInternalServerError @@ -84,7 +84,7 @@ func (a *Handler) deleteAvailability() { // parse the query } else if taskID := a.C.QueryInt("availabilityID", -1); taskID == -1 { - logger.Log().Msg("availability-deletion failed: invalid query: doesn't include \"availabilityID\"") + logger.Info().Msg("availability-deletion failed: invalid query: doesn't include \"availabilityID\"") a.Status = fiber.StatusBadRequest diff --git a/backend/pkg/router/events.go b/backend/pkg/router/events.go index 5421cb6..23ac077 100644 --- a/backend/pkg/router/events.go +++ b/backend/pkg/router/events.go @@ -24,13 +24,13 @@ func (a *Handler) postEvent() { if err := a.C.BodyParser(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) // validate the parsed body } else if err := validate.Struct(body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("invalid body: %v", err) + logger.Info().Msgf("invalid body: %v", err) // create the event } else if err := events.Create(body); err != nil { @@ -52,13 +52,13 @@ func (a *Handler) patchEvent() { if err := a.C.BodyParser(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) // validate the body } else if err := validate.Struct(body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("ivnalid body: %v", err) + logger.Info().Msgf("ivnalid body: %v", err) // update the event } else if err := events.Update(body); err != nil { @@ -99,7 +99,7 @@ func (a *Handler) getEventUserAssignmentAvailability() { if events, err := a.UserName.WithUserAvailability(); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("getting events with tasks and user-availability failed: %v", err) + logger.Info().Msgf("getting events with tasks and user-availability failed: %v", err) } else { a.Data = events } @@ -130,7 +130,7 @@ func (a *Handler) getEventsUserAssigned() { if events, err := a.UserName.GetAssignedEvents(); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("retrieval of user-assigned-events failed: %v", err) + logger.Info().Msgf("retrieval of user-assigned-events failed: %v", err) } else { a.Data = events } @@ -141,7 +141,7 @@ func (a *Handler) putEventUserAvailability() { if eventID := a.C.QueryInt("eventID", -1); eventID == -1 { a.Status = fiber.StatusBadRequest - logger.Log().Msg("setting user-event-availability failed: query is missing \"eventID\"") + logger.Info().Msg("setting user-event-availability failed: query is missing \"eventID\"") } else { // parse the body body := a.C.Body() @@ -149,7 +149,7 @@ func (a *Handler) putEventUserAvailability() { if availabilityID, err := strconv.Atoi(string(body)); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("setting user-event-availability failed: can't get parse: %v", err) + logger.Info().Msgf("setting user-event-availability failed: can't get parse: %v", err) } else { // if there was already a task assigned for this user-event-combi, remove it var taskIDs []int @@ -200,23 +200,23 @@ func (a *Handler) putEventAssignment() { } else if taskID := tasks.TaskID(a.C.QueryInt("taskID", -1)); taskID == -1 { a.Status = fiber.StatusBadRequest - logger.Log().Msg("setting event-assignment failed: query is missing \"taskID\"") + logger.Info().Msg("setting event-assignment failed: query is missing \"taskID\"") // parse the body } else if userName := users.UserName(a.C.Body()); userName == "" { a.Status = fiber.StatusBadRequest - logger.Log().Msg("setting event-assignment failed: body is missing") + logger.Info().Msg("setting event-assignment failed: body is missing") // check wether the user has actually entered an availability for the event } else if availabilityID, err := userName.GetUserAvailability(eventID); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("setting event-assignment failed: can't check users availability: %v", err) + logger.Info().Msgf("setting event-assignment failed: can't check users availability: %v", err) } else if availabilityID == nil { a.Status = fiber.StatusConflict - logger.Log().Msgf("setting event-assignment failed: user %q isn't available for event with eventID = %d", userName, eventID) + logger.Info().Msgf("setting event-assignment failed: user %q isn't available for event with eventID = %d", userName, eventID) // check wether the user can be assigned for this task } else if check, err := userName.CheckTask(taskID); err != nil { @@ -226,7 +226,7 @@ func (a *Handler) putEventAssignment() { } else if !check { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("setting event-assignment failed: task with taskID = %d is not possible for user", taskID) + logger.Info().Msgf("setting event-assignment failed: task with taskID = %d is not possible for user", taskID) // set the availability in the database } else if err := eventID.SetAssignment(taskID, userName); err != nil { @@ -273,7 +273,7 @@ func (a *Handler) deleteEvent() { a.Status = fiber.StatusForbidden // -1 can't be valid } else if eventId := a.C.QueryInt("eventID", -1); eventId == -1 { - logger.Log().Msgf("event-delete failed: \"eventID\" is missing in query") + logger.Info().Msgf("event-delete failed: \"eventID\" is missing in query") a.Status = fiber.StatusBadRequest } else if err := events.Delete(eventId); err != nil { @@ -282,6 +282,6 @@ func (a *Handler) deleteEvent() { a.Status = fiber.StatusInternalServerError } else { - logger.Log().Msgf("deleted event with eventID %d", eventId) + logger.Info().Msgf("deleted event with eventID %d", eventId) } } diff --git a/backend/pkg/router/router.go b/backend/pkg/router/router.go index edcd38c..7d92570 100644 --- a/backend/pkg/router/router.go +++ b/backend/pkg/router/router.go @@ -1,7 +1,10 @@ package router import ( + "errors" "fmt" + "os" + "path/filepath" "time" "github.com/go-playground/validator/v10" @@ -129,6 +132,25 @@ func init() { app.Post("/api/login", handleLogin) app.Get("/api/logout", handleLogout) + app.Use(func(c *fiber.Ctx) error { + path := c.Path() + + if _, err := os.Stat(filepath.Join("html", path)); errors.Is(err, os.ErrNotExist) { + htmlPath := path + ".html" + + if _, err := os.Stat(filepath.Join("html", htmlPath)); err == nil { + c.Path(htmlPath) + } else { + logger.Debug().Msgf("%q", err) + } + } + + c.Next() + + return nil + }) + app.Static("/", "html") + // register the registered endpoints for method, handlers := range endpoints { for address, handler := range handlers { @@ -146,7 +168,7 @@ func init() { } else if !loggedIn { args.Status = fiber.StatusUnauthorized - logger.Log().Msgf("user not authorized") + logger.Info().Msgf("user not authorized") } else { handler(&args) } diff --git a/backend/pkg/router/tasks.go b/backend/pkg/router/tasks.go index 3db8b7c..6297194 100644 --- a/backend/pkg/router/tasks.go +++ b/backend/pkg/router/tasks.go @@ -18,7 +18,7 @@ func (a *Handler) getTasks() { func (a *Handler) postTask() { // check admin if !a.Admin { - logger.Log().Msgf("user is not admin") + logger.Info().Msgf("user is not admin") a.Status = fiber.StatusUnauthorized } else { @@ -26,13 +26,13 @@ func (a *Handler) postTask() { var task tasks.Task if err := a.C.BodyParser(&task); err != nil { - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) a.Status = fiber.StatusBadRequest // validate the body } else if err := validate.Struct(&task); err != nil { - logger.Log().Msgf("invalid body: %v", err) + logger.Info().Msgf("invalid body: %v", err) a.Status = fiber.StatusBadRequest @@ -48,7 +48,7 @@ func (a *Handler) postTask() { func (a *Handler) patchTask() { // check admin if !a.Admin { - logger.Log().Msgf("user is not admin") + logger.Info().Msgf("user is not admin") a.Status = fiber.StatusUnauthorized } else { @@ -56,13 +56,13 @@ func (a *Handler) patchTask() { var task tasks.TaskDB if err := a.C.BodyParser(&task); err != nil { - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) a.Status = fiber.StatusBadRequest // validate the body } else if err := validate.Struct(&task); err != nil { - logger.Log().Msgf("invalid body: %v", err) + logger.Info().Msgf("invalid body: %v", err) a.Status = fiber.StatusBadRequest @@ -84,7 +84,7 @@ func (a *Handler) deleteTask() { // parse the query } else if taskID := a.C.QueryInt("taskID", -1); taskID == -1 { - logger.Log().Msg("task-deletion failed: invalid query: doesn't include \"taskID\"") + logger.Info().Msg("task-deletion failed: invalid query: doesn't include \"taskID\"") a.Status = fiber.StatusBadRequest diff --git a/backend/pkg/router/user.go b/backend/pkg/router/user.go index a5a53c5..afcea9f 100644 --- a/backend/pkg/router/user.go +++ b/backend/pkg/router/user.go @@ -10,7 +10,7 @@ func (a *Handler) getUsers() { if !a.Admin { a.Status = fiber.StatusForbidden - logger.Log().Msgf("user is no admin") + logger.Info().Msgf("user is no admin") } else if users, err := users.Get(); err != nil { a.Status = fiber.StatusInternalServerError @@ -26,7 +26,7 @@ func (a *Handler) postUser() { if !a.Admin { a.Status = fiber.StatusForbidden - logger.Log().Msgf("user is no admin") + logger.Info().Msgf("user is no admin") } else { // parse the body var body users.UserAdd @@ -54,7 +54,7 @@ func (a *Handler) putPassword() { var body users.UserChangePassword if err := a.C.BodyParser(&body); err != nil { - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) a.Status = fiber.StatusBadRequest @@ -98,7 +98,7 @@ func (a *Handler) patchUser() { if !a.Admin { a.Status = fiber.StatusForbidden - logger.Log().Msgf("user is no admin") + logger.Info().Msgf("user is no admin") } else { // parse the body var body struct { @@ -109,7 +109,7 @@ func (a *Handler) patchUser() { if err := a.C.BodyParser(&body); err != nil { a.Status = fiber.StatusBadRequest - logger.Log().Msgf("can't parse body: %v", err) + logger.Info().Msgf("can't parse body: %v", err) // prevent to demoting self from admin } else if !body.Admin && body.UserName == a.UserName { @@ -196,19 +196,19 @@ func (a *Handler) deleteUser() { // get the username from the query } else if userName := a.C.Query("userName"); userName == "" { - logger.Log().Msg("user-deletion failed: query is missing \"userName\"") + logger.Info().Msg("user-deletion failed: query is missing \"userName\"") a.Status = fiber.StatusBadRequest // check wether the user tries to delete himself } else if users.UserName(userName) == a.UserName { - logger.Log().Msg("user-deletion failed: self-deletion is illegal") + logger.Info().Msg("user-deletion failed: self-deletion is illegal") a.Status = fiber.StatusBadRequest // check wether the user tries to delete the admin } else if userName == "admin" { - logger.Log().Msg("user-deletion failed: admin-deletion is illegal") + logger.Info().Msg("user-deletion failed: admin-deletion is illegal") a.Status = fiber.StatusBadRequest diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..09f2974 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,25 @@ +services: + app: + image: z1glr/golunteer:latest + build: . + ports: + - 80:80 + volumes: + - database:/usr/bin/app/data + - logs:/usr/bin/app/logs + - ./config.yaml:/usr/bin/app/config.yaml + restart: unless-stopped + +volumes: + database: + driver: local + driver_opts: + type: none + device: "./data" + o: bind + logs: + driver: local + driver_opts: + type: none + device: "./logs" + o: bind \ No newline at end of file