feat: change from Standart Library to echo framework to simplify development
This commit is contained in:
parent
d74046522b
commit
4514ce44a2
6 changed files with 254 additions and 275 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue