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 go 1.25.3
require ( require (
github.com/labstack/echo/v4 v4.13.4
golang.org/x/crypto v0.43.0 golang.org/x/crypto v0.43.0
modernc.org/sqlite v1.40.0 modernc.org/sqlite v1.40.0
) )
@ -10,11 +11,18 @@ require (
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // 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/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/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/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/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 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 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/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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 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= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=

View file

@ -2,11 +2,10 @@ package main
import ( import (
"database/sql" "database/sql"
"encoding/json"
"log"
"net/http" "net/http"
"strconv" "strconv"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@ -14,28 +13,25 @@ type App struct {
DB *sql.DB 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 var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := c.Bind(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
return
} }
user, err := GetUserByUsername(app.DB, req.Username) user, err := GetUserByUsername(app.DB, req.Username)
if err != nil { if err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized) return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials")
return
} }
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
http.Error(w, "Invalid credentials", http.StatusUnauthorized) return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials")
return
} }
token, err := createToken(user.ID, user.Username, user.IsAdmin) token, err := createToken(user.ID, user.Username, user.IsAdmin)
if err != nil { if err != nil {
http.Error(w, "Error creating token", http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, "error creating token")
return
} }
response := LoginResponse{ response := LoginResponse{
@ -44,179 +40,125 @@ func (app *App) LoginHandler(w http.ResponseWriter, r *http.Request) {
IsAdmin: user.IsAdmin, IsAdmin: user.IsAdmin,
} }
w.Header().Set("Content-Type", "application/json") return c.JSON(http.StatusOK, response)
json.NewEncoder(w).Encode(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) schedules, err := GetAllSchedules(app.DB)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return }
return c.JSON(http.StatusOK, schedules)
} }
w.Header().Set("Content-Type", "application/json") func (app *App) CreateScheduleHandler(c echo.Context) error {
json.NewEncoder(w).Encode(schedules)
}
func (app *App) CreateScheduleHandler(w http.ResponseWriter, r *http.Request) {
var schedule Schedule var schedule Schedule
if err := json.NewDecoder(r.Body).Decode(&schedule); err != nil { if err := c.Bind(&schedule); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
return
} }
if err := CreateSchedule(app.DB, &schedule); err != nil { if err := CreateSchedule(app.DB, &schedule); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return
} }
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) { func (app *App) DeleteScheduleHandler(c echo.Context) error {
idStr := r.URL.Query().Get("id") id, err := strconv.Atoi(c.QueryParam("id"))
id, err := strconv.Atoi(idStr)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
return
} }
if err := DeleteSchedule(app.DB, id); err != nil { if err := DeleteSchedule(app.DB, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return
} }
w.WriteHeader(http.StatusOK) return c.NoContent(http.StatusOK)
} }
func (app *App) CreateUserHandler(w http.ResponseWriter, r *http.Request) { // User Handlers
var req struct { func (app *App) CreateUserHandler(c echo.Context) error {
Username string `json:"username"` var req CreateUserRequest
Password string `json:"password"` if err := c.Bind(&req); err != nil {
IsAdmin bool `json:"is_admin"` return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
} }
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
http.Error(w, "Error hashing password", http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, "error hashing password")
return
} }
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil { if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return
} }
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) users, err := GetAllUsers(app.DB)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return }
return c.JSON(http.StatusOK, users)
} }
w.Header().Set("Content-Type", "application/json") func (app *App) DeleteUserHandler(c echo.Context) error {
json.NewEncoder(w).Encode(users) id, err := strconv.Atoi(c.QueryParam("id"))
}
// 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)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusBadRequest, "invalid id")
return
} }
w.Header().Set("Content-Type", "application/json") if err := DeleteUser(app.DB, id); err != nil {
json.NewEncoder(w).Encode(entries) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
} }
func (app *App) GetAllTimeEntriesHandler(w http.ResponseWriter, r *http.Request) { return c.NoContent(http.StatusOK)
entries, err := GetAllTimeEntries(app.DB)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
w.Header().Set("Content-Type", "application/json") // Time Entry Handlers
json.NewEncoder(w).Encode(entries) func (app *App) CreateTimeEntryHandler(c echo.Context) error {
} userID := c.Get("user_id").(int)
func (app *App) CreateTimeEntryHandler(w http.ResponseWriter, r *http.Request) {
userIDStr := r.Header.Get("X-User-ID")
userID, _ := strconv.Atoi(userIDStr)
var entry TimeEntry var entry TimeEntry
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil { if err := c.Bind(&entry); err != nil {
log.Print("Error on Decoding occured") return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
http.Error(w, err.Error(), http.StatusBadRequest)
return
} }
entry.UserID = userID entry.UserID = userID
if err := CreateTimeEntry(app.DB, &entry); err != nil { if err := CreateTimeEntry(app.DB, &entry); err != nil {
log.Print("Error on creating time entry in Database occured") return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
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) { func (app *App) GetMyTimeEntriesHandler(c echo.Context) error {
idStr := r.URL.Query().Get("id") userID := c.Get("user_id").(int)
id, err := strconv.Atoi(idStr)
entries, err := GetTimeEntriesByUser(app.DB, userID)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return
} }
if err := DeleteUser(app.DB, id); err != nil { return c.JSON(http.StatusOK, entries)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} }
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) hours, err := GetWeeklyHours(app.DB)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
return
} }
return c.JSON(http.StatusOK, hours)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(hours)
} }

