feat: change from Standart Library to echo framework to simplify development

This commit is contained in:
Patryk Hegenberg 2025-11-05 07:46:18 +01:00
parent d74046522b
commit 4514ce44a2
6 changed files with 254 additions and 275 deletions

View file

@ -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

View file

@ -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=

View file

@ -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())
}
return c.JSON(http.StatusOK, schedules)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(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())
}
return c.JSON(http.StatusOK, users)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(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)
if err := DeleteUser(app.DB, id); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
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
return c.NoContent(http.StatusOK)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
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
return c.JSON(http.StatusOK, entries)
}
w.WriteHeader(http.StatusOK)
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(w http.ResponseWriter, r *http.Request) {
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)
}

View file

@ -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,
})
}
}
}

View file

@ -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 {
// Check expiration
if exp, ok := claimsMap["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("token expired")
}
return &claims, nil
}
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
claims := &Claims{
UserID: int(claimsMap["user_id"].(float64)),
Username: claimsMap["username"].(string),
IsAdmin: claimsMap["is_admin"].(bool),
}
return claims, nil
}
// 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 == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header")
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := verifyToken(tokenString)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
}
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))
// Store claims in context
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("is_admin", claims.IsAdmin)
next(w, r)
return next(c)
}
}
}
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")
}
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)
return next(c)
}
}
}
// 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",
})
}

View file

@ -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"`
}