diff --git a/backend/go.mod b/backend/go.mod index acf5f9a..c45ed46 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,6 +3,7 @@ module school-timetracker go 1.25.3 require ( + github.com/labstack/echo/v4 v4.13.4 golang.org/x/crypto v0.43.0 modernc.org/sqlite v1.40.0 ) @@ -10,11 +11,18 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/net v0.45.0 // indirect golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.11.0 // indirect modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 34771d9..3ab6680 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,28 +1,52 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= diff --git a/backend/handlers.go b/backend/handlers.go index 04b9a73..964d6e1 100644 --- a/backend/handlers.go +++ b/backend/handlers.go @@ -2,11 +2,10 @@ package main import ( "database/sql" - "encoding/json" - "log" "net/http" "strconv" + "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" ) @@ -14,28 +13,25 @@ type App struct { DB *sql.DB } -func (app *App) LoginHandler(w http.ResponseWriter, r *http.Request) { +// Login Handler +func (app *App) LoginHandler(c echo.Context) error { var req LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } user, err := GetUserByUsername(app.DB, req.Username) if err != nil { - http.Error(w, "Invalid credentials", http.StatusUnauthorized) - return + return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { - http.Error(w, "Invalid credentials", http.StatusUnauthorized) - return + return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") } token, err := createToken(user.ID, user.Username, user.IsAdmin) if err != nil { - http.Error(w, "Error creating token", http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, "error creating token") } response := LoginResponse{ @@ -44,179 +40,125 @@ func (app *App) LoginHandler(w http.ResponseWriter, r *http.Request) { IsAdmin: user.IsAdmin, } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + return c.JSON(http.StatusOK, response) } -func (app *App) GetSchedulesHandler(w http.ResponseWriter, r *http.Request) { +// Schedule Handlers +func (app *App) GetSchedulesHandler(c echo.Context) error { schedules, err := GetAllSchedules(app.DB) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(schedules) + return c.JSON(http.StatusOK, schedules) } -func (app *App) CreateScheduleHandler(w http.ResponseWriter, r *http.Request) { +func (app *App) CreateScheduleHandler(c echo.Context) error { var schedule Schedule - if err := json.NewDecoder(r.Body).Decode(&schedule); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return + if err := c.Bind(&schedule); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } if err := CreateSchedule(app.DB, &schedule); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - w.WriteHeader(http.StatusCreated) + return c.JSON(http.StatusCreated, map[string]string{"message": "schedule created"}) } -func (app *App) DeleteScheduleHandler(w http.ResponseWriter, r *http.Request) { - idStr := r.URL.Query().Get("id") - id, err := strconv.Atoi(idStr) +func (app *App) DeleteScheduleHandler(c echo.Context) error { + id, err := strconv.Atoi(c.QueryParam("id")) if err != nil { - http.Error(w, "Invalid ID", http.StatusBadRequest) - return + return echo.NewHTTPError(http.StatusBadRequest, "invalid id") } if err := DeleteSchedule(app.DB, id); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - w.WriteHeader(http.StatusOK) + return c.NoContent(http.StatusOK) } -func (app *App) CreateUserHandler(w http.ResponseWriter, r *http.Request) { - var req struct { - Username string `json:"username"` - Password string `json:"password"` - IsAdmin bool `json:"is_admin"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return +// User Handlers +func (app *App) CreateUserHandler(c echo.Context) error { + var req CreateUserRequest + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { - http.Error(w, "Error hashing password", http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, "error hashing password") } if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - w.WriteHeader(http.StatusCreated) + return c.JSON(http.StatusCreated, map[string]string{"message": "user created"}) } -func (app *App) GetUsersHandler(w http.ResponseWriter, r *http.Request) { +func (app *App) GetUsersHandler(c echo.Context) error { users, err := GetAllUsers(app.DB) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(users) + return c.JSON(http.StatusOK, users) } -// func (app *App) CreateTimeEntryHandler(w http.ResponseWriter, r *http.Request) { -// userIDStr := r.Header.Get("X-User-ID") -// userID, _ := strconv.Atoi(userIDStr) - -// var entry TimeEntry -// if err := json.NewDecoder(r.Body).Decode(&entry); err != nil { -// http.Error(w, err.Error(), http.StatusBadRequest) -// return -// } - -// entry.UserID = userID - -// if err := CreateTimeEntry(app.DB, &entry); err != nil { -// http.Error(w, err.Error(), http.StatusInternalServerError) -// return -// } - -// w.WriteHeader(http.StatusCreated) -// } - -func (app *App) GetMyTimeEntriesHandler(w http.ResponseWriter, r *http.Request) { - userIDStr := r.Header.Get("X-User-ID") - userID, _ := strconv.Atoi(userIDStr) - - entries, err := GetTimeEntriesByUser(app.DB, userID) +func (app *App) DeleteUserHandler(c echo.Context) error { + id, err := strconv.Atoi(c.QueryParam("id")) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusBadRequest, "invalid id") } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entries) -} - -func (app *App) GetAllTimeEntriesHandler(w http.ResponseWriter, r *http.Request) { - entries, err := GetAllTimeEntries(app.DB) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + if err := DeleteUser(app.DB, id); err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(entries) + return c.NoContent(http.StatusOK) } -func (app *App) CreateTimeEntryHandler(w http.ResponseWriter, r *http.Request) { - userIDStr := r.Header.Get("X-User-ID") - userID, _ := strconv.Atoi(userIDStr) +// Time Entry Handlers +func (app *App) CreateTimeEntryHandler(c echo.Context) error { + userID := c.Get("user_id").(int) var entry TimeEntry - if err := json.NewDecoder(r.Body).Decode(&entry); err != nil { - log.Print("Error on Decoding occured") - http.Error(w, err.Error(), http.StatusBadRequest) - return + if err := c.Bind(&entry); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request") } entry.UserID = userID if err := CreateTimeEntry(app.DB, &entry); err != nil { - log.Print("Error on creating time entry in Database occured") - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - w.WriteHeader(http.StatusCreated) + return c.JSON(http.StatusCreated, map[string]string{"message": "time entry created"}) } -func (app *App) DeleteUserHandler(w http.ResponseWriter, r *http.Request) { - idStr := r.URL.Query().Get("id") - id, err := strconv.Atoi(idStr) +func (app *App) GetMyTimeEntriesHandler(c echo.Context) error { + userID := c.Get("user_id").(int) + + entries, err := GetTimeEntriesByUser(app.DB, userID) if err != nil { - http.Error(w, "Invalid ID", http.StatusBadRequest) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if err := DeleteUser(app.DB, id); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) + return c.JSON(http.StatusOK, entries) } -func (app *App) GetWeeklyHoursHandler(w http.ResponseWriter, r *http.Request) { +func (app *App) GetAllTimeEntriesHandler(c echo.Context) error { + entries, err := GetAllTimeEntries(app.DB) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return c.JSON(http.StatusOK, entries) +} + +func (app *App) GetWeeklyHoursHandler(c echo.Context) error { hours, err := GetWeeklyHours(app.DB) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(hours) + return c.JSON(http.StatusOK, hours) } diff --git a/backend/main.go b/backend/main.go index 6b73d66..225b8fc 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,9 +4,13 @@ import ( "log" "net/http" "os" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) func main() { + // Database Setup dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./timetracking.db" @@ -17,32 +21,78 @@ func main() { app := &App{DB: db} + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, + })) + + // Custom error handler + e.HTTPErrorHandler = customHTTPErrorHandler + // Public routes - http.HandleFunc("/api/login", CORS(app.LoginHandler)) + e.POST("/api/login", app.LoginHandler) - // Protected routes - http.HandleFunc("/api/schedules", CORS(AuthMiddleware(app.GetSchedulesHandler))) - http.HandleFunc("/api/time-entries", CORS(AuthMiddleware(app.CreateTimeEntryHandler))) - http.HandleFunc("/api/my-time-entries", CORS(AuthMiddleware(app.GetMyTimeEntriesHandler))) + // Protected routes group + protected := e.Group("/api") + protected.Use(JWTMiddleware()) + { + protected.GET("/schedules", app.GetSchedulesHandler) + protected.POST("/time-entries", app.CreateTimeEntryHandler) + protected.GET("/my-time-entries", app.GetMyTimeEntriesHandler) + } - // Admin routes - http.HandleFunc("/api/admin/schedules", CORS(AdminMiddleware(app.CreateScheduleHandler))) - http.HandleFunc("/api/admin/schedules/delete", CORS(AdminMiddleware(app.DeleteScheduleHandler))) - http.HandleFunc("/api/admin/users", CORS(AdminMiddleware(app.CreateUserHandler))) - http.HandleFunc("/api/admin/users/list", CORS(AdminMiddleware(app.GetUsersHandler))) - http.HandleFunc("/api/admin/users/delete", CORS(AdminMiddleware(app.DeleteUserHandler))) // Neu - http.HandleFunc("/api/admin/time-entries", CORS(AdminMiddleware(app.GetAllTimeEntriesHandler))) - http.HandleFunc("/api/admin/weekly-hours", CORS(AdminMiddleware(app.GetWeeklyHoursHandler))) // Neu + // Admin routes group + admin := e.Group("/api/admin") + admin.Use(JWTMiddleware()) + admin.Use(AdminMiddleware()) + { + admin.POST("/schedules", app.CreateScheduleHandler) + admin.DELETE("/schedules/delete", app.DeleteScheduleHandler) + admin.POST("/users", app.CreateUserHandler) + admin.GET("/users/list", app.GetUsersHandler) + admin.DELETE("/users/delete", app.DeleteUserHandler) + admin.GET("/time-entries", app.GetAllTimeEntriesHandler) + admin.GET("/weekly-hours", app.GetWeeklyHoursHandler) + } - // Serve frontend - fs := http.FileServer(http.Dir("./static")) - http.Handle("/", fs) + // Static files + e.Static("/", "./static") + // Start server port := os.Getenv("PORT") if port == "" { port = "8080" } log.Printf("Server starting on port %s", port) - log.Fatal(http.ListenAndServe(":"+port, nil)) + e.Logger.Fatal(e.Start(":" + port)) +} + +// Custom error handler for better error responses +func customHTTPErrorHandler(err error, c echo.Context) { + code := http.StatusInternalServerError + message := "Internal Server Error" + + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + message = he.Message.(string) + } + + // Don't override response if already written + if !c.Response().Committed { + if c.Request().Method == http.MethodHead { + c.NoContent(code) + } else { + c.JSON(code, map[string]string{ + "error": message, + }) + } + } } diff --git a/backend/middleware.go b/backend/middleware.go index 569adc3..61bfa7f 100644 --- a/backend/middleware.go +++ b/backend/middleware.go @@ -6,64 +6,35 @@ import ( "encoding/base64" "encoding/json" "fmt" - "log/slog" "net/http" "strings" "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) var jwtSecret = []byte("your-secret-key-change-in-production") -type Claims struct { - UserID int `json:"user_id"` - Username string `json:"username"` - IsAdmin bool `json:"is_admin"` - Exp int64 `json:"exp"` -} - -type responseWriter struct { - http.ResponseWriter - status int - wroteHeader bool - written int64 -} - -func wrapResponseWriter(w http.ResponseWriter) *responseWriter { - return &responseWriter{ - ResponseWriter: w, - status: http.StatusOK, - } -} - -func (rw *responseWriter) WriteHeader(code int) { - if rw.wroteHeader { - return - } - rw.status = code - rw.ResponseWriter.WriteHeader(code) - rw.wroteHeader = true -} - -func (rw *responseWriter) Write(b []byte) (int, error) { - if !rw.wroteHeader { - rw.WriteHeader(http.StatusOK) - } - n, err := rw.ResponseWriter.Write(b) - rw.written += int64(n) - return n, err -} - +// JWT Token Funktionen (bleiben gleich) func createToken(userID int, username string, isAdmin bool) (string, error) { claims := Claims{ UserID: userID, Username: username, IsAdmin: isAdmin, - Exp: time.Now().Add(24 * time.Hour).Unix(), } header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) - payload, _ := json.Marshal(claims) + // Füge Expiration hinzu + claimsWithExp := map[string]interface{}{ + "user_id": claims.UserID, + "username": claims.Username, + "is_admin": claims.IsAdmin, + "exp": time.Now().Add(24 * time.Hour).Unix(), + } + + payload, _ := json.Marshal(claimsWithExp) payloadEncoded := base64.RawURLEncoding.EncodeToString(payload) message := header + "." + payloadEncoded @@ -95,86 +66,68 @@ func verifyToken(tokenString string) (*Claims, error) { return nil, err } - var claims Claims - if err := json.Unmarshal(payload, &claims); err != nil { + var claimsMap map[string]interface{} + if err := json.Unmarshal(payload, &claimsMap); err != nil { return nil, err } - if time.Now().Unix() > claims.Exp { - return nil, fmt.Errorf("token expired") + // Check expiration + if exp, ok := claimsMap["exp"].(float64); ok { + if time.Now().Unix() > int64(exp) { + return nil, fmt.Errorf("token expired") + } } - return &claims, nil + claims := &Claims{ + UserID: int(claimsMap["user_id"].(float64)), + Username: claimsMap["username"].(string), + IsAdmin: claimsMap["is_admin"].(bool), + } + + return claims, nil } -func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return +// Echo JWT Middleware +func JWTMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header") + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + claims, err := verifyToken(tokenString) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "invalid token") + } + + // Store claims in context + c.Set("user_id", claims.UserID) + c.Set("username", claims.Username) + c.Set("is_admin", claims.IsAdmin) + + return next(c) } - - tokenString := strings.TrimPrefix(authHeader, "Bearer ") - claims, err := verifyToken(tokenString) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - r.Header.Set("X-User-ID", fmt.Sprintf("%d", claims.UserID)) - r.Header.Set("X-Username", claims.Username) - r.Header.Set("X-Is-Admin", fmt.Sprintf("%t", claims.IsAdmin)) - - next(w, r) } } -func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc { - return AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { - isAdmin := r.Header.Get("X-Is-Admin") == "true" - if !isAdmin { - http.Error(w, "Forbidden", http.StatusForbidden) - return +// Admin Middleware +func AdminMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + isAdmin, ok := c.Get("is_admin").(bool) + if !ok || !isAdmin { + return echo.NewHTTPError(http.StatusForbidden, "admin access required") + } + return next(c) } - next(w, r) - }) -} - -func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - wrapped := wrapResponseWriter(w) - - defer func() { - slog.Info("http request", - "method", r.Method, - "path", r.URL.Path, - "query", r.URL.RawQuery, - "status", wrapped.status, - "duration_ms", time.Since(start).Milliseconds(), - "client_ip", r.RemoteAddr, - "user_agent", r.UserAgent(), - "bytes_written", wrapped.written, - ) - }() - - next(wrapped, r) - } -} - -func CORS(next http.HandlerFunc) http.HandlerFunc { - return LoggingMiddleware(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - next(w, r) + } +} + +// Custom Logger Middleware (optional - Echo hat bereits einen) +func CustomLogger() echo.MiddlewareFunc { + return middleware.LoggerWithConfig(middleware.LoggerConfig{ + Format: "${time_rfc3339} | ${status} | ${latency_human} | ${method} ${uri}\n", }) } diff --git a/backend/models.go b/backend/models.go index fa69693..cd2f7b6 100644 --- a/backend/models.go +++ b/backend/models.go @@ -1,8 +1,6 @@ package main -import ( - "time" -) +import "time" type TimeEntry struct { ID int `json:"id"` @@ -10,10 +8,10 @@ type TimeEntry struct { ScheduleID int `json:"schedule_id"` Date string `json:"date"` Type string `json:"type"` - StartTime string `json:"start_time"` // Neu - EndTime string `json:"end_time"` // Neu + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` CreatedAt time.Time `json:"created_at"` - Username string `json:"username"` // Neu - für Join + Username string `json:"username"` } type WeeklyHours struct { @@ -33,25 +31,16 @@ type User struct { type Schedule struct { ID int `json:"id"` - DayOfWeek int `json:"day_of_week"` // 0=Monday, 4=Friday + DayOfWeek int `json:"day_of_week"` StartTime string `json:"start_time"` EndTime string `json:"end_time"` - Type string `json:"type"` // "lesson" or "break" + Type string `json:"type"` Title string `json:"title"` } -// type TimeEntry struct { -// ID int `json:"id"` -// UserID int `json:"user_id"` -// ScheduleID int `json:"schedule_id"` -// Date string `json:"date"` -// Type string `json:"type"` // "lesson" or "break" -// CreatedAt time.Time `json:"created_at"` -// } - type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` } type LoginResponse struct { @@ -59,3 +48,16 @@ type LoginResponse struct { Username string `json:"username"` IsAdmin bool `json:"is_admin"` } + +type CreateUserRequest struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required,min=6"` + IsAdmin bool `json:"is_admin"` +} + +// Claims für JWT +type Claims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + IsAdmin bool `json:"is_admin"` +}