View file

@ -4,9 +4,13 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
) )
func main() { func main() {
// Database Setup
dbPath := os.Getenv("DB_PATH") dbPath := os.Getenv("DB_PATH")
if dbPath == "" { if dbPath == "" {
dbPath = "./timetracking.db" dbPath = "./timetracking.db"
@ -17,32 +21,78 @@ func main() {
app := &App{DB: db} 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 // Public routes
http.HandleFunc("/api/login", CORS(app.LoginHandler)) e.POST("/api/login", app.LoginHandler)
// Protected routes // Protected routes group
http.HandleFunc("/api/schedules", CORS(AuthMiddleware(app.GetSchedulesHandler))) protected := e.Group("/api")
http.HandleFunc("/api/time-entries", CORS(AuthMiddleware(app.CreateTimeEntryHandler))) protected.Use(JWTMiddleware())
http.HandleFunc("/api/my-time-entries", CORS(AuthMiddleware(app.GetMyTimeEntriesHandler))) {
protected.GET("/schedules", app.GetSchedulesHandler)
protected.POST("/time-entries", app.CreateTimeEntryHandler)
protected.GET("/my-time-entries", app.GetMyTimeEntriesHandler)
}
// Admin routes // Admin routes group
http.HandleFunc("/api/admin/schedules", CORS(AdminMiddleware(app.CreateScheduleHandler))) admin := e.Group("/api/admin")
http.HandleFunc("/api/admin/schedules/delete", CORS(AdminMiddleware(app.DeleteScheduleHandler))) admin.Use(JWTMiddleware())
http.HandleFunc("/api/admin/users", CORS(AdminMiddleware(app.CreateUserHandler))) admin.Use(AdminMiddleware())
http.HandleFunc("/api/admin/users/list", CORS(AdminMiddleware(app.GetUsersHandler))) {
http.HandleFunc("/api/admin/users/delete", CORS(AdminMiddleware(app.DeleteUserHandler))) // Neu admin.POST("/schedules", app.CreateScheduleHandler)
http.HandleFunc("/api/admin/time-entries", CORS(AdminMiddleware(app.GetAllTimeEntriesHandler))) admin.DELETE("/schedules/delete", app.DeleteScheduleHandler)
http.HandleFunc("/api/admin/weekly-hours", CORS(AdminMiddleware(app.GetWeeklyHoursHandler))) // Neu 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 // Static files
fs := http.FileServer(http.Dir("./static")) e.Static("/", "./static")
http.Handle("/", fs)
// Start server
port := os.Getenv("PORT") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8080" port = "8080"
} }
log.Printf("Server starting on port %s", port) 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/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
) )
var jwtSecret = []byte("your-secret-key-change-in-production") var jwtSecret = []byte("your-secret-key-change-in-production")
type Claims struct { // JWT Token Funktionen (bleiben gleich)
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
}
func createToken(userID int, username string, isAdmin bool) (string, error) { func createToken(userID int, username string, isAdmin bool) (string, error) {
claims := Claims{ claims := Claims{
UserID: userID, UserID: userID,
Username: username, Username: username,
IsAdmin: isAdmin, IsAdmin: isAdmin,
Exp: time.Now().Add(24 * time.Hour).Unix(),
} }
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) 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) payloadEncoded := base64.RawURLEncoding.EncodeToString(payload)
message := header + "." + payloadEncoded message := header + "." + payloadEncoded
@ -95,86 +66,68 @@ func verifyToken(tokenString string) (*Claims, error) {
return nil, err return nil, err
} }
var claims Claims var claimsMap map[string]interface{}
if err := json.Unmarshal(payload, &claims); err != nil { if err := json.Unmarshal(payload, &claimsMap); err != nil {
return nil, err 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 nil, fmt.Errorf("token expired")
} }
return &claims, nil
} }
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { claims := &Claims{
return func(w http.ResponseWriter, r *http.Request) { UserID: int(claimsMap["user_id"].(float64)),
authHeader := r.Header.Get("Authorization") 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 == "" { if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized) return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header")
return
} }
tokenString := strings.TrimPrefix(authHeader, "Bearer ") tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := verifyToken(tokenString) claims, err := verifyToken(tokenString)
if err != nil { if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized) return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
return
} }
r.Header.Set("X-User-ID", fmt.Sprintf("%d", claims.UserID)) // Store claims in context
r.Header.Set("X-Username", claims.Username) c.Set("user_id", claims.UserID)
r.Header.Set("X-Is-Admin", fmt.Sprintf("%t", claims.IsAdmin)) c.Set("username", claims.Username)
c.Set("is_admin", claims.IsAdmin)
next(w, r) return next(c)
}
} }
} }
func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc { // Admin Middleware
return AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { func AdminMiddleware() echo.MiddlewareFunc {
isAdmin := r.Header.Get("X-Is-Admin") == "true" return func(next echo.HandlerFunc) echo.HandlerFunc {
if !isAdmin { return func(c echo.Context) error {
http.Error(w, "Forbidden", http.StatusForbidden) isAdmin, ok := c.Get("is_admin").(bool)
return if !ok || !isAdmin {
return echo.NewHTTPError(http.StatusForbidden, "admin access required")
} }
next(w, r) return next(c)
}) }
} }
}
func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { // Custom Logger Middleware (optional - Echo hat bereits einen)
start := time.Now() func CustomLogger() echo.MiddlewareFunc {
return middleware.LoggerWithConfig(middleware.LoggerConfig{
wrapped := wrapResponseWriter(w) Format: "${time_rfc3339} | ${status} | ${latency_human} | ${method} ${uri}\n",
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)
}) })
} }

