3 Commits

Author SHA1 Message Date
c29a952e1d fix: changed backup filename for better sorting of backups
All checks were successful
Tests / Run Go Tests (push) Successful in 1m43s
2026-02-27 16:18:28 +01:00
b12a467ef9 updated install script to also reconfigure/update everything 2026-02-26 21:43:40 +01:00
8bb1777519 feat: booking can only in between specified hours
every booking happening outside these hours will be clamped to the hours
also added few more config options + regex filters
2026-02-25 01:02:15 +01:00
11 changed files with 443 additions and 246 deletions

3
.gitignore vendored
View File

@@ -30,7 +30,8 @@ DB/pg_data
.env.* .env.*
.env .env
!.env.example
Docker/config
.idea .idea
.vscode .vscode

View File

@@ -10,7 +10,6 @@ import (
"errors" "errors"
"log" "log"
"net/http" "net/http"
"time"
) )
// Relevant for arduino inputs -> creates new Booking from get and put method // Relevant for arduino inputs -> creates new Booking from get and put method
@@ -40,7 +39,7 @@ func createBooking(w http.ResponseWriter, r *http.Request) {
} }
booking := (*models.Booking).FromUrlParams(nil, r.URL.Query()) booking := (*models.Booking).FromUrlParams(nil, r.URL.Query())
booking.Timestamp = time.Now() // booking.Timestamp = time.Now()
if booking.Verify() { if booking.Verify() {
err := booking.Insert() err := booking.Insert()
if errors.Is(models.SameBookingError{}, err) { if errors.Is(models.SameBookingError{}, err) {

View File

@@ -52,6 +52,8 @@ func main() {
defer models.DB.(*sql.DB).Close() defer models.DB.(*sql.DB).Close()
models.Options = configure()
if helper.GetEnv("GO_ENV", "production") != "debug" { if helper.GetEnv("GO_ENV", "production") != "debug" {
err = Migrate() err = Migrate()
if err != nil { if err != nil {
@@ -114,3 +116,10 @@ func loggingMiddleware(next http.Handler) http.Handler {
slog.Info("Completet Request", slog.String("Time", time.Since(start).String())) slog.Info("Completet Request", slog.String("Time", time.Since(start).String()))
}) })
} }
func configure() models.BookingOptions {
return models.BookingOptions{
AllowOutOfBounds: helper.GetEnv("BOOKING_OUT_OF_BOUNDS", "false") == "true",
AllowUnknownUser: helper.GetEnv("BOOKING_FOR_UNKNOWN_USER", "false") == "true",
}
}

View File

@@ -39,6 +39,11 @@ type Booking struct {
Valid bool `json:"valid"` Valid bool `json:"valid"`
} }
type BookingOptions struct {
AllowOutOfBounds bool
AllowUnknownUser bool
}
type IDatabase interface { type IDatabase interface {
Prepare(query string) (*sql.Stmt, error) Prepare(query string) (*sql.Stmt, error)
Exec(query string, args ...any) (sql.Result, error) Exec(query string, args ...any) (sql.Result, error)
@@ -46,6 +51,8 @@ type IDatabase interface {
var DB IDatabase var DB IDatabase
var Options BookingOptions
func (b *Booking) NewBooking(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking { func (b *Booking) NewBooking(cardUid string, gereatId int16, checkInOut int16, typeId int8) Booking {
bookingType, err := GetBookingTypeById(typeId) bookingType, err := GetBookingTypeById(typeId)
if err != nil { if err != nil {
@@ -92,10 +99,44 @@ func (b *Booking) Verify() bool {
} else { } else {
b.BookingType.Name = bookingType.Name b.BookingType.Name = bookingType.Name
} }
user, err := GetUserByCardUID(b.CardUID)
if err == sql.ErrNoRows {
log.Println("Cannot find user with given CardUID")
return Options.AllowUnknownUser // if allow do not fail verify if not allow fail verify
}
if err != nil {
slog.Error("Cannot get user from CardUID", "error", err)
return false
}
if bookingOutOfBounds(b, &user) {
auditLog, closeLog := logs.NewAudit()
defer closeLog()
if !Options.AllowOutOfBounds {
return false
}
oldTime := b.Timestamp
if oldTime.IsZero() {
oldTime = time.Now()
}
if b.CheckInOut%2 == 1 && b.CheckInOut < 200 { //kommen Booking
b.Timestamp = user.ArbeitMinStartTime(oldTime)
} else {
b.Timestamp = user.ArbeitMaxEndeTime(oldTime)
}
auditLog.Printf("Buchung (%s) von '%s' außerhalb der regulaeren Zeit. Verschieben der Zeit %s -> %s", b.GetBookingType(), user.CardUID, oldTime.Format(time.TimeOnly), b.Timestamp.Format(time.TimeOnly))
slog.Info("Booking is out of work time bounds, setting time to match worktime bounds", "new_time", b.Timestamp.String(), "old_time", oldTime)
}
return true return true
} }
func (b *Booking) Insert() error { func (b *Booking) Insert() error {
if !b.Timestamp.IsZero() {
return b.InsertWithTimestamp()
}
if !checkLastBooking(*b) { if !checkLastBooking(*b) {
return SameBookingError{} return SameBookingError{}
} }
@@ -224,20 +265,21 @@ func (b *Booking) Update(nb Booking) {
b.GeraetID = nb.GeraetID b.GeraetID = nb.GeraetID
} }
if b.Timestamp != nb.Timestamp { if b.Timestamp != nb.Timestamp {
auditLog.Printf("Änderung in Buchung %d von '%s': Buchungszeit (%s -> %s).", b.CounterId, b.CardUID, b.Timestamp.Format("15:04"), nb.Timestamp.Format("15:04)")) auditLog.Printf("Änderung in Buchung %d von '%s': Buchungszeit (%s -> %s).", b.CounterId, b.CardUID, b.Timestamp.Format(time.TimeOnly), nb.Timestamp.Format(time.TimeOnly))
b.Timestamp = nb.Timestamp b.Timestamp = nb.Timestamp
} }
} }
func checkLastBooking(b Booking) bool { func checkLastBooking(b Booking) bool {
var check_in_out int var check_in_out int
slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String()) var timestamp time.Time
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`)) slog.Debug("Checking with timestamp:", "timestamp", b.Timestamp)
stmt, err := DB.Prepare((`SELECT check_in_out, timestamp FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp" <= $2 ORDER BY "timestamp" DESC LIMIT 1;`))
if err != nil { if err != nil {
log.Fatalf("Error preparing query: %v", err) log.Fatalf("Error preparing query: %v", err)
return false return false
} }
err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out) err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out, &timestamp)
slog.Info("Checking last bookings check_in_out", "Check", check_in_out) slog.Info("Checking last bookings check_in_out", "Check", check_in_out)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return true return true
@@ -246,9 +288,13 @@ func checkLastBooking(b Booking) bool {
log.Println("Error checking last booking: ", err) log.Println("Error checking last booking: ", err)
return false return false
} }
if int16(check_in_out)%2 == b.CheckInOut%2 { if int16(check_in_out)%2 == b.CheckInOut%2 {
return false return false
} }
if timestamp.Equal(b.Timestamp) {
return false
}
return true return true
} }
@@ -257,8 +303,6 @@ func (b *Booking) UpdateTime(newTime time.Time) {
if hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute() { if hour == b.Timestamp.Hour() && minute == b.Timestamp.Minute() {
return return
} }
// TODO: add check for time overlap
var newBooking Booking var newBooking Booking
newBooking.Timestamp = time.Date(b.Timestamp.Year(), b.Timestamp.Month(), b.Timestamp.Day(), hour, minute, 0, 0, b.Timestamp.Location()) newBooking.Timestamp = time.Date(b.Timestamp.Year(), b.Timestamp.Month(), b.Timestamp.Day(), hour, minute, 0, 0, b.Timestamp.Location())
if b.CheckInOut < 3 { if b.CheckInOut < 3 {
@@ -268,14 +312,11 @@ func (b *Booking) UpdateTime(newTime time.Time) {
newBooking.CheckInOut = 4 newBooking.CheckInOut = 4
} }
b.Update(newBooking) b.Update(newBooking)
// TODO Check verify
if b.Verify() { if b.Verify() {
b.Save() b.Save()
} else { } else {
log.Println("Cannot save updated booking!", b.ToString()) log.Println("Cannot save updated booking!", b.ToString())
} }
// b.Verify()
// b.Save()
} }
func (b *Booking) ToString() string { func (b *Booking) ToString() string {
@@ -327,3 +368,12 @@ func GetBookingTypesCached() []BookingType {
} }
return types.([]BookingType) return types.([]BookingType)
} }
func bookingOutOfBounds(b *Booking, u *User) bool {
bookingTime := b.Timestamp
if b.Timestamp.IsZero() {
bookingTime = time.Now()
}
res := bookingTime.Before(u.ArbeitMinStartTime(bookingTime)) || bookingTime.After(u.ArbeitMaxEndeTime(bookingTime))
return res
}

View File

@@ -28,6 +28,8 @@ type User struct {
ArbeitszeitPerTag float32 //`json:"arbeitszeit_per_tag"` ArbeitszeitPerTag float32 //`json:"arbeitszeit_per_tag"`
ArbeitszeitPerWoche float32 //`json:"arbeitszeit_per_woche"` ArbeitszeitPerWoche float32 //`json:"arbeitszeit_per_woche"`
Overtime time.Duration Overtime time.Duration
ArbeitMinStart time.Time
ArbeitMaxEnde time.Time
} }
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) { func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
@@ -65,8 +67,41 @@ func (u *User) GetReportedOvertime(startDate time.Time) (time.Duration, error) {
return overtime, nil return overtime, nil
} }
func GetUserByCardUID(cardUid string) (User, error) {
var user User
qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE card_uid = $1;`))
if err != nil {
return user, err
}
err = qStr.QueryRow(cardUid).Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde)
if err != nil {
return user, err
}
return user, nil
}
func (u *User) ArbeitMinStartTime(date time.Time) time.Time {
if date.Hour() > 0 {
date = date.Truncate(24 * time.Hour).Add(-time.Hour)
}
date = date.Truncate(time.Hour)
slog.Info("Date truncate", "date", date)
return date.Add(time.Hour*time.Duration(u.ArbeitMinStart.Hour()) + time.Minute*time.Duration(u.ArbeitMinStart.Minute()))
}
func (u *User) ArbeitMaxEndeTime(date time.Time) time.Time {
if date.Hour() > 0 {
date = date.Truncate(24 * time.Hour).Add(-time.Hour)
}
date = date.Truncate(time.Hour)
slog.Info("Date truncate", "date", date)
return date.Add(time.Hour*time.Duration(u.ArbeitMaxEnde.Hour()) + time.Minute*time.Duration(u.ArbeitMaxEnde.Minute()))
}
func GetAllUsers() ([]User, error) { func GetAllUsers() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname,arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten;`)) qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten;`))
var users []User var users []User
if err != nil { if err != nil {
return users, err return users, err
@@ -80,34 +115,7 @@ func GetAllUsers() ([]User, error) {
for rows.Next() { for rows.Next() {
var user User var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil { if err := rows.Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde); err != nil {
log.Println("Error creating user!", err)
continue
}
users = append(users, user)
}
if err = rows.Err(); err != nil {
return users, nil
}
return users, nil
}
func (u *User) GetAll() ([]User, error) {
qStr, err := DB.Prepare((`SELECT card_uid, vorname, nachname FROM s_personal_daten;`))
var users []User
if err != nil {
return users, err
}
defer qStr.Close()
rows, err := qStr.Query()
if err != nil {
return users, err
}
defer rows.Close()
for rows.Next() {
var user User
if err := rows.Scan(&user.CardUID, &user.Vorname, &user.Name); err != nil {
log.Println("Error creating user!", err) log.Println("Error creating user!", err)
continue continue
} }
@@ -167,11 +175,11 @@ func (u *User) CheckOut() error {
func GetUserByPersonalNr(personalNummer int) (User, error) { func GetUserByPersonalNr(personalNummer int) (User, error) {
var user User var user User
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = $1;`)) qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE personal_nummer = $1;`))
if err != nil { if err != nil {
return user, err return user, err
} }
err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche) err = qStr.QueryRow(personalNummer).Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde)
if err != nil { if err != nil {
return user, err return user, err
@@ -185,7 +193,7 @@ func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
return users, errors.New("No personalNumbers provided") return users, errors.New("No personalNumbers provided")
} }
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag, arbeitszeit_per_woche FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`)) qStr, err := DB.Prepare((`SELECT personal_nummer, vorname, nachname, card_uid, arbeitszeit_per_tag, arbeitszeit_per_woche, arbeitszeit_min_start, arbeitszeit_max_ende FROM s_personal_daten WHERE personal_nummer = ANY($1::int[]);`))
if err != nil { if err != nil {
return users, err return users, err
} }
@@ -200,7 +208,7 @@ func GetUserByPersonalNrMulti(personalNummerMulti []int) ([]User, error) {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var user User var user User
if err := rows.Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche); err != nil { if err := rows.Scan(&user.PersonalNummer, &user.Vorname, &user.Name, &user.CardUID, &user.ArbeitszeitPerTag, &user.ArbeitszeitPerWoche, &user.ArbeitMinStart, &user.ArbeitMaxEnde); err != nil {
return users, err return users, err
} }
users = append(users, user) users = append(users, user)
@@ -246,6 +254,7 @@ func (u *User) ChangePass(password, newPassword string) (bool, error) {
} }
func (u *User) GetTeamMembers() ([]User, error) { func (u *User) GetTeamMembers() ([]User, error) {
var teamMemberPNrs []int
var teamMembers []User var teamMembers []User
qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1 ORDER BY "nachname";`) qStr, err := DB.Prepare(`SELECT personal_nummer FROM s_personal_daten WHERE vorgesetzter_pers_nr = $1 ORDER BY "nachname";`)
if err != nil { if err != nil {
@@ -261,12 +270,16 @@ func (u *User) GetTeamMembers() ([]User, error) {
for rows.Next() { for rows.Next() {
var personalNr int var personalNr int
err := rows.Scan(&personalNr) err := rows.Scan(&personalNr)
user, err := GetUserByPersonalNr(personalNr) teamMemberPNrs = append(teamMemberPNrs, personalNr)
if err != nil { if err != nil {
log.Println("Error getting user!") log.Println("Error getting user!")
return teamMembers, err return teamMembers, err
} }
teamMembers = append(teamMembers, user) }
teamMembers, err = GetUserByPersonalNrMulti(teamMemberPNrs)
if err != nil {
log.Println("Error getting users!")
return teamMembers, err
} }
return teamMembers, nil return teamMembers, nil
@@ -343,22 +356,6 @@ LIMIT 1;
return lastSub return lastSub
} }
func (u *User) GetFromCardUID(card_uid string) (User, error) {
user := User{}
var err error
qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM s_personal_daten WHERE card_uid = $1;`))
if err != nil {
return user, err
}
err = qStr.QueryRow(card_uid).Scan(&user.PersonalNummer, &user.CardUID, &user.Vorname, &user.Name, &user.ArbeitszeitPerTag)
if err != nil {
return user, err
}
return user, nil
}
func (u *User) IsSuperior(e User) bool { func (u *User) IsSuperior(e User) bool {
var isSuperior int var isSuperior int
qStr, err := DB.Prepare(`SELECT COUNT(1) FROM s_personal_daten WHERE personal_nummer = $1 AND vorgesetzter_pers_nr = $2`) qStr, err := DB.Prepare(`SELECT COUNT(1) FROM s_personal_daten WHERE personal_nummer = $1 AND vorgesetzter_pers_nr = $2`)
@@ -372,7 +369,6 @@ func (u *User) IsSuperior(e User) bool {
return false return false
} }
return isSuperior == 1 return isSuperior == 1
} }
func getMonday(ts time.Time) time.Time { func getMonday(ts time.Time) time.Time {

View File

@@ -424,10 +424,12 @@ GROUP BY
// returns bool wheter the workday was ended with an automatic logout // returns bool wheter the workday was ended with an automatic logout
func (d *WorkDay) RequiresAction() bool { func (d *WorkDay) RequiresAction() bool {
if len(d.Bookings) == 0 { for i := range d.Bookings {
return false if d.Bookings[i].CheckInOut > 250 {
return true
} }
return d.Bookings[len(d.Bookings)-1].CheckInOut == 254 }
return false
} }
func (d *WorkDay) GetDayProgress(u User) int8 { func (d *WorkDay) GetDayProgress(u User) int8 {

View File

@@ -61,6 +61,8 @@ templ SettingsPage(status int) {
<div class="grid-cell col-span-3"> <div class="grid-cell col-span-3">
<p>Nutzername: <span class="text-neutral-500">{ user.Vorname } { user.Name }</span></p> <p>Nutzername: <span class="text-neutral-500">{ user.Vorname } { user.Name }</span></p>
<p>Personalnummer: <span class="text-neutral-500">{ user.PersonalNummer }</span></p> <p>Personalnummer: <span class="text-neutral-500">{ user.PersonalNummer }</span></p>
<p>Frühester Arbeitsbegin: <span class="text-neutral-500">{ user.ArbeitMinStart.Format("15:06") } Uhr</span></p>
<p>Spätester Arbeitsende: <span class="text-neutral-500">{ user.ArbeitMaxEnde.Format("15:06") } Uhr</span></p>
<p>Arbeitszeit pro Tag: <span class="text-neutral-500">{ helper.FormatDuration(user.ArbeitszeitProTag()) }</span></p> <p>Arbeitszeit pro Tag: <span class="text-neutral-500">{ helper.FormatDuration(user.ArbeitszeitProTag()) }</span></p>
<p>Arbeitszeit pro Woche: <span class="text-neutral-500">{ helper.FormatDuration(user.ArbeitszeitProWoche()) }</span></p> <p>Arbeitszeit pro Woche: <span class="text-neutral-500">{ helper.FormatDuration(user.ArbeitszeitProWoche()) }</span></p>
</div> </div>

View File

@@ -1,6 +1,6 @@
# cron-timing: 05 01 * * 1 # cron-timing: 05 01 * * 1
container_name="arbeitszeitmessung-main-db-1" container_name="arbeitszeitmessung-main-db-1"
filename=backup-$(date '+%d%m%Y').sql filename=backup-$(date '+%Y%m%d').sql
backup_folder=__BACKUP_FOLDER__ backup_folder=__BACKUP_FOLDER__
database_name=__DATABASE__ database_name=__DATABASE__
docker exec $container_name pg_dump $database_name > $backup_folder/$filename docker exec $container_name pg_dump $database_name > $backup_folder/$filename

View File

@@ -1,3 +1,4 @@
# cron-timing: 01 00 01 01 *
# Calls endpoint to write all public Holidays for the current year inside a database. # Calls endpoint to write all public Holidays for the current year inside a database.
port=__PORT__ port=__PORT__
curl localhost:$port/auto/feiertage curl localhost:$port/auto/feiertage

View File

@@ -1,13 +1,16 @@
POSTGRES_USER=root # Postgres ADMIN Nutzername POSTGRES_USER=root # Postgres ADMIN Nutzername. regex:^\w+$
POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort
POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung) POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung). regex:^\w+$
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung) POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...) POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name. regex:^[a-z]+$
POSTGRES_PORT=5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$ POSTGRES_PORT=5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
TZ=Europe/Berlin # Zeitzone TZ=Europe/Berlin # Zeitzone
API_TOKEN=dont_access # API Token für ESP Endpoints API_TOKEN=dont_access # API Token für ESP32 Endpoints
WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$ WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
LOG_PATH=__ROOT__/logs # Pfad für Audit Logs LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen. regex:^(debug|info|warn|error)$
BACKUP_FOLDER=__ROOT__/backup # Pfad für DB Backup Datein BACKUP_FOLDER=__ROOT__/backup # Pfad für DB Backup Datein
BOOKING_OUT_OF_BOUNDS=true # Buchungen außerhalb der festgelegten Arbeitszeit erlauben und auf Arbeitszeit anpassen. regex:^(true|false)$
BOOKING_FOR_UNKNOWN_USER=true # Buchungen mit unbekannter CardUID erlauben. regex:^(true|false)$

View File

@@ -1,13 +1,19 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#©Tom Tröger 2026
set -e set -e
envFile=Docker/.env envFile=Docker/.env
envBkp=Docker/.env.old
envExample=Docker/env.example envExample=Docker/env.example
autoBackupScript=Cron/autoBackup.sh cronFilePath=Cron
autoHolidaysScript=Cron/autoHolidays.sh customCronFilePath=Docker/config/cron
autoLogoutScript=Cron/autoLogout.sh
autoBackupScript=autoBackup.sh
autoHolidaysScript=autoHolidays.sh
autoLogoutScript=autoLogout.sh
function checkDocker() {
echo "Checking Docker installation..." echo "Checking Docker installation..."
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
echo "Docker not found. Install Docker? [y/N]" echo "Docker not found. Install Docker? [y/N]"
@@ -29,31 +35,57 @@ if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose plugin missing. You may need to update Docker." echo "Docker Compose plugin missing. You may need to update Docker."
exit 1 exit 1
fi fi
}
########################################################################### ###########################################################################
function setupConfig() {
local reconfig=false
if [ $# -gt 0 ]; then
if ask_reconfig $1 "Reconfigure .env File?"
then
reconfig=true
else
return 0
fi
fi
echo -e "\r\n==================================================\r\n"
echo "Preparing .env file..." echo "Preparing .env file..."
if [ ! -f $envFile ]; then if [ ! -f $envFile ] || [ $reconfig == true ]; then
if [ -f $envExample ]; then if [ -f $envExample ]; then
if [ $reconfig == true ]; then
echo "Reconfiguring env file. Backup stored at $envBkp"
echo "All previous values will be used as defaults!"
cp $envFile $envBkp
else
echo ".env not found. Creating interactively from .env.example." echo ".env not found. Creating interactively from .env.example."
fi
> $envFile > $envFile
while IFS= read -r line; do while IFS= read -r line; do
#ignore empty lines and comments #ignore empty lines and comments
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
key=$(printf "%s" "$line" | cut -d '=' -f 1) local key=$(printf "%s" "$line" | cut -d '=' -f 1)
rest=$(printf "%s" "$line" | cut -d '=' -f 2-) local rest=$(printf "%s" "$line" | cut -d '=' -f 2-)
# extract inline comment portion # extract inline comment portion
comment=$(printf "%s" "$rest" | sed -n 's/.*# \(.*\)$/\1/p') local comment=$(printf "%s" "$rest" | sed -n 's/.*# \(.*\)$/\1/p')
raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//') local raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
local default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
if [ $reconfig == true ]; then
local previous_value=$(grep -E "^$key=" $envBkp | cut -d= -f2)
if [ -n "$previous_value" ]; then
default_value=$previous_value
fi
fi
# Replace __ROOT__ with script pwd # Replace __ROOT__ with script pwd
default_value="${default_value/__ROOT__/$(pwd)}" local default_value="${default_value/__ROOT__/$(pwd)}"
regex="" regex=""
if [[ "$comment" =~ regex:(.*)$ ]]; then if [[ "$comment" =~ regex:(.*)$ ]]; then
@@ -106,23 +138,71 @@ if [ ! -f $envFile ]; then
else else
echo "Using existing .env. (found at $envFile)" echo "Using existing .env. (found at $envFile)"
fi fi
}
########################################################################### ###########################################################################
function setupFolders(){
if [ $# -gt 0 ]; then
if ! ask_reconfig $1 "Recreate Folders?"
then
return 0
fi
fi
LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2) LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2)
if [ -z "$LOG_PATH" ]; then if [ -z "$LOG_PATH" ]; then
echo "LOG_PATH not found in .env using default $(pwd)/logs" echo "LOG_PATH not found in .env using default $(pwd)/logs"
LOG_PATH=$(pwd)/logs LOG_PATH=$(pwd)/logs
fi fi
if [ ! -d "$LOG_PATH" ]; then
mkdir -p $LOG_PATH mkdir -p $LOG_PATH
echo "Created logs folder at $LOG_PATH" echo "Created logs folder at $LOG_PATH"
fi
POSTGRES_PATH=$(grep -E '^POSTGRES_PATH=' $envFile | cut -d= -f2)
if [ -z "$POSTGRES_PATH" ]; then
echo "POSTGRES_PATH not found in .env using default $(pwd)/DB"
POSTGRES_PATH=$(pwd)/DB
fi
if [ ! -d "$POSTGRES_PATH" ]; then
mkdir -p $POSTGRES_PATH
echo "Created DB folder at $POSTGRES_PATH"
fi
BACKUP_FOLDER=$(grep -E '^BACKUP_FOLDER=' $envFile | cut -d= -f2)
if [ -z "$BACKUP_FOLDER" ]; then
echo "BACKUP_FOLDER not found in .env using default $(pwd)/backup"
BACKUP_FOLDER=$(pwd)/backup
fi
if [ ! -d "$BACKUP_FOLDER" ]; then
mkdir -p $BACKUP_FOLDER
echo "Created backup folder at $BACKUP_FOLDER"
fi
}
########################################################################### ###########################################################################
echo -e "\n\n" function setupCron(){
echo -e "\r\n==================================================\r\n"
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]" echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
read -r setup_cron read -r setup_cron
if [[ "$setup_cron" =~ ^[Yy]$ ]]; then if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
echo "Copying custom cron files to $customCronFilePath"
mkdir -p "$customCronFilePath"
if [ ! -s "$customCronFilePath/$autoBackupScript" ];then
cp "$cronFilePath/$autoBackupScript" "$customCronFilePath/$autoBackupScript"
echo "Copied $autoBackupScript"
fi
if [ ! -s "$customCronFilePath/$autoLogoutScript" ];then
cp "$cronFilePath/$autoLogoutScript" "$customCronFilePath/$autoLogoutScript"
echo "Copied $autoLogoutScript"
fi
if [ ! -s "$customCronFilePath/$autoHolidaysScript" ];then
cp "$cronFilePath/$autoHolidaysScript" "$customCronFilePath/$autoHolidaysScript"
echo "Copied $autoHolidaysScript"
fi
WEB_PORT=$(grep -E '^WEB_PORT=' $envFile | cut -d= -f2) WEB_PORT=$(grep -E '^WEB_PORT=' $envFile | cut -d= -f2)
if [ -z "$WEB_PORT" ]; then if [ -z "$WEB_PORT" ]; then
echo "WEB_PORT not found in .env using default 8000" echo "WEB_PORT not found in .env using default 8000"
@@ -141,19 +221,20 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
BACKUP_FOLDER="$(pwd)/backup" BACKUP_FOLDER="$(pwd)/backup"
fi fi
sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript sed -i "s|__PORT__|$WEB_PORT|" $customCronFilePath/$autoHolidaysScript && \
sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript sed -i "s|__PORT__|$WEB_PORT|" $customCronFilePath/$autoLogoutScript && \
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript sed -i "s|__DATABASE__|$POSTGRES_DB|" $customCronFilePath/$autoBackupScript && \
sed -i "s/__BACKUP_FOLDER__/$BACKUP_FOLDER" $autoBackupScript sed -i "s|__BACKUP_FOLDER__|$BACKUP_FOLDER|" $customCronFilePath/$autoBackupScript
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript chmod +x "$customCronFilePath/$autoBackupScript" "$customCronFilePath/$autoHolidaysScript" "$customCronFilePath/$autoLogoutScript"
# echo "Scripts build with PORT=$WEB_PORT and DATABSE=$POSTGRES_DB!" # echo "Scripts build with PORT=$WEB_PORT and DATABSE=$POSTGRES_DB!"
echo "Adding rules to crontab." echo "Adding rules to crontab."
cron_commands=$(mktemp /tmp/arbeitszeitmessung-cron.XXX) cron_commands=$(mktemp /tmp/arbeitszeitmessung-cron.XXX)
pwd
for file in Cron/*; do for file in $customCronFilePath/*; do
cron_timing=$(grep -E '^# cron-timing:' "$file" | sed 's/^# cron-timing:[[:space:]]*//') cron_timing=$(grep -E '^# cron-timing:' "$file" | sed 's/^# cron-timing:[[:space:]]*//')
if [ -z "$cron_timing" ]; then if [ -z "$cron_timing" ]; then
@@ -163,7 +244,6 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
( crontab -l ; echo "$cron_timing $(pwd)/$file" )| awk '!x[$0]++' | crontab - ( crontab -l ; echo "$cron_timing $(pwd)/$file" )| awk '!x[$0]++' | crontab -
echo "Added entry to crontab: $cron_timing $(pwd)/$file." echo "Added entry to crontab: $cron_timing $(pwd)/$file."
sleep 2
done done
if systemctl is-active --quiet cron.service ; then if systemctl is-active --quiet cron.service ; then
@@ -176,10 +256,11 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
else else
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!" echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
fi fi
}
########################################################################### ###########################################################################
echo -e "\n\n" function startContainer(){
echo -e "\r\n==================================================\r\n"
echo "Start containers with docker compose up -d? [y/N]" echo "Start containers with docker compose up -d? [y/N]"
read -r start_containers read -r start_containers
if [[ "$start_containers" =~ ^[Yy]$ ]]; then if [[ "$start_containers" =~ ^[Yy]$ ]]; then
@@ -189,5 +270,58 @@ if [[ "$start_containers" =~ ^[Yy]$ ]]; then
else else
echo "You can start them manually with: docker compose up -d" echo "You can start them manually with: docker compose up -d"
fi fi
}
###########################################################################
function help(){
echo "Installer Script für Arbeitszeitmessung Software"
echo -e "\r\n==================================================\r\n"
echo "Nutzung: ./install.sh [options]"
echo -e "\r\n==================================================\r\n"
echo "Optionen:"
echo " -h zeigt diese Übersicht"
echo " -c .env Datei bearbeiten/aktualisieren && cron neu configurieren"
echo -e "\r\n=================================================="
}
###########################################################################
function main(){
echo -e "================Arbeitszeitmessung================\r\n"
if [ $# -gt 0 ];then
if [ $1 == reconfig ]; then
echo -e "================Reconfiguring================\r\n"
setupConfig $1
setupFolders $1
setupCron $1
fi
else
checkDocker
setupConfig
setupFolders
setupCron
startContainer
fi
echo "Installation finished, you can re-run the script any time!" echo "Installation finished, you can re-run the script any time!"
}
###########################################################################
function ask_reconfig(){
echo -e "\r\n==================================================\r\n"
echo "$2 [y/N]"
read -r do_reconfig
[[ "$do_reconfig" =~ ^[Yy]$ ]] && return # true
echo "Skipping..."
return 1
}
###########################################################################
while getopts ":hc" opt; do
case $opt in
h) help; exit 0 ;;
c) main reconfig; exit 0 ;;
*) echo "Ungültiges Argument"; exit 1 ;;
esac
done
main