920 lines
24 KiB
Go
920 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/labstack/echo/v4"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type App struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func HandleError(c echo.Context, err *AppError) error {
|
|
log.Printf("[%s] %s", err.Code, err.Error())
|
|
|
|
return c.JSON(err.HTTPStatus, err.ToResponse())
|
|
}
|
|
|
|
func getClaims(c echo.Context) (*Claims, error) {
|
|
user, ok := c.Get("user").(*jwt.Token)
|
|
if !ok {
|
|
return nil, fmt.Errorf("JWT token missing or invalid")
|
|
}
|
|
|
|
claims, ok := user.Claims.(*Claims)
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed to parse JWT claims")
|
|
}
|
|
|
|
return claims, nil
|
|
}
|
|
|
|
func isDuplicateError(err error) bool {
|
|
return err != nil && (err.Error() == "UNIQUE constraint failed" ||
|
|
strings.Contains(err.Error(), "UNIQUE") ||
|
|
strings.Contains(err.Error(), "duplicate"))
|
|
}
|
|
|
|
func (app *App) LoginHandler(c echo.Context) error {
|
|
var req LoginRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Login-Daten"))
|
|
}
|
|
|
|
if req.Username == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Benutzername"))
|
|
}
|
|
if req.Password == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Passwort"))
|
|
}
|
|
|
|
user, err := GetUserByUsername(app.DB, req.Username)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrInvalidCredentialsMsg())
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
|
return HandleError(c, ErrInvalidCredentialsMsg())
|
|
}
|
|
|
|
if !user.IsAdmin {
|
|
_, err := VerifyLicenseFile()
|
|
if err != nil {
|
|
return c.JSON(http.StatusForbidden, map[string]string{
|
|
"error": "Lizenzfehler: " + err.Error(),
|
|
"code": "LICENSE_INVALID",
|
|
})
|
|
}
|
|
|
|
}
|
|
token, err := createToken(user.ID, user.Username, user.IsAdmin)
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
response := LoginResponse{
|
|
Token: token,
|
|
Username: user.Username,
|
|
IsAdmin: user.IsAdmin,
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
func (app *App) GetSchedulesHandler(c echo.Context) error {
|
|
schedules, err := GetAllSchedules(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
return c.JSON(http.StatusOK, schedules)
|
|
}
|
|
|
|
func (app *App) CreateScheduleHandler(c echo.Context) error {
|
|
var schedule Schedule
|
|
if err := c.Bind(&schedule); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Stundenplan-Daten"))
|
|
}
|
|
|
|
if schedule.StartTime == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Startzeit"))
|
|
}
|
|
if schedule.EndTime == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Endzeit"))
|
|
}
|
|
if schedule.Title == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Titel"))
|
|
}
|
|
|
|
if err := CreateSchedule(app.DB, &schedule); err != nil {
|
|
if isDuplicateError(err) {
|
|
return HandleError(c, ErrAlreadyExistsMsg("Stundenplan-Eintrag"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, map[string]string{"message": "Stundenplan erstellt"})
|
|
}
|
|
|
|
func (app *App) DeleteScheduleHandler(c echo.Context) error {
|
|
id, err := strconv.Atoi(c.QueryParam("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Stundenplan-ID"))
|
|
}
|
|
|
|
if err := DeleteSchedule(app.DB, id); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Stundenplan"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (app *App) GetYearlyHoursSummaryHandler(c echo.Context) error {
|
|
hours, err := GetYearlyHoursSummary(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if hours == nil {
|
|
hours = []WeeklyHours{}
|
|
}
|
|
return c.JSON(http.StatusOK, hours)
|
|
}
|
|
|
|
func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
|
var req struct {
|
|
UserID int `json:"user_id"`
|
|
Date string `json:"date"`
|
|
Hours float64 `json:"hours"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
|
}
|
|
|
|
if req.UserID == 0 {
|
|
return HandleError(c, ErrMissingFieldMsg("Benutzer"))
|
|
}
|
|
if req.Date == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Datum"))
|
|
}
|
|
if req.Hours == 0 {
|
|
return HandleError(c, ErrMissingFieldMsg("Stunden"))
|
|
}
|
|
|
|
entry := TimeEntry{
|
|
UserID: req.UserID,
|
|
Date: req.Date,
|
|
StartTime: "00:00",
|
|
EndTime: "00:00",
|
|
Type: "manual",
|
|
}
|
|
|
|
if err := CreateManualTimeEntry(app.DB, &entry, req.Hours); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (app *App) GetUsersHandler(c echo.Context) error {
|
|
users, err := GetAllUsers(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if users == nil {
|
|
users = []User{}
|
|
}
|
|
return c.JSON(http.StatusOK, users)
|
|
}
|
|
|
|
func (app *App) DeleteUserHandler(c echo.Context) error {
|
|
id, err := strconv.Atoi(c.QueryParam("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Benutzer-ID"))
|
|
}
|
|
|
|
if id == 1 {
|
|
return HandleError(c, ErrProtectedUserMsg())
|
|
}
|
|
|
|
if err := DeleteUser(app.DB, id); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (app *App) CreateTimeEntryHandler(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
var entry TimeEntry
|
|
if err := c.Bind(&entry); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
|
}
|
|
|
|
if entry.Date == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Datum"))
|
|
}
|
|
if entry.StartTime == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Startzeit"))
|
|
}
|
|
if entry.EndTime == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Endzeit"))
|
|
}
|
|
|
|
entry.UserID = claims.UserID
|
|
|
|
if err := CreateTimeEntry(app.DB, &entry); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, map[string]string{"message": "Zeiteintrag erstellt"})
|
|
}
|
|
|
|
func (app *App) GetMyTimeEntriesHandler(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
entries, err := GetTimeEntriesByUser(app.DB, claims.UserID)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if entries == nil {
|
|
entries = []TimeEntry{}
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, entries)
|
|
}
|
|
|
|
func (app *App) GetWeekDates(c echo.Context) error {
|
|
year, err := strconv.Atoi(c.QueryParam("year"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Jahr"))
|
|
}
|
|
|
|
week, err := strconv.Atoi(c.QueryParam("week"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
|
}
|
|
|
|
if week < 1 || week > 53 {
|
|
return HandleError(c, ErrInvalidInputMsg("Woche (muss zwischen 1 und 53 liegen)"))
|
|
}
|
|
|
|
dates := calculateWeekDates(year, week)
|
|
return c.JSON(http.StatusOK, dates)
|
|
}
|
|
|
|
func (app *App) CheckWeekHasEntries(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
year, err := strconv.Atoi(c.QueryParam("year"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Jahr"))
|
|
}
|
|
|
|
week, err := strconv.Atoi(c.QueryParam("week"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
|
}
|
|
|
|
hasEntries, err := CheckUserHasEntriesForWeek(app.DB, claims.UserID, year, week)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]bool{"has_entries": hasEntries})
|
|
}
|
|
|
|
func (app *App) GetAllTimeEntriesHandler(c echo.Context) error {
|
|
entries, err := GetAllTimeEntries(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if entries == nil {
|
|
entries = []TimeEntry{}
|
|
}
|
|
return c.JSON(http.StatusOK, entries)
|
|
}
|
|
|
|
func (app *App) GetWeeklyHoursHandler(c echo.Context) error {
|
|
hours, err := GetWeeklyHours(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if hours == nil {
|
|
hours = []WeeklyHours{}
|
|
}
|
|
return c.JSON(http.StatusOK, hours)
|
|
}
|
|
|
|
func (app *App) DeleteWeekEntries(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
year, err := strconv.Atoi(c.QueryParam("year"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Jahr"))
|
|
}
|
|
|
|
week, err := strconv.Atoi(c.QueryParam("week"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
|
}
|
|
|
|
if err := DeleteNonManualTimeEntriesByUserAndWeek(app.DB, claims.UserID, year, week); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
type WeekDates struct {
|
|
Year int `json:"year"`
|
|
Week int `json:"week"`
|
|
Dates map[string]string `json:"dates"`
|
|
Range string `json:"range"`
|
|
}
|
|
|
|
func calculateWeekDates(year, week int) WeekDates {
|
|
jan4 := time.Date(year, time.January, 4, 0, 0, 0, 0, time.UTC)
|
|
|
|
weekday := int(jan4.Weekday())
|
|
if weekday == 0 {
|
|
weekday = 7
|
|
}
|
|
daysToMonday := weekday - 1
|
|
mondayWeek1 := jan4.AddDate(0, 0, -daysToMonday)
|
|
|
|
targetMonday := mondayWeek1.AddDate(0, 0, (week-1)*7)
|
|
|
|
dates := make(map[string]string)
|
|
weekDays := []string{"0", "1", "2", "3", "4"}
|
|
|
|
var firstDate, lastDate time.Time
|
|
for i, day := range weekDays {
|
|
date := targetMonday.AddDate(0, 0, i)
|
|
dates[day] = date.Format("2006-01-02")
|
|
|
|
if i == 0 {
|
|
firstDate = date
|
|
}
|
|
if i == 4 {
|
|
lastDate = date
|
|
}
|
|
}
|
|
|
|
rangeStr := firstDate.Format("2006-01-02") + " bis " + lastDate.Format("2006-01-02")
|
|
|
|
return WeekDates{
|
|
Year: year,
|
|
Week: week,
|
|
Dates: dates,
|
|
Range: rangeStr,
|
|
}
|
|
}
|
|
|
|
type BatchTimeEntryRequest struct {
|
|
Entries []struct {
|
|
ScheduleID int `json:"schedule_id"`
|
|
Date string `json:"date"`
|
|
Type string `json:"type"`
|
|
StartTime string `json:"start_time"`
|
|
EndTime string `json:"end_time"`
|
|
} `json:"entries"`
|
|
}
|
|
|
|
func (app *App) CreateBatchTimeEntriesHandler(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
var req BatchTimeEntryRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
|
}
|
|
|
|
if len(req.Entries) == 0 {
|
|
return HandleError(c, ErrMissingFieldMsg("Zeiteinträge"))
|
|
}
|
|
|
|
if len(req.Entries) > 0 {
|
|
firstDate := req.Entries[0].Date
|
|
t, err := time.Parse("2006-01-02", firstDate)
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Datum-Format"))
|
|
}
|
|
year, week := t.ISOWeek()
|
|
|
|
if err := DeleteNonManualTimeEntriesByUserAndWeek(app.DB, claims.UserID, year, week); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
}
|
|
|
|
tx, err := app.DB.Begin()
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
stmt, err := tx.Prepare("INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time) VALUES (?, ?, ?, ?, ?, ?)")
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, entry := range req.Entries {
|
|
_, err := stmt.Exec(claims.UserID, entry.ScheduleID, entry.Date, entry.Type, entry.StartTime, entry.EndTime)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, map[string]string{"message": "Zeiteinträge erstellt"})
|
|
}
|
|
|
|
func (app *App) UpdateUserHandler(c echo.Context) error {
|
|
userID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Benutzer-ID"))
|
|
}
|
|
|
|
if userID == 1 {
|
|
return HandleError(c, ErrProtectedUserMsg())
|
|
}
|
|
|
|
var req UpdateUserRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Benutzerdaten"))
|
|
}
|
|
|
|
if req.YearlyHours <= 0 {
|
|
return HandleError(c, ErrInvalidInputMsg("Jahresarbeitsstunden (muss positiv sein)"))
|
|
}
|
|
|
|
if err := UpdateUser(app.DB, userID, req.YearlyHours); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func (app *App) ResetPasswordHandler(c echo.Context) error {
|
|
userID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Benutzer-ID"))
|
|
}
|
|
|
|
var req ResetPasswordRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Passwort-Daten"))
|
|
}
|
|
|
|
if len(req.NewPassword) < 6 {
|
|
return HandleError(c, ErrInvalidInputMsg("Passwort (mindestens 6 Zeichen)"))
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
if err := ResetUserPassword(app.DB, userID, string(hashedPassword)); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func (app *App) UpdateTimeEntryHandler(c echo.Context) error {
|
|
entryID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-ID"))
|
|
}
|
|
|
|
var req UpdateTimeEntryRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-Daten"))
|
|
}
|
|
|
|
if req.Date == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Datum"))
|
|
}
|
|
if req.StartTime == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Startzeit"))
|
|
}
|
|
if req.EndTime == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Endzeit"))
|
|
}
|
|
|
|
if err := UpdateTimeEntry(app.DB, entryID, req.Date, req.StartTime, req.EndTime, req.Type); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Zeiteintrag"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusOK)
|
|
}
|
|
|
|
func (app *App) DeleteTimeEntryHandler(c echo.Context) error {
|
|
entryID, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Zeiteintrag-ID"))
|
|
}
|
|
|
|
if err := DeleteTimeEntry(app.DB, entryID); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Zeiteintrag"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (app *App) GetMyInfoHandler(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
user, err := GetUserByID(app.DB, claims.UserID)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Benutzer"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, user)
|
|
}
|
|
|
|
func (app *App) CreateUserHandler(c echo.Context) error {
|
|
var req CreateUserRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Benutzerdaten"))
|
|
}
|
|
|
|
if req.Username == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Benutzername"))
|
|
}
|
|
if req.Password == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Passwort"))
|
|
}
|
|
if len(req.Password) < 6 {
|
|
return HandleError(c, ErrInvalidInputMsg("Passwort (mindestens 6 Zeichen)"))
|
|
}
|
|
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
if req.YearlyHours == 0 {
|
|
req.YearlyHours = 60.0
|
|
}
|
|
|
|
if err := CreateUser(app.DB, req.Username, string(hashedPassword), req.IsAdmin, req.YearlyHours); err != nil {
|
|
if isDuplicateError(err) {
|
|
return HandleError(c, ErrAlreadyExistsMsg("Benutzername"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusCreated)
|
|
}
|
|
|
|
func (app *App) GetSchoolYearsHandler(c echo.Context) error {
|
|
years, err := GetAllSchoolYears(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if years == nil {
|
|
years = []SchoolYear{}
|
|
}
|
|
return c.JSON(http.StatusOK, years)
|
|
}
|
|
|
|
func (app *App) CreateSchoolYearHandler(c echo.Context) error {
|
|
var req CreateSchoolYearRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Schuljahr-Daten"))
|
|
}
|
|
|
|
if req.Name == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Name"))
|
|
}
|
|
if req.StartDate == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Startdatum"))
|
|
}
|
|
if req.EndDate == "" {
|
|
return HandleError(c, ErrMissingFieldMsg("Enddatum"))
|
|
}
|
|
|
|
if err := CreateSchoolYear(app.DB, req.Name, req.StartDate, req.EndDate); err != nil {
|
|
if isDuplicateError(err) {
|
|
return HandleError(c, ErrAlreadyExistsMsg("Schuljahr"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusCreated)
|
|
}
|
|
|
|
func (app *App) SetActiveSchoolYearHandler(c echo.Context) error {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Schuljahr-ID"))
|
|
}
|
|
|
|
if err := SetActiveSchoolYear(app.DB, id); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Schuljahr"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (app *App) GetActiveSchoolYearHandler(c echo.Context) error {
|
|
year, err := GetActiveSchoolYear(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if year == nil {
|
|
return c.JSON(http.StatusOK, map[string]any{"active": false})
|
|
}
|
|
return c.JSON(http.StatusOK, year)
|
|
}
|
|
|
|
func (app *App) GenerateYearlySummaryPDFHandler(c echo.Context) error {
|
|
schoolYear, err := GetActiveSchoolYear(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if schoolYear == nil {
|
|
return HandleError(c, ErrNoActiveSchoolYearMsg())
|
|
}
|
|
|
|
summary, err := GetYearlyHoursSummary(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
pdfBytes, err := GenerateYearlySummaryPDF(schoolYear, summary)
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
filename := fmt.Sprintf("Jahresuebersicht_%s.pdf", schoolYear.Name)
|
|
c.Response().Header().Set("Content-Type", "application/pdf")
|
|
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
|
|
|
return c.Blob(http.StatusOK, "application/pdf", pdfBytes)
|
|
}
|
|
|
|
func (app *App) DeleteSchoolYearHandler(c echo.Context) error {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Schuljahr-ID"))
|
|
}
|
|
|
|
if err := DeleteSchoolYear(app.DB, id); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return HandleError(c, ErrNotFoundMsg("Schuljahr"))
|
|
}
|
|
if err.Error() == "cannot delete active school year" {
|
|
return HandleError(c, &AppError{
|
|
Code: "CANNOT_DELETE_ACTIVE_SCHOOL_YEAR",
|
|
Message: "Aktives Schuljahr kann nicht gelöscht werden",
|
|
HTTPStatus: http.StatusBadRequest,
|
|
})
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (app *App) ChangeMyPasswordHandler(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
var req ChangePasswordRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Anfragedaten"))
|
|
}
|
|
|
|
if len(req.NewPassword) < 6 {
|
|
return HandleError(c, ErrInvalidInputMsg("Neues Passwort muss mind. 6 Zeichen lang sein"))
|
|
}
|
|
|
|
var currentHash string
|
|
err = app.DB.QueryRow("SELECT password FROM users WHERE id = ?", claims.UserID).Scan(¤tHash)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(req.OldPassword)); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Altes Passwort ist falsch"))
|
|
}
|
|
|
|
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
_, err = app.DB.Exec("UPDATE users SET password = ? WHERE id = ?", string(newHash), claims.UserID)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "Passwort erfolgreich geändert"})
|
|
}
|
|
|
|
func (app *App) GetLogoHandler(c echo.Context) error {
|
|
if _, err := os.Stat("school_logo.png"); os.IsNotExist(err) {
|
|
return c.NoContent(http.StatusNotFound)
|
|
}
|
|
c.Response().Header().Set("Cache-Control", "no-cache")
|
|
return c.File("school_logo.png")
|
|
}
|
|
|
|
func (app *App) UploadLogoHandler(c echo.Context) error {
|
|
file, err := c.FormFile("logo")
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Keine Datei hochgeladen"))
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.Create("school_logo.png")
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err = io.Copy(dst, src); err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "Logo erfolgreich hochgeladen"})
|
|
}
|
|
|
|
func (app *App) GetLicenseStatusHandler(c echo.Context) error {
|
|
var count int
|
|
app.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
|
|
|
status := GetCurrentLicenseStatus(nil)
|
|
status.UserCount = count
|
|
|
|
if status.IsValid && status.MaxUsers > 0 && count > status.MaxUsers {
|
|
status.IsValid = false
|
|
status.Message = fmt.Sprintf("Benutzerlimit überschritten (%d / %d)", count, status.MaxUsers)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, status)
|
|
}
|
|
|
|
func (app *App) UploadLicenseHandler(c echo.Context) error {
|
|
file, err := c.FormFile("license")
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Keine Datei"))
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := os.Create("license.lic")
|
|
if err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err = io.Copy(dst, src); err != nil {
|
|
return HandleError(c, ErrInternalMsg(err))
|
|
}
|
|
|
|
if _, err := VerifyLicenseFile(); err != nil {
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "Lizenz hochgeladen, aber ungültig: " + err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "Lizenz erfolgreich aktiviert"})
|
|
}
|
|
|
|
func (app *App) GetAllSubstitutionsHandler(c echo.Context) error {
|
|
subs, err := GetAllSubstitutions(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if subs == nil {
|
|
subs = []Substitution{}
|
|
}
|
|
return c.JSON(http.StatusOK, subs)
|
|
}
|
|
|
|
func (app *App) GetOpenSubstitutionsHandler(c echo.Context) error {
|
|
subs, err := GetOpenSubstitutions(app.DB)
|
|
if err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
if subs == nil {
|
|
subs = []Substitution{}
|
|
}
|
|
return c.JSON(http.StatusOK, subs)
|
|
}
|
|
|
|
func (app *App) AcceptSubstitutionHandler(c echo.Context) error {
|
|
claims, err := getClaims(c)
|
|
if err != nil {
|
|
return HandleError(c, ErrUnauthorizedMsg())
|
|
}
|
|
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("ID"))
|
|
}
|
|
|
|
if err := AcceptSubstitution(app.DB, id, claims.UserID); err != nil {
|
|
if err.Error() == "Vertretung wurde bereits vergeben oder existiert nicht" {
|
|
return HandleError(c, ErrAlreadyExistsMsg("Diese Vertretung ist leider schon vergeben"))
|
|
}
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"message": "Vertretung erfolgreich übernommen!"})
|
|
}
|
|
|
|
func (app *App) CreateSubstitutionHandler(c echo.Context) error {
|
|
var req CreateSubstitutionRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("Eingabedaten"))
|
|
}
|
|
if err := CreateSubstitution(app.DB, req.Date, req.StartTime, req.EndTime, req.Title, req.Notes, req.ScheduleID); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
return c.JSON(http.StatusCreated, map[string]string{"message": "Vertretung ausgeschrieben"})
|
|
}
|
|
|
|
func (app *App) DeleteSubstitutionHandler(c echo.Context) error {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
return HandleError(c, ErrInvalidInputMsg("ID"))
|
|
}
|
|
if err := DeleteSubstitution(app.DB, id); err != nil {
|
|
return HandleError(c, ErrDatabaseMsg(err))
|
|
}
|
|
return c.NoContent(http.StatusOK)
|
|
}
|