View file

@ -1,8 +1,6 @@
package main package main
import ( import "time"
"time"
)
type TimeEntry struct { type TimeEntry struct {
ID int `json:"id"` ID int `json:"id"`
@ -10,10 +8,10 @@ type TimeEntry struct {
ScheduleID int `json:"schedule_id"` ScheduleID int `json:"schedule_id"`
Date string `json:"date"` Date string `json:"date"`
Type string `json:"type"` Type string `json:"type"`
StartTime string `json:"start_time"` // Neu StartTime string `json:"start_time"`
EndTime string `json:"end_time"` // Neu EndTime string `json:"end_time"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Username string `json:"username"` // Neu - für Join Username string `json:"username"`
} }
type WeeklyHours struct { type WeeklyHours struct {
@ -33,25 +31,16 @@ type User struct {
type Schedule struct { type Schedule struct {
ID int `json:"id"` 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"` StartTime string `json:"start_time"`
EndTime string `json:"end_time"` EndTime string `json:"end_time"`
Type string `json:"type"` // "lesson" or "break" Type string `json:"type"`
Title string `json:"title"` 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 { type LoginRequest struct {
Username string `json:"username"` Username string `json:"username" validate:"required"`
Password string `json:"password"` Password string `json:"password" validate:"required"`
} }
type LoginResponse struct { type LoginResponse struct {
@ -59,3 +48,16 @@ type LoginResponse struct {
Username string `json:"username"` Username string `json:"username"`
IsAdmin bool `json:"is_admin"` 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"`
}