feat: improve app security and error handling
Improve overall app security by: - using dynamic statements for all sql querries - introducing environment variables for initial admin password - introducing enironment variable for cors address - improving error handling
This commit is contained in:
parent
95057c1b8d
commit
3ac1947106
11 changed files with 1333 additions and 453 deletions
|
|
@ -3,10 +3,13 @@ package main
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
|
@ -15,24 +18,60 @@ type App struct {
|
|||
DB *sql.DB
|
||||
}
|
||||
|
||||
func HandleError(c echo.Context, err *AppError) error {
|
||||
log.Printf("[%s] %s", err.Code, err.Error())
|
||||
|
||||
return c.JSON(err.HTTPStatus, err.ToResponse())
|
||||
}
|
||||
|
||||
func getClaims(c echo.Context) (*Claims, error) {
|
||||
user, ok := c.Get("user").(*jwt.Token)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("JWT token missing or invalid")
|
||||
}
|
||||
|
||||
claims, ok := user.Claims.(*Claims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse JWT claims")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func isDuplicateError(err error) bool {
|
||||
return err != nil && (err.Error() == "UNIQUE constraint failed" ||
|
||||
strings.Contains(err.Error(), "UNIQUE") ||
|
||||
strings.Contains(err.Error(), "duplicate"))
|
||||
}
|
||||
|
||||
func (app *App) LoginHandler(c echo.Context) error {
|
||||
var req LoginRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
||||
return HandleError(c, ErrInvalidInputMsg("Login-Daten"))
|
||||
}
|
||||
|
||||
if req.Username == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Benutzername"))
|
||||
}
|
||||
if req.Password == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Passwort"))
|
||||
}
|
||||
|
||||
user, err := GetUserByUsername(app.DB, req.Username)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials")
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrInvalidCredentialsMsg())
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials")
|
||||
return HandleError(c, ErrInvalidCredentialsMsg())
|
||||
}
|
||||
|
||||
token, err := createToken(user.ID, user.Username, user.IsAdmin)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "error creating token")
|
||||
return HandleError(c, ErrInternalMsg(err))
|
||||
}
|
||||
|
||||
response := LoginResponse{
|
||||
|
|
@ -47,7 +86,7 @@ func (app *App) LoginHandler(c echo.Context) error {
|
|||
func (app *App) GetSchedulesHandler(c echo.Context) error {
|
||||
schedules, err := GetAllSchedules(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
return c.JSON(http.StatusOK, schedules)
|
||||
}
|
||||
|
|
@ -55,24 +94,40 @@ func (app *App) GetSchedulesHandler(c echo.Context) error {
|
|||
func (app *App) CreateScheduleHandler(c echo.Context) error {
|
||||
var schedule Schedule
|
||||
if err := c.Bind(&schedule); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
||||
return HandleError(c, ErrInvalidInputMsg("Stundenplan-Daten"))
|
||||
}
|
||||
|
||||
if schedule.StartTime == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Startzeit"))
|
||||
}
|
||||
if schedule.EndTime == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Endzeit"))
|
||||
}
|
||||
if schedule.Title == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Titel"))
|
||||
}
|
||||
|
||||
if err := CreateSchedule(app.DB, &schedule); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if isDuplicateError(err) {
|
||||
return HandleError(c, ErrAlreadyExistsMsg("Stundenplan-Eintrag"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, map[string]string{"message": "schedule created"})
|
||||
return c.JSON(http.StatusCreated, map[string]string{"message": "Stundenplan erstellt"})
|
||||
}
|
||||
|
||||
func (app *App) DeleteScheduleHandler(c echo.Context) error {
|
||||
id, err := strconv.Atoi(c.QueryParam("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
|
||||
return HandleError(c, ErrInvalidInputMsg("Stundenplan-ID"))
|
||||
}
|
||||
|
||||
if err := DeleteSchedule(app.DB, id); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Stundenplan"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
|
|
@ -81,7 +136,7 @@ func (app *App) DeleteScheduleHandler(c echo.Context) error {
|
|||
func (app *App) GetYearlyHoursSummaryHandler(c echo.Context) error {
|
||||
hours, err := GetYearlyHoursSummary(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if hours == nil {
|
||||
hours = []WeeklyHours{}
|
||||
|
|
@ -90,11 +145,6 @@ func (app *App) GetYearlyHoursSummaryHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
||||
isAdmin, _ := c.Get("is_admin").(bool)
|
||||
if !isAdmin {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Only admins can create entries for others")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID int `json:"user_id"`
|
||||
Date string `json:"date"`
|
||||
|
|
@ -103,7 +153,17 @@ func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
||||
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
||||
}
|
||||
|
||||
if req.UserID == 0 {
|
||||
return HandleError(c, ErrMissingFieldMsg("Benutzer"))
|
||||
}
|
||||
if req.Date == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Datum"))
|
||||
}
|
||||
if req.Hours == 0 {
|
||||
return HandleError(c, ErrMissingFieldMsg("Stunden"))
|
||||
}
|
||||
|
||||
entry := TimeEntry{
|
||||
|
|
@ -115,16 +175,16 @@ func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
if err := CreateManualTimeEntry(app.DB, &entry, req.Hours); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *App) GetUsersHandler(c echo.Context) error {
|
||||
users, err := GetAllUsers(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if users == nil {
|
||||
users = []User{}
|
||||
|
|
@ -135,39 +195,62 @@ func (app *App) GetUsersHandler(c echo.Context) error {
|
|||
func (app *App) DeleteUserHandler(c echo.Context) error {
|
||||
id, err := strconv.Atoi(c.QueryParam("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
|
||||
return HandleError(c, ErrInvalidInputMsg("Benutzer-ID"))
|
||||
}
|
||||
|
||||
if id == 1 {
|
||||
return HandleError(c, ErrProtectedUserMsg())
|
||||
}
|
||||
|
||||
if err := DeleteUser(app.DB, id); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *App) CreateTimeEntryHandler(c echo.Context) error {
|
||||
userID := c.Get("user_id").(int)
|
||||
claims, err := getClaims(c)
|
||||
if err != nil {
|
||||
return HandleError(c, ErrUnauthorizedMsg())
|
||||
}
|
||||
|
||||
var entry TimeEntry
|
||||
if err := c.Bind(&entry); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
||||
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
||||
}
|
||||
|
||||
entry.UserID = userID
|
||||
if entry.Date == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Datum"))
|
||||
}
|
||||
if entry.StartTime == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Startzeit"))
|
||||
}
|
||||
if entry.EndTime == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Endzeit"))
|
||||
}
|
||||
|
||||
entry.UserID = claims.UserID
|
||||
|
||||
if err := CreateTimeEntry(app.DB, &entry); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, map[string]string{"message": "time entry created"})
|
||||
return c.JSON(http.StatusCreated, map[string]string{"message": "Zeiteintrag erstellt"})
|
||||
}
|
||||
|
||||
func (app *App) GetMyTimeEntriesHandler(c echo.Context) error {
|
||||
userID := c.Get("user_id").(int)
|
||||
|
||||
entries, err := GetTimeEntriesByUser(app.DB, userID)
|
||||
claims, err := getClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrUnauthorizedMsg())
|
||||
}
|
||||
|
||||
entries, err := GetTimeEntriesByUser(app.DB, claims.UserID)
|
||||
if err != nil {
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if entries == nil {
|
||||
entries = []TimeEntry{}
|
||||
|
|
@ -179,12 +262,16 @@ func (app *App) GetMyTimeEntriesHandler(c echo.Context) error {
|
|||
func (app *App) GetWeekDates(c echo.Context) error {
|
||||
year, err := strconv.Atoi(c.QueryParam("year"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid year")
|
||||
return HandleError(c, ErrInvalidInputMsg("Jahr"))
|
||||
}
|
||||
|
||||
week, err := strconv.Atoi(c.QueryParam("week"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid week")
|
||||
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
||||
}
|
||||
|
||||
if week < 1 || week > 53 {
|
||||
return HandleError(c, ErrInvalidInputMsg("Woche (muss zwischen 1 und 53 liegen)"))
|
||||
}
|
||||
|
||||
dates := calculateWeekDates(year, week)
|
||||
|
|
@ -192,21 +279,24 @@ func (app *App) GetWeekDates(c echo.Context) error {
|
|||
}
|
||||
|
||||
func (app *App) CheckWeekHasEntries(c echo.Context) error {
|
||||
userID := c.Get("user_id").(int)
|
||||
claims, err := getClaims(c)
|
||||
if err != nil {
|
||||
return HandleError(c, ErrUnauthorizedMsg())
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(c.QueryParam("year"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid year")
|
||||
return HandleError(c, ErrInvalidInputMsg("Jahr"))
|
||||
}
|
||||
|
||||
week, err := strconv.Atoi(c.QueryParam("week"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid week")
|
||||
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
||||
}
|
||||
|
||||
hasEntries, err := CheckUserHasEntriesForWeek(app.DB, userID, year, week)
|
||||
hasEntries, err := CheckUserHasEntriesForWeek(app.DB, claims.UserID, year, week)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]bool{"has_entries": hasEntries})
|
||||
|
|
@ -215,7 +305,7 @@ func (app *App) CheckWeekHasEntries(c echo.Context) error {
|
|||
func (app *App) GetAllTimeEntriesHandler(c echo.Context) error {
|
||||
entries, err := GetAllTimeEntries(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if entries == nil {
|
||||
entries = []TimeEntry{}
|
||||
|
|
@ -226,7 +316,7 @@ func (app *App) GetAllTimeEntriesHandler(c echo.Context) error {
|
|||
func (app *App) GetWeeklyHoursHandler(c echo.Context) error {
|
||||
hours, err := GetWeeklyHours(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if hours == nil {
|
||||
hours = []WeeklyHours{}
|
||||
|
|
@ -235,20 +325,23 @@ func (app *App) GetWeeklyHoursHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
func (app *App) DeleteWeekEntries(c echo.Context) error {
|
||||
userID := c.Get("user_id").(int)
|
||||
claims, err := getClaims(c)
|
||||
if err != nil {
|
||||
return HandleError(c, ErrUnauthorizedMsg())
|
||||
}
|
||||
|
||||
year, err := strconv.Atoi(c.QueryParam("year"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid year")
|
||||
return HandleError(c, ErrInvalidInputMsg("Jahr"))
|
||||
}
|
||||
|
||||
week, err := strconv.Atoi(c.QueryParam("week"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid week")
|
||||
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
||||
}
|
||||
|
||||
if err := DeleteTimeEntriesByUserAndWeek(app.DB, userID, year, week); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err := DeleteTimeEntriesByUserAndWeek(app.DB, claims.UserID, year, week); err != nil {
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
|
|
@ -310,52 +403,70 @@ type BatchTimeEntryRequest struct {
|
|||
}
|
||||
|
||||
func (app *App) CreateBatchTimeEntriesHandler(c echo.Context) error {
|
||||
userID := c.Get("user_id").(int)
|
||||
claims, err := getClaims(c)
|
||||
if err != nil {
|
||||
return HandleError(c, ErrUnauthorizedMsg())
|
||||
}
|
||||
|
||||
var req BatchTimeEntryRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
|
||||
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
||||
}
|
||||
|
||||
if len(req.Entries) == 0 {
|
||||
return HandleError(c, ErrMissingFieldMsg("Zeiteinträge"))
|
||||
}
|
||||
|
||||
tx, err := app.DB.Begin()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "transaction error")
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time) VALUES (?, ?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "prepare error")
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, entry := range req.Entries {
|
||||
_, err := stmt.Exec(userID, entry.ScheduleID, entry.Date, entry.Type, entry.StartTime, entry.EndTime)
|
||||
_, err := stmt.Exec(claims.UserID, entry.ScheduleID, entry.Date, entry.Type, entry.StartTime, entry.EndTime)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "insert error")
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "commit error")
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusCreated, map[string]string{"message": "entries created"})
|
||||
return c.JSON(http.StatusCreated, map[string]string{"message": "Zeiteinträge erstellt"})
|
||||
}
|
||||
|
||||
func (app *App) UpdateUserHandler(c echo.Context) error {
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
|
||||
return HandleError(c, ErrInvalidInputMsg("Benutzer-ID"))
|
||||
}
|
||||
|
||||
if userID == 1 {
|
||||
return HandleError(c, ErrProtectedUserMsg())
|
||||
}
|
||||
|
||||
var req UpdateUserRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
return HandleError(c, ErrInvalidInputMsg("Benutzerdaten"))
|
||||
}
|
||||
|
||||
if req.YearlyHours <= 0 {
|
||||
return HandleError(c, ErrInvalidInputMsg("Jahresarbeitsstunden (muss positiv sein)"))
|
||||
}
|
||||
|
||||
if err := UpdateUser(app.DB, userID, req.YearlyHours); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
|
|
@ -364,21 +475,28 @@ func (app *App) UpdateUserHandler(c echo.Context) error {
|
|||
func (app *App) ResetPasswordHandler(c echo.Context) error {
|
||||
userID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user ID")
|
||||
return HandleError(c, ErrInvalidInputMsg("Benutzer-ID"))
|
||||
}
|
||||
|
||||
var req ResetPasswordRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
return HandleError(c, ErrInvalidInputMsg("Passwort-Daten"))
|
||||
}
|
||||
|
||||
if len(req.NewPassword) < 6 {
|
||||
return HandleError(c, ErrInvalidInputMsg("Passwort (mindestens 6 Zeichen)"))
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Error hashing password")
|
||||
return HandleError(c, ErrInternalMsg(err))
|
||||
}
|
||||
|
||||
if err := ResetUserPassword(app.DB, userID, string(hashedPassword)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
|
|
@ -387,16 +505,29 @@ func (app *App) ResetPasswordHandler(c echo.Context) error {
|
|||
func (app *App) UpdateTimeEntryHandler(c echo.Context) error {
|
||||
entryID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid entry ID")
|
||||
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-ID"))
|
||||
}
|
||||
|
||||
var req UpdateTimeEntryRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
||||
}
|
||||
|
||||
if req.Date == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Datum"))
|
||||
}
|
||||
if req.StartTime == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Startzeit"))
|
||||
}
|
||||
if req.EndTime == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Endzeit"))
|
||||
}
|
||||
|
||||
if err := UpdateTimeEntry(app.DB, entryID, req.Date, req.StartTime, req.EndTime, req.Type); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Zeiteintrag"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
|
|
@ -405,22 +536,31 @@ func (app *App) UpdateTimeEntryHandler(c echo.Context) error {
|
|||
func (app *App) DeleteTimeEntryHandler(c echo.Context) error {
|
||||
entryID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid entry ID")
|
||||
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-ID"))
|
||||
}
|
||||
|
||||
if err := DeleteTimeEntry(app.DB, entryID); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Zeiteintrag"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *App) GetMyInfoHandler(c echo.Context) error {
|
||||
userID := c.Get("user_id").(int)
|
||||
|
||||
user, err := GetUserByID(app.DB, userID)
|
||||
claims, err := getClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrUnauthorizedMsg())
|
||||
}
|
||||
|
||||
user, err := GetUserByID(app.DB, claims.UserID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, user)
|
||||
|
|
@ -429,12 +569,22 @@ func (app *App) GetMyInfoHandler(c echo.Context) error {
|
|||
func (app *App) CreateUserHandler(c echo.Context) error {
|
||||
var req CreateUserRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
return HandleError(c, ErrInvalidInputMsg("Benutzerdaten"))
|
||||
}
|
||||
|
||||
if req.Username == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Benutzername"))
|
||||
}
|
||||
if req.Password == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Passwort"))
|
||||
}
|
||||
if len(req.Password) < 6 {
|
||||
return HandleError(c, ErrInvalidInputMsg("Passwort (mindestens 6 Zeichen)"))
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Error hashing password")
|
||||
return HandleError(c, ErrInternalMsg(err))
|
||||
}
|
||||
|
||||
if req.YearlyHours == 0 {
|
||||
|
|
@ -442,7 +592,10 @@ func (app *App) CreateUserHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin, req.YearlyHours); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if isDuplicateError(err) {
|
||||
return HandleError(c, ErrAlreadyExistsMsg("Benutzername"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
|
|
@ -451,7 +604,7 @@ func (app *App) CreateUserHandler(c echo.Context) error {
|
|||
func (app *App) GetSchoolYearsHandler(c echo.Context) error {
|
||||
years, err := GetAllSchoolYears(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if years == nil {
|
||||
years = []SchoolYear{}
|
||||
|
|
@ -462,11 +615,24 @@ func (app *App) GetSchoolYearsHandler(c echo.Context) error {
|
|||
func (app *App) CreateSchoolYearHandler(c echo.Context) error {
|
||||
var req CreateSchoolYearRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
return HandleError(c, ErrInvalidInputMsg("Schuljahr-Daten"))
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Name"))
|
||||
}
|
||||
if req.StartDate == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Startdatum"))
|
||||
}
|
||||
if req.EndDate == "" {
|
||||
return HandleError(c, ErrMissingFieldMsg("Enddatum"))
|
||||
}
|
||||
|
||||
if err := CreateSchoolYear(app.DB, req.Name, req.StartDate, req.EndDate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if isDuplicateError(err) {
|
||||
return HandleError(c, ErrAlreadyExistsMsg("Schuljahr"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
|
|
@ -475,11 +641,14 @@ func (app *App) CreateSchoolYearHandler(c echo.Context) error {
|
|||
func (app *App) SetActiveSchoolYearHandler(c echo.Context) error {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID")
|
||||
return HandleError(c, ErrInvalidInputMsg("Schuljahr-ID"))
|
||||
}
|
||||
|
||||
if err := SetActiveSchoolYear(app.DB, id); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
if err == sql.ErrNoRows {
|
||||
return HandleError(c, ErrNotFoundMsg("Schuljahr"))
|
||||
}
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
|
|
@ -488,7 +657,7 @@ func (app *App) SetActiveSchoolYearHandler(c echo.Context) error {
|
|||
func (app *App) GetActiveSchoolYearHandler(c echo.Context) error {
|
||||
year, err := GetActiveSchoolYear(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if year == nil {
|
||||
return c.JSON(http.StatusOK, map[string]any{"active": false})
|
||||
|
|
@ -497,24 +666,22 @@ func (app *App) GetActiveSchoolYearHandler(c echo.Context) error {
|
|||
}
|
||||
|
||||
func (app *App) GenerateYearlySummaryPDFHandler(c echo.Context) error {
|
||||
isAdmin, _ := c.Get("is_admin").(bool)
|
||||
if !isAdmin {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Only admins can generate PDFs")
|
||||
}
|
||||
|
||||
schoolYear, err := GetActiveSchoolYear(app.DB)
|
||||
if err != nil || schoolYear == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "No active school year found")
|
||||
if err != nil {
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
if schoolYear == nil {
|
||||
return HandleError(c, ErrNoActiveSchoolYearMsg())
|
||||
}
|
||||
|
||||
summary, err := GetYearlyHoursSummary(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
return HandleError(c, ErrDatabaseMsg(err))
|
||||
}
|
||||
|
||||
pdfBytes, err := GenerateYearlySummaryPDF(schoolYear, summary)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate PDF: "+err.Error())
|
||||
return HandleError(c, ErrInternalMsg(err))
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("Jahresuebersicht_%s.pdf", schoolYear.Name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue