From 3e0f84f618251b89b39230567134c9b1b39aee4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 1 Aug 2025 18:21:39 +0200 Subject: [PATCH 01/39] trying atlas --- atlas.hcl | 10 ++++++++++ schema.sql | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 atlas.hcl create mode 100644 schema.sql diff --git a/atlas.hcl b/atlas.hcl new file mode 100644 index 0000000..ebc39a6 --- /dev/null +++ b/atlas.hcl @@ -0,0 +1,10 @@ +# The "dev" environment represents our local testings. +env "local" { + url = "postgres://root:very_secure@:5432/arbeitszeitmessung?search_path=public&sslmode=disable" + migration { + dir = "file://migrations" + format = golang-migrate + } + dev = "docker://postgres/16/dev?search_path=public" + src = "file://schema.sql" +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..55dcecd --- /dev/null +++ b/schema.sql @@ -0,0 +1,58 @@ +-- Create "abwesenheit" table +CREATE TABLE "abwesenheit" ( + "counter_id" bigserial NOT NULL, + "card_uid" character varying(255) NULL, + "abwesenheit_typ" smallint NULL, + "datum" timestamptz NULL DEFAULT (now())::date, + PRIMARY KEY ("counter_id") +); +-- Create "anwesenheit" table +CREATE TABLE "anwesenheit" ( + "counter_id" bigserial NOT NULL, + "timestamp" timestamptz NULL DEFAULT CURRENT_TIMESTAMP, + "card_uid" character varying(255) NULL, + "check_in_out" smallint NULL, + "anwesenheit_type" smallint NULL, + "geraet_id" smallint NULL, + PRIMARY KEY ("counter_id") +); +-- Set comment to column: "check_in_out" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."check_in_out" IS '1=Check In 2=Check Out , 3=Check in Manuell, 4=Check out manuell255=Automatic Check Out'; +-- Set comment to column: "geraet_id" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes'; +-- Create "personal_daten" table +CREATE TABLE "personal_daten" ( + "personal_nummer" integer NOT NULL, + "aktiv_beschaeftigt" boolean NULL, + "vorname" character varying(255) NULL, + "nachname" character varying(255) NULL, + "geburtsdatum" date NULL, + "plz" character varying(255) NULL, + "adresse" character varying(255) NULL, + "geschlecht" smallint NULL, + "card_uid" character varying(255) NULL, + "hauptbeschaeftigungs_ort" smallint NULL, + "arbeitszeit_per_tag" real NULL, + "arbeitszeit_min_start" time NULL, + "arbeitszeit_max_ende" time NULL, + "vorgesetzter_pers_nr" integer NULL, + PRIMARY KEY ("personal_nummer") +); +-- Set comment to column: "geschlecht" on table: "personal_daten" +COMMENT ON COLUMN "personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; +-- Create "user_password" table +CREATE TABLE "user_password" ( + "personal_nummer" integer NOT NULL, + "pass_hash" text NULL, + "zuletzt_geandert" timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY ("personal_nummer") +); +-- Create "wochen_report" table +CREATE TABLE "wochen_report" ( + "id" serial NOT NULL, + "personal_nummer" integer NULL, + "woche_start" date NULL, + "bestaetigt" boolean NULL DEFAULT false, + PRIMARY KEY ("id"), + CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start") +); -- 2.49.1 From 4555ab9a3006151db71da36b7174c1fe90343b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Mon, 4 Aug 2025 23:20:29 +0200 Subject: [PATCH 02/39] added go-migrate Migrations --- .gitignore | 1 + .../{01_create_tables.sql => 01_schema.sql} | 13 ------------- DB/initdb/03_sample_data.sql | 8 ++++++++ Docker/.env.example | 4 ++-- atlas.hcl => example.atlas.hcl | 4 ++-- migrations/20250802065143_initial.down.sql | 16 ++++++++++++++++ .../20250802065143_initial.up.sql | 17 ++++++++--------- .../20250802075213_control_tables.down.sql | 10 ++++++++++ migrations/20250802075213_control_tables.up.sql | 16 ++++++++++++++++ migrations/atlas.sum | 3 +++ 10 files changed, 66 insertions(+), 26 deletions(-) rename DB/initdb/{01_create_tables.sql => 01_schema.sql} (77%) create mode 100644 DB/initdb/03_sample_data.sql rename atlas.hcl => example.atlas.hcl (61%) create mode 100644 migrations/20250802065143_initial.down.sql rename schema.sql => migrations/20250802065143_initial.up.sql (82%) create mode 100644 migrations/20250802075213_control_tables.down.sql create mode 100644 migrations/20250802075213_control_tables.up.sql create mode 100644 migrations/atlas.sum diff --git a/.gitignore b/.gitignore index e7c242b..11a8757 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ DB/pg_data .vscode node_modules +atlas.hcl diff --git a/DB/initdb/01_create_tables.sql b/DB/initdb/01_schema.sql similarity index 77% rename from DB/initdb/01_create_tables.sql rename to DB/initdb/01_schema.sql index 0c90c86..c987aef 100644 --- a/DB/initdb/01_create_tables.sql +++ b/DB/initdb/01_schema.sql @@ -98,16 +98,3 @@ CREATE TABLE "s_abwesenheit_typen" ( -- Adds crypto extension CREATE EXTENSION IF NOT EXISTS pgcrypto; - --- ---------------------------- --- Insert example data --- ---------------------------- - -INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES -(123, 't', 'Max', 'Mustermann', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'acde-edca', 1, 7.5, '07:00:00', '20:00:00', 0); - -INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES -(123, crypt('max_pass', gen_salt('bf'))); - -INSERT INTO "s_anwesenheit_typen" ("anwesenheit_id", "anwesenheit_name") VALUES (1, 'Büro'); -INSERT INTO "s_abwesenheit_typen" ("abwesenheit_id", "abwesenheit_name") VALUES (1, 'Urlaub'), (2, 'Krank'), (3, 'Kurzarbeit'); diff --git a/DB/initdb/03_sample_data.sql b/DB/initdb/03_sample_data.sql new file mode 100644 index 0000000..6d84496 --- /dev/null +++ b/DB/initdb/03_sample_data.sql @@ -0,0 +1,8 @@ +INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES +(123, 't', 'Max', 'Mustermann', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'acde-edca', 1, 7.5, '07:00:00', '20:00:00', 0); + +INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES +(123, crypt('max_pass', gen_salt('bf'))); + +INSERT INTO "s_anwesenheit_typen" ("anwesenheit_id", "anwesenheit_name") VALUES (1, 'Büro'); +INSERT INTO "s_abwesenheit_typen" ("abwesenheit_id", "abwesenheit_name") VALUES (1, 'Urlaub'), (2, 'Krank'), (3, 'Kurzarbeit'); diff --git a/Docker/.env.example b/Docker/.env.example index 8eea0f3..256e380 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -1,7 +1,7 @@ POSTGRES_USER=root POSTGRES_PASSWORD=very_secure -POSTGRES_API_USER=api_nuter -POSTGRES_API_PASSWORD=password +POSTGRES_API_USER=api_nutzer +POSTGRES_API_PASS=password POSTGRES_PATH=../DB POSTGRES_DB=arbeitszeitmessung EXPOSED_PORT=8000 diff --git a/atlas.hcl b/example.atlas.hcl similarity index 61% rename from atlas.hcl rename to example.atlas.hcl index ebc39a6..7e69ee9 100644 --- a/atlas.hcl +++ b/example.atlas.hcl @@ -1,10 +1,10 @@ # The "dev" environment represents our local testings. env "local" { - url = "postgres://root:very_secure@:5432/arbeitszeitmessung?search_path=public&sslmode=disable" + url = "postgres://user:password@:5432/database?search_path=public&sslmode=disable" migration { dir = "file://migrations" format = golang-migrate } dev = "docker://postgres/16/dev?search_path=public" - src = "file://schema.sql" + src = "file://DB/initdb/01_schema.sql" } diff --git a/migrations/20250802065143_initial.down.sql b/migrations/20250802065143_initial.down.sql new file mode 100644 index 0000000..1f8630e --- /dev/null +++ b/migrations/20250802065143_initial.down.sql @@ -0,0 +1,16 @@ +-- reverse: create "wochen_report" table +DROP TABLE "wochen_report"; +-- reverse: create "user_password" table +DROP TABLE "user_password"; +-- reverse: set comment to column: "geschlecht" on table: "personal_daten" +COMMENT ON COLUMN "personal_daten"."geschlecht" IS NULL; +-- reverse: create "personal_daten" table +DROP TABLE "personal_daten"; +-- reverse: set comment to column: "geraet_id" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."geraet_id" IS NULL; +-- reverse: set comment to column: "check_in_out" on table: "anwesenheit" +COMMENT ON COLUMN "anwesenheit"."check_in_out" IS NULL; +-- reverse: create "anwesenheit" table +DROP TABLE "anwesenheit"; +-- reverse: create "abwesenheit" table +DROP TABLE "abwesenheit"; diff --git a/schema.sql b/migrations/20250802065143_initial.up.sql similarity index 82% rename from schema.sql rename to migrations/20250802065143_initial.up.sql index 55dcecd..5144756 100644 --- a/schema.sql +++ b/migrations/20250802065143_initial.up.sql @@ -1,4 +1,4 @@ --- Create "abwesenheit" table +-- create "abwesenheit" table CREATE TABLE "abwesenheit" ( "counter_id" bigserial NOT NULL, "card_uid" character varying(255) NULL, @@ -6,21 +6,20 @@ CREATE TABLE "abwesenheit" ( "datum" timestamptz NULL DEFAULT (now())::date, PRIMARY KEY ("counter_id") ); --- Create "anwesenheit" table +-- create "anwesenheit" table CREATE TABLE "anwesenheit" ( "counter_id" bigserial NOT NULL, "timestamp" timestamptz NULL DEFAULT CURRENT_TIMESTAMP, "card_uid" character varying(255) NULL, "check_in_out" smallint NULL, - "anwesenheit_type" smallint NULL, "geraet_id" smallint NULL, PRIMARY KEY ("counter_id") ); --- Set comment to column: "check_in_out" on table: "anwesenheit" +-- set comment to column: "check_in_out" on table: "anwesenheit" COMMENT ON COLUMN "anwesenheit"."check_in_out" IS '1=Check In 2=Check Out , 3=Check in Manuell, 4=Check out manuell255=Automatic Check Out'; --- Set comment to column: "geraet_id" on table: "anwesenheit" +-- set comment to column: "geraet_id" on table: "anwesenheit" COMMENT ON COLUMN "anwesenheit"."geraet_id" IS 'ID des Lesegerätes'; --- Create "personal_daten" table +-- create "personal_daten" table CREATE TABLE "personal_daten" ( "personal_nummer" integer NOT NULL, "aktiv_beschaeftigt" boolean NULL, @@ -38,16 +37,16 @@ CREATE TABLE "personal_daten" ( "vorgesetzter_pers_nr" integer NULL, PRIMARY KEY ("personal_nummer") ); --- Set comment to column: "geschlecht" on table: "personal_daten" +-- set comment to column: "geschlecht" on table: "personal_daten" COMMENT ON COLUMN "personal_daten"."geschlecht" IS '1==weiblich, 2==maennlich, 3==divers'; --- Create "user_password" table +-- create "user_password" table CREATE TABLE "user_password" ( "personal_nummer" integer NOT NULL, "pass_hash" text NULL, "zuletzt_geandert" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("personal_nummer") ); --- Create "wochen_report" table +-- create "wochen_report" table CREATE TABLE "wochen_report" ( "id" serial NOT NULL, "personal_nummer" integer NULL, diff --git a/migrations/20250802075213_control_tables.down.sql b/migrations/20250802075213_control_tables.down.sql new file mode 100644 index 0000000..f8eef8a --- /dev/null +++ b/migrations/20250802075213_control_tables.down.sql @@ -0,0 +1,10 @@ +-- reverse rename "s_personal_daten" table +ALTER TABLE "s_personal_daten" RENAME TO "personal_daten"; + +DROP TABLE "s_personal_daten"; +-- reverse: create "s_anwesenheit_typen" table +DROP TABLE "s_anwesenheit_typen"; +-- reverse: create "s_abwesenheit_typen" table +DROP TABLE "s_abwesenheit_typen"; +-- reverse: modify "anwesenheit" table +ALTER TABLE "anwesenheit" DROP COLUMN "manuelle_buchung"; diff --git a/migrations/20250802075213_control_tables.up.sql b/migrations/20250802075213_control_tables.up.sql new file mode 100644 index 0000000..d372a37 --- /dev/null +++ b/migrations/20250802075213_control_tables.up.sql @@ -0,0 +1,16 @@ +-- modify "anwesenheit" table +ALTER TABLE "anwesenheit" ADD COLUMN "manuelle_buchung" boolean NULL; +-- create "s_abwesenheit_typen" table +CREATE TABLE "s_abwesenheit_typen" ( + "abwesenheit_id" smallint NOT NULL, + "abwesenheit_name" character varying(255) NULL, + PRIMARY KEY ("abwesenheit_id") +); +-- create "s_anwesenheit_typen" table +CREATE TABLE "s_anwesenheit_typen" ( + "anwesenheit_id" smallint NOT NULL, + "anwesenheit_name" character varying(255) NULL, + PRIMARY KEY ("anwesenheit_id") +); +-- create "s_personal_daten" table +ALTER TABLE "personal_daten" RENAME TO "s_personal_daten"; diff --git a/migrations/atlas.sum b/migrations/atlas.sum new file mode 100644 index 0000000..58b1ee2 --- /dev/null +++ b/migrations/atlas.sum @@ -0,0 +1,3 @@ +h1:5gPDmrcQS12KjKLuwN1ycTBHtbHbkzd7rUIj01uJrhA= +20250802065143_initial.up.sql h1:9cUWduWgONRfI5LV+b3nFvei6DKDqPxcNryKVg1xo80= +20250802075213_control_tables.up.sql h1:5vQLBHMM2Sa1FErP5gQUUHAoSiV2RQ0cOlMEEDFcoKA= -- 2.49.1 From 64bc58a3a56d51f00ea49e7cfcdfd947a8093ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Mon, 4 Aug 2025 23:20:41 +0200 Subject: [PATCH 03/39] updated templ version --- Backend/go.mod | 6 +++--- Backend/go.sum | 4 ++-- Backend/templates/headerComponent_templ.go | 2 +- Backend/templates/pages_templ.go | 2 +- Backend/templates/teamComponents_templ.go | 2 +- Backend/templates/timeComponents_templ.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Backend/go.mod b/Backend/go.mod index 4c04ddd..f680c35 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -1,12 +1,12 @@ module arbeitszeitmessung -go 1.23 +go 1.23.0 -toolchain go1.23.6 +toolchain go1.24.5 require github.com/lib/pq v1.10.9 -require github.com/a-h/templ v0.3.833 +require github.com/a-h/templ v0.3.924 require github.com/alexedwards/scs/v2 v2.8.0 diff --git a/Backend/go.sum b/Backend/go.sum index 8c124ff..58f050d 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,5 +1,5 @@ -github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU= -github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk= +github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k= +github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index 4710b0a..fa6028b 100644 --- a/Backend/templates/headerComponent_templ.go +++ b/Backend/templates/headerComponent_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index c649f96..40a7152 100644 --- a/Backend/templates/pages_templ.go +++ b/Backend/templates/pages_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index efd4951..2f343e2 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. diff --git a/Backend/templates/timeComponents_templ.go b/Backend/templates/timeComponents_templ.go index c560ce9..e4b99e8 100644 --- a/Backend/templates/timeComponents_templ.go +++ b/Backend/templates/timeComponents_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.833 +// templ: version: v0.3.924 package templates //lint:file-ignore SA4006 This context is only used if a nested component is present. -- 2.49.1 From a87bef8c89fe7a47c9c588b3272f5e2d3bc2d4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 12 Aug 2025 15:47:36 +0200 Subject: [PATCH 04/39] added user Session Handler --> closed #20 --- Backend/endpoints/team.go | 5 --- Backend/endpoints/team_presence.go | 10 ++--- Backend/endpoints/time-create.go | 4 -- Backend/endpoints/time.go | 7 --- .../{user-login.go => user-session.go} | 24 ++++------ Backend/endpoints/user-settings.go | 34 -------------- Backend/endpoints/user.go | 45 +++++++++++++++++++ Backend/main.go | 15 +++---- Backend/static/script.js | 12 ++--- Backend/templates/headerComponent.templ | 5 +-- Backend/templates/headerComponent_templ.go | 12 ++++- Backend/templates/pages.templ | 14 ++++-- Backend/templates/pages_templ.go | 41 ++++++++++++++--- 13 files changed, 127 insertions(+), 101 deletions(-) rename Backend/endpoints/{user-login.go => user-session.go} (87%) create mode 100644 Backend/endpoints/user.go diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index 9b214ea..3526085 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -17,13 +17,10 @@ func TeamHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: submitReport(w, r) - break case http.MethodGet: showWeeks(w, r) - break default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break } } @@ -47,10 +44,8 @@ func submitReport(w http.ResponseWriter, r *http.Request) { switch r.FormValue("method") { case "send": err = workWeek.Send() - break case "accept": err = workWeek.Accept() - break default: break } diff --git a/Backend/endpoints/team_presence.go b/Backend/endpoints/team_presence.go index 5ca18e0..b313627 100644 --- a/Backend/endpoints/team_presence.go +++ b/Backend/endpoints/team_presence.go @@ -8,24 +8,21 @@ import ( "net/http" ) -func TeamPresenceHandler(w http.ResponseWriter, r *http.Request){ +func TeamPresenceHandler(w http.ResponseWriter, r *http.Request) { helper.RequiresLogin(Session, w, r) helper.SetCors(w) switch r.Method { case http.MethodGet: teamPresence(w, r) - break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) - break default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break } } -func teamPresence(w http.ResponseWriter, r *http.Request){ +func teamPresence(w http.ResponseWriter, r *http.Request) { user, err := (*models.User).GetUserFromSession(nil, Session, r.Context()) if err != nil { log.Println("Error getting user!", err) @@ -37,8 +34,7 @@ func teamPresence(w http.ResponseWriter, r *http.Request){ teamPresence[present] = append(teamPresence[present], user) } - - if(err != nil){ + if err != nil { log.Println("Error getting team", err) } templates.TeamPresencePage(teamPresence).Render(r.Context(), w) diff --git a/Backend/endpoints/time-create.go b/Backend/endpoints/time-create.go index 7dd0e29..05edd1c 100644 --- a/Backend/endpoints/time-create.go +++ b/Backend/endpoints/time-create.go @@ -16,17 +16,13 @@ func TimeCreateHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPut: createBooking(w, r) - break case http.MethodGet: createBooking(w, r) - break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) - break default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - break } } diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index 9dae1a3..eef7537 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -20,17 +20,13 @@ func TimeHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: getBookings(w, r) - break case http.MethodPost: updateBooking(w, r) - break case http.MethodOptions: // just support options header for non GET Requests from SWAGGER w.WriteHeader(http.StatusOK) - break default: http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break } } @@ -120,7 +116,6 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { if err != nil { log.Println("Error inserting booking", err) } - break case "change": absenceType, err := strconv.Atoi(r.FormValue("absence")) if err != nil { @@ -151,9 +146,7 @@ func updateBooking(w http.ResponseWriter, r *http.Request) { booking.UpdateTime(parsedTime) } } - break } - getBookings(w, r) } diff --git a/Backend/endpoints/user-login.go b/Backend/endpoints/user-session.go similarity index 87% rename from Backend/endpoints/user-login.go rename to Backend/endpoints/user-session.go index ee19238..9b9084c 100644 --- a/Backend/endpoints/user-login.go +++ b/Backend/endpoints/user-session.go @@ -21,20 +21,6 @@ func CreateSessionManager(lifetime time.Duration) *scs.SessionManager { return Session } -func LoginHandler(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - showLoginPage(w, r, false) - break - case http.MethodPost: - loginUser(w, r) - break - default: - http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break - } -} - func showLoginPage(w http.ResponseWriter, r *http.Request, failed bool) { r = r.WithContext(context.WithValue(r.Context(), "session", Session)) if helper.GetEnv("GO_ENV", "production") == "debug" { @@ -83,5 +69,13 @@ func loginUser(w http.ResponseWriter, r *http.Request) { return } showLoginPage(w, r, false) - return +} + +func logoutUser(w http.ResponseWriter, r *http.Request) { + log.Println("Loggin out user!") + err := Session.Destroy(r.Context()) + if err != nil { + log.Println("Error destroying session!", err) + } + http.Redirect(w, r, "/user/login", http.StatusSeeOther) } diff --git a/Backend/endpoints/user-settings.go b/Backend/endpoints/user-settings.go index 7524f81..d8509a0 100644 --- a/Backend/endpoints/user-settings.go +++ b/Backend/endpoints/user-settings.go @@ -1,36 +1,12 @@ package endpoints import ( - "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "arbeitszeitmessung/templates" "log" "net/http" ) -func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { - helper.RequiresLogin(Session, w, r) - switch r.Method { - case http.MethodGet: - showUserPage(w, r, 0) - break - case http.MethodPost: - switch r.FormValue("action") { - case "change-pass": - changePassword(w, r) - break - case "logout-user": - logoutUser(w, r) - break - } - - break - default: - http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) - break - } -} - // change user password and store salted hash in db func changePassword(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() @@ -61,16 +37,6 @@ func changePassword(w http.ResponseWriter, r *http.Request) { showUserPage(w, r, http.StatusUnauthorized) } -func logoutUser(w http.ResponseWriter, r *http.Request) { - - err := Session.Destroy(r.Context()) - if err != nil { - log.Println("Error destroying session!", err) - } - http.Redirect(w, r, "/user/login", http.StatusSeeOther) -} - func showUserPage(w http.ResponseWriter, r *http.Request, status int) { templates.UserPage(status).Render(r.Context(), w) - return } diff --git a/Backend/endpoints/user.go b/Backend/endpoints/user.go new file mode 100644 index 0000000..540d117 --- /dev/null +++ b/Backend/endpoints/user.go @@ -0,0 +1,45 @@ +package endpoints + +import ( + "arbeitszeitmessung/helper" + "net/http" +) + +func UserHandler(w http.ResponseWriter, r *http.Request) { + switch r.PathValue("action") { + case "login": + LoginHandler(w, r) + case "settings": + UserSettingsHandler(w, r) + case "logout": + logoutUser(w, r) + } +} + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + showLoginPage(w, r, false) + case http.MethodPost: + loginUser(w, r) + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + } +} + +func UserSettingsHandler(w http.ResponseWriter, r *http.Request) { + helper.RequiresLogin(Session, w, r) + switch r.Method { + case http.MethodGet: + showUserPage(w, r, 0) + case http.MethodPost: + switch r.FormValue("action") { + case "change-pass": + changePassword(w, r) + case "logout-user": + logoutUser(w, r) + } + default: + http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed) + } +} diff --git a/Backend/main.go b/Backend/main.go index 969ab60..3d2aee2 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -22,17 +22,15 @@ func main() { if err != nil { log.Println("No .env file found in directory!") } - if(helper.GetEnv("GO_ENV", "production") == "debug") { + if helper.GetEnv("GO_ENV", "production") == "debug" { log.Println("Debug mode enabled") log.Println("Environment Variables") envs := os.Environ() - for _, e := range envs { - fmt.Println(e) - } + for _, e := range envs { + fmt.Println(e) + } } - - models.DB, err = OpenDatabase() if err != nil { log.Fatal(err) @@ -48,8 +46,9 @@ func main() { server.HandleFunc("/time/new", endpoints.TimeCreateHandler) server.Handle("/time", ParamsMiddleware(endpoints.TimeHandler)) server.HandleFunc("/logout", endpoints.LogoutHandler) - server.HandleFunc("/user/login", endpoints.LoginHandler) - server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) + server.HandleFunc("/user/{action}", endpoints.UserHandler) + // server.HandleFunc("/user/login", endpoints.LoginHandler) + // server.HandleFunc("/user/settings", endpoints.UserSettingsHandler) server.HandleFunc("/team", endpoints.TeamHandler) server.HandleFunc("/team/presence", endpoints.TeamPresenceHandler) server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect)) diff --git a/Backend/static/script.js b/Backend/static/script.js index 5bc2abb..520ec52 100644 --- a/Backend/static/script.js +++ b/Backend/static/script.js @@ -1,7 +1,5 @@ function editDay(element, event, formId) { - var form = element - .closest(".grid-sub") - .querySelector(".all-booking-component > form"); + var form = element.closest(".grid-sub").querySelector(".all-booking-component > form"); form.classList.toggle("edit"); element.classList.toggle("edit"); if (element.classList.contains("edit")) { @@ -15,9 +13,7 @@ function editDay(element, event, formId) { } function editAbwesenheit(element, event) { - var newBookingComponent = element - .closest(".grid-sub") - .querySelector(".new-booking-component"); + var newBookingComponent = element.closest(".grid-sub").querySelector(".new-booking-component"); if (element.value == 0) { newBookingComponent.style.display = ""; } else { @@ -32,3 +28,7 @@ function navigateWeek(element, event, direction) { date.setHours(10); dateInput.valueAsDate = date; } + +function logoutUser() { + fetch("/user/logout", {}).then(() => window.location.reload()); +} diff --git a/Backend/templates/headerComponent.templ b/Backend/templates/headerComponent.templ index bfb040f..ec2d673 100644 --- a/Backend/templates/headerComponent.templ +++ b/Backend/templates/headerComponent.templ @@ -1,14 +1,13 @@ package templates - - templ headerComponent() { -
+
Zeitverwaltung Abrechnung if true { Anwesenheit } Einstellungen + @LogoutButton()
} diff --git a/Backend/templates/headerComponent_templ.go b/Backend/templates/headerComponent_templ.go index fa6028b..aeef6ec 100644 --- a/Backend/templates/headerComponent_templ.go +++ b/Backend/templates/headerComponent_templ.go @@ -29,7 +29,7 @@ func headerComponent() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
Zeitverwaltung Abrechnung ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -39,7 +39,15 @@ func headerComponent() templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Einstellungen
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "Einstellungen") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = LogoutButton().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/Backend/templates/pages.templ b/Backend/templates/pages.templ index 73d9785..11d8b3f 100644 --- a/Backend/templates/pages.templ +++ b/Backend/templates/pages.templ @@ -68,15 +68,15 @@ templ UserPage(status int) {
-
+

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

- +
- +
} @@ -88,7 +88,9 @@ templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) { @headerComponent()
-
{ fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name) }
+
+ { fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name) } +
for _, day := range userWeek.WorkDays { @weekDayComponent(userWeek.User, day) @@ -182,3 +184,7 @@ templ TeamPresencePage(teamPresence map[bool][]models.User) {
} + +templ LogoutButton() { + +} diff --git a/Backend/templates/pages_templ.go b/Backend/templates/pages_templ.go index 40a7152..d9d42b6 100644 --- a/Backend/templates/pages_templ.go +++ b/Backend/templates/pages_templ.go @@ -192,7 +192,7 @@ func UserPage(status int) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

Nutzer abmelden

Nutzer von Weboberfläche abmelden.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -238,7 +238,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s %s", userWeek.User.Vorname, userWeek.User.Name)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 91, Col: 111} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 92, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -261,7 +261,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(userWeek.WeekStart.Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 99, Col: 110} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 101, Col: 110} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -291,7 +291,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d, %d", kw, year)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 105, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 107, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -331,7 +331,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(userWeek.User.PersonalNummer)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 114, Col: 88} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 116, Col: 88} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -344,7 +344,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(userWeek.WeekStart.Format(time.DateOnly)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 115, Col: 86} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/pages.templ`, Line: 117, Col: 86} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -495,4 +495,33 @@ func TeamPresencePage(teamPresence map[bool][]models.User) templ.Component { }) } +func LogoutButton() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + var _ = templruntime.GeneratedTemplate -- 2.49.1 From ba885357c289d236f133a4112a664fedfc95ef9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Tue, 12 Aug 2025 16:47:06 +0200 Subject: [PATCH 05/39] fixes #21 --- Backend/models/absence.go | 26 +--- Backend/templates/teamComponents.templ | 8 +- Backend/templates/teamComponents_templ.go | 178 ++++++++++++---------- example.atlas.hcl | 10 -- 4 files changed, 104 insertions(+), 118 deletions(-) delete mode 100644 example.atlas.hcl diff --git a/Backend/models/absence.go b/Backend/models/absence.go index a1f1b9e..bbc9cc7 100644 --- a/Backend/models/absence.go +++ b/Backend/models/absence.go @@ -11,30 +11,6 @@ type AbsenceType struct { Name string } -// const ( -// AbsenceNone int8 = iota -// AbsenceUrlaub -// AbsenceKurzarbeit -// AbsenceKrank -// AbsenceKindkrank -// ) - -// var AbsenceTypes = []AbsenceType{ -// // {Value: AbsenceNone, Label: "Abwesenheit"}, -// {Id: AbsenceUrlaub, Name: "Urlaub"}, -// {Id: AbsenceKurzarbeit, Name: "Kurzarbeit"}, -// {Id: AbsenceKrank, Name: "Krank"}, -// {Id: AbsenceKindkrank, Name: "Kindkrank"}, -// } - -// var AbsenceTypesLabel = map[int8]string{ -// 0: "None", -// AbsenceUrlaub: "Urlaub", -// AbsenceKurzarbeit: "Kurzarbeit", -// AbsenceKrank: "Krank", -// AbsenceKindkrank: "Kindkrank", -// } - type Absence struct { CounterId int CardUID string @@ -68,7 +44,7 @@ func (a *Absence) Insert() error { log.Println("Error preparing sql Statement", err) return err } - err = qStr.QueryRow(a.CardUID, a.AbwesenheitTyp, a.Datum).Scan(&a.CounterId) + err = qStr.QueryRow(a.CardUID, a.AbwesenheitTyp.Id, a.Datum).Scan(&a.CounterId) if err != nil { log.Println("Error executing insert statement", err) return err diff --git a/Backend/templates/teamComponents.templ b/Backend/templates/teamComponents.templ index 40679c5..6da52f9 100644 --- a/Backend/templates/teamComponents.templ +++ b/Backend/templates/teamComponents.templ @@ -22,12 +22,14 @@ templ weekDayComponent(user models.User, day models.WorkDay) { - if day.TimeFrom == day.TimeTo { -

Keine Anwesenheit

- } else { + if day.Absence.Datum.Equal(day.Day) { +

{ day.Absence.AbwesenheitTyp.Name }

+ } else if !day.TimeFrom.Equal(day.TimeTo) { { day.TimeFrom.Format("15:04") } - { day.TimeTo.Format("15:04") } + } else { +

Keine Anwesenheit

} diff --git a/Backend/templates/teamComponents_templ.go b/Backend/templates/teamComponents_templ.go index 2f343e2..eb1d08c 100644 --- a/Backend/templates/teamComponents_templ.go +++ b/Backend/templates/teamComponents_templ.go @@ -101,44 +101,62 @@ func weekDayComponent(user models.User, day models.WorkDay) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if day.TimeFrom == day.TimeTo { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

Keine Anwesenheit

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if day.Absence.Datum.Equal(day.Day) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(day.TimeFrom.Format("15:04")) + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(day.Absence.AbwesenheitTyp.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 28, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 26, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " - ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if !day.TimeFrom.Equal(day.TimeTo) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.TimeTo.Format("15:04")) + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.TimeFrom.Format("15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 30, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 28, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
- ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.TimeTo.Format("15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 30, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Keine Anwesenheit

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -162,53 +180,53 @@ func employeComponent(week models.WorkWeek) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) year, kw := week.WeekStart.ISOWeek() - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 43, Col: 53} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 43, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 45, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

Arbeitszeit

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(week.GetWorkHourString()) + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(week.User.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 45, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 45, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Arbeitszeit

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(week.GetWorkHourString()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 47, Col: 52} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -218,46 +236,46 @@ func employeComponent(week models.WorkWeek) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Woche: ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 53, Col: 85} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "

Woche: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(week.User.PersonalNummer)) + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%02d-%d", kw, year)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 55, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 55, Col: 85} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -281,53 +299,53 @@ func userPresenceComponent(user models.User, present bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var15 := templ.GetChildren(ctx) - if templ_7745c5c3_Var15 == nil { - templ_7745c5c3_Var15 = templ.NopComponent + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if present { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
Anwesend
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
Anwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
Abwesend
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
Abwesend
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 71, Col: 19} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(user.Vorname) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 71, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 73, Col: 19} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 73, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/example.atlas.hcl b/example.atlas.hcl deleted file mode 100644 index 7e69ee9..0000000 --- a/example.atlas.hcl +++ /dev/null @@ -1,10 +0,0 @@ -# The "dev" environment represents our local testings. -env "local" { - url = "postgres://user:password@:5432/database?search_path=public&sslmode=disable" - migration { - dir = "file://migrations" - format = golang-migrate - } - dev = "docker://postgres/16/dev?search_path=public" - src = "file://DB/initdb/01_schema.sql" -} -- 2.49.1 From b614049d037a74814ec088a63ba5ecfcf8a2daa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 13 Aug 2025 15:50:11 +0200 Subject: [PATCH 06/39] added tests for db and user --- .gitignore | 1 + Backend/database.go | 30 ++-------- Backend/endpoints/team.go | 2 +- Backend/endpoints/time.go | 2 +- Backend/endpoints/user-session.go | 2 +- Backend/endpoints/user-settings.go | 2 +- Backend/main.go | 1 - Backend/models/booking.go | 7 ++- Backend/models/db_test.go | 38 +++++++++++++ Backend/models/user.go | 6 +- Backend/models/user_test.go | 56 +++++++++++++++++++ ...{03_sample_data.sql => 02_sample_data.sql} | 2 +- .../{02_create_user.sh => 03_create_user.sh} | 0 Docker/docker-compose.test.yml | 13 +++++ 14 files changed, 126 insertions(+), 36 deletions(-) create mode 100644 Backend/models/db_test.go create mode 100644 Backend/models/user_test.go rename DB/initdb/{03_sample_data.sql => 02_sample_data.sql} (82%) rename DB/initdb/{02_create_user.sh => 03_create_user.sh} (100%) create mode 100644 Docker/docker-compose.test.yml diff --git a/.gitignore b/.gitignore index 11a8757..4e80b98 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ DB/pg_data node_modules atlas.hcl +.scannerwork diff --git a/Backend/database.go b/Backend/database.go index f814974..24b7dd6 100644 --- a/Backend/database.go +++ b/Backend/database.go @@ -7,39 +7,17 @@ import ( "fmt" ) -func OpenDatabase() (*sql.DB, error) { +func OpenDatabase() (models.IDatabase, error) { dbHost := helper.GetEnv("POSTGRES_HOST", "localhost") dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer") dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password") connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbName) - return sql.Open("postgres", connStr) -} - -func GetBookingsByCardID(db *sql.DB, card_id string) ([]models.Booking, error) { - qStr, err := db.Prepare((`SELECT * FROM anwesenheit WHERE card_id = $1`)) + db, err := sql.Open("postgres", connStr) if err != nil { return nil, err } - var bookings []models.Booking - rows, err := qStr.Query(card_id) - if err == sql.ErrNoRows { - return bookings, err - } - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var booking models.Booking - if err := rows.Scan(&booking.CounterId, &booking.Timestamp, &booking.CardUID, &booking.GeraetID, &booking.CheckInOut); err != nil { - return bookings, err - } - bookings = append(bookings, booking) - } - if err = rows.Err(); err != nil { - return bookings, err - } - return bookings, nil + defer db.Close() + return db, err } diff --git a/Backend/endpoints/team.go b/Backend/endpoints/team.go index 3526085..b93585e 100644 --- a/Backend/endpoints/team.go +++ b/Backend/endpoints/team.go @@ -33,7 +33,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) { userPN, _ := strconv.Atoi(r.FormValue("user")) _weekTs := r.FormValue("week") weekTs, err := time.Parse(time.DateOnly, _weekTs) - user, err := (*models.User).GetByPersonalNummer(nil, userPN) + user, err := models.GetUserByPersonalNr(userPN) workWeek := (*models.WorkWeek).GetWeek(nil, user, weekTs, false) if err != nil { diff --git a/Backend/endpoints/time.go b/Backend/endpoints/time.go index eef7537..0172b4c 100644 --- a/Backend/endpoints/time.go +++ b/Backend/endpoints/time.go @@ -178,7 +178,7 @@ func getBookingsAPI(w http.ResponseWriter, r *http.Request) { return } - user, err := (*models.User).GetByPersonalNummer(nil, user_pn) + user, err := models.GetUserByPersonalNr(user_pn) if err != nil { log.Println("No user found with the given personal number!") http.Error(w, "No user found", http.StatusNotFound) diff --git a/Backend/endpoints/user-session.go b/Backend/endpoints/user-session.go index 9b9084c..ffed8b9 100644 --- a/Backend/endpoints/user-session.go +++ b/Backend/endpoints/user-session.go @@ -53,7 +53,7 @@ func loginUser(w http.ResponseWriter, r *http.Request) { return } - user, err := (*models.User).GetByPersonalNummer(nil, personal_nummer) + user, err := models.GetUserByPersonalNr(personal_nummer) if err != nil { log.Println("No user found under this personal number!") http.Error(w, "No user found!", http.StatusNotFound) diff --git a/Backend/endpoints/user-settings.go b/Backend/endpoints/user-settings.go index d8509a0..889267d 100644 --- a/Backend/endpoints/user-settings.go +++ b/Backend/endpoints/user-settings.go @@ -21,7 +21,7 @@ func changePassword(w http.ResponseWriter, r *http.Request) { showUserPage(w, r, http.StatusBadRequest) return } - user, err := (*models.User).GetByPersonalNummer(nil, Session.GetInt(r.Context(), "user")) + user, err := models.GetUserByPersonalNr(Session.GetInt(r.Context(), "user")) if err != nil { log.Println("Error getting user!", err) showUserPage(w, r, http.StatusBadRequest) diff --git a/Backend/main.go b/Backend/main.go index 3d2aee2..272eb6f 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -35,7 +35,6 @@ func main() { if err != nil { log.Fatal(err) } - defer models.DB.Close() fs := http.FileServer(http.Dir("./static")) endpoints.CreateSessionManager(24 * time.Hour) diff --git a/Backend/models/booking.go b/Backend/models/booking.go index 9265808..a739f5b 100644 --- a/Backend/models/booking.go +++ b/Backend/models/booking.go @@ -30,7 +30,12 @@ type Booking struct { CounterId int `json:"counter_id"` } -var DB *sql.DB +type IDatabase interface { + Prepare(query string) (*sql.Stmt, error) + Exec(query string, args ...any) (sql.Result, error) +} + +var DB IDatabase func (b *Booking) New(card_uid string, geraet_id int16, check_in_out int16) Booking { return Booking{ diff --git a/Backend/models/db_test.go b/Backend/models/db_test.go new file mode 100644 index 0000000..21d25f5 --- /dev/null +++ b/Backend/models/db_test.go @@ -0,0 +1,38 @@ +package models_test + +import ( + "arbeitszeitmessung/models" + "database/sql" + "testing" + + _ "github.com/lib/pq" +) + +type DBFixture struct { + Database models.IDatabase + TX *sql.Tx +} + +func SetupDBFixture(t *testing.T) *DBFixture { + t.Helper() + + db, err := sql.Open("postgres", "postgres://postgres:password@localhost:5433/arbeitszeitmessung?sslmode=disable") + if err != nil { + t.Fatalf("failed to connect to database: %v", err) + } + + tx, err := db.Begin() + if err != nil { + t.Fatalf("Failed to start transaction: %v", err) + } + + t.Cleanup(func() { + tx.Rollback() + db.Close() + }) + + return &DBFixture{ + Database: tx, + TX: tx, + } +} diff --git a/Backend/models/user.go b/Backend/models/user.go index 2f181a2..5bd9ded 100644 --- a/Backend/models/user.go +++ b/Backend/models/user.go @@ -24,13 +24,13 @@ func (u *User) GetUserFromSession(Session *scs.SessionManager, ctx context.Conte var user User var err error if helper.GetEnv("GO_ENV", "production") == "debug" { - user, err = (*User).GetByPersonalNummer(nil, 123) + user, err = GetUserByPersonalNr(123) } else { if !Session.Exists(ctx, "user") { log.Println("No user in session storage!") return user, errors.New("No user in session storage!") } - user, err = (*User).GetByPersonalNummer(nil, Session.GetInt(ctx, "user")) + user, err = GetUserByPersonalNr(Session.GetInt(ctx, "user")) } if err != nil { log.Println("Cannot get user from session!") @@ -95,7 +95,7 @@ func (u *User) CheckOut() error { return nil } -func (u *User) GetByPersonalNummer(personalNummer int) (User, error) { +func GetUserByPersonalNr(personalNummer int) (User, error) { var user User qStr, err := DB.Prepare((`SELECT personal_nummer, card_uid, vorname, nachname, arbeitszeit_per_tag FROM s_personal_daten WHERE personal_nummer = $1;`)) diff --git a/Backend/models/user_test.go b/Backend/models/user_test.go new file mode 100644 index 0000000..dd9ccf4 --- /dev/null +++ b/Backend/models/user_test.go @@ -0,0 +1,56 @@ +package models_test + +import ( + "arbeitszeitmessung/models" + "database/sql" + "testing" +) + +var testUser models.User = models.User{Vorname: "Kim", Name: "Mustermensch", PersonalNummer: 456, CardUID: "aaaa-aaaa", ArbeitszeitPerTag: 8} + +func SetupUserFixture(t *testing.T, db models.IDatabase) { + t.Helper() + db.Exec(`INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES +(456, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, '07:00:00', '20:00:00', 0);`) +} + +func TestGetUserByPersonalNr(t *testing.T) { + tc := SetupDBFixture(t) + SetupUserFixture(t, tc.Database) + + models.DB = tc.Database + + user, err := models.GetUserByPersonalNr(testUser.PersonalNummer) + if err != nil { + t.Fatal(err) + } + if user != testUser { + t.Error("Retrieved user not the same as testUser!") + } + + _, err = models.GetUserByPersonalNr(000) + if err != sql.ErrNoRows { + t.Error("Wrong error handling, when retrieving wrong personalnummer") + } +} + +func TestCheckAnwesenheit(t *testing.T) { + tc := SetupDBFixture(t) + models.DB = tc.Database + SetupUserFixture(t, tc.Database) + + var actual bool + + if actual = testUser.CheckAnwesenheit(); actual != false { + t.Errorf("Checkabwesenheit with no booking should be false but is %t", actual) + } + tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '2 hour', 'aaaa-aaaa', 1, 1);") + if actual = testUser.CheckAnwesenheit(); actual != true { + t.Errorf("Checkabwesenheit with 'kommen' booking should be true but is %t", actual) + } + + tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '1 hour', 'aaaa-aaaa', 2, 1);") + if actual = testUser.CheckAnwesenheit(); actual != false { + t.Errorf("Checkabwesenheit with 'gehen' booking should be false but is %t", actual) + } +} diff --git a/DB/initdb/03_sample_data.sql b/DB/initdb/02_sample_data.sql similarity index 82% rename from DB/initdb/03_sample_data.sql rename to DB/initdb/02_sample_data.sql index 6d84496..44c2cb6 100644 --- a/DB/initdb/03_sample_data.sql +++ b/DB/initdb/02_sample_data.sql @@ -1,5 +1,5 @@ INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES -(123, 't', 'Max', 'Mustermann', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'acde-edca', 1, 7.5, '07:00:00', '20:00:00', 0); +(123, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, '07:00:00', '20:00:00', 0); INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES (123, crypt('max_pass', gen_salt('bf'))); diff --git a/DB/initdb/02_create_user.sh b/DB/initdb/03_create_user.sh similarity index 100% rename from DB/initdb/02_create_user.sh rename to DB/initdb/03_create_user.sh diff --git a/Docker/docker-compose.test.yml b/Docker/docker-compose.test.yml new file mode 100644 index 0000000..fbfeb50 --- /dev/null +++ b/Docker/docker-compose.test.yml @@ -0,0 +1,13 @@ +name: arbeitszeitmessung-test +services: + db: + image: postgres:16 + restart: unless-stopped + env_file: + - .env.test + environment: + PGDATA: /var/lib/postgresql/data/pg_data + volumes: + - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d + ports: + - 5433:5432 -- 2.49.1 From 60c91c1ce688b67ad74c9aa27f2b3ceb37154e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 13 Aug 2025 16:10:24 +0200 Subject: [PATCH 07/39] working on sonarcube-test converage --- Backend/sonar-project.properties | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Backend/sonar-project.properties diff --git a/Backend/sonar-project.properties b/Backend/sonar-project.properties new file mode 100644 index 0000000..7b20983 --- /dev/null +++ b/Backend/sonar-project.properties @@ -0,0 +1,8 @@ +sonar.token=sqp_d5e23f24eb2725b03bb8055771cd8c4b48e788cc +sonar.projectKey=Arbeitszeitmessung +sonar.sources=. +sonar.exclusions=**/*_test.go + +sonar.tests=. +sonar.test.inclusions=**/*_test.go +sonar.go.coverage.reportPaths=.test/coverage.out -- 2.49.1 From 33185150cc45f19dee950bc371d60ca46b78159b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 20 Aug 2025 15:53:59 +0200 Subject: [PATCH 08/39] readied jenkinsfile for build --- Backend/helper/time_test.go | 1 + Backend/sonar-project.properties | 2 +- Jenkinsfile | 4 ++-- Makefile | 16 ++++++++++------ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Backend/helper/time_test.go b/Backend/helper/time_test.go index d106e7f..9593ab6 100644 --- a/Backend/helper/time_test.go +++ b/Backend/helper/time_test.go @@ -25,6 +25,7 @@ func TestFormatDuration(t *testing.T) { {"30min", time.Duration(30 * time.Minute)}, {"1h 30min", time.Duration(90 * time.Minute)}, {"-1h 30min", time.Duration(-90 * time.Minute)}, + {"", 0}, } for _, d := range durations { t.Run(d.name, func(t *testing.T) { diff --git a/Backend/sonar-project.properties b/Backend/sonar-project.properties index 7b20983..d859e77 100644 --- a/Backend/sonar-project.properties +++ b/Backend/sonar-project.properties @@ -1,8 +1,8 @@ -sonar.token=sqp_d5e23f24eb2725b03bb8055771cd8c4b48e788cc sonar.projectKey=Arbeitszeitmessung sonar.sources=. sonar.exclusions=**/*_test.go sonar.tests=. sonar.test.inclusions=**/*_test.go +sonar.go.tests.reportPaths=.test/report.json sonar.go.coverage.reportPaths=.test/coverage.out diff --git a/Jenkinsfile b/Jenkinsfile index ee0171f..5e68d30 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,7 @@ pipeline { environment { - DOCKER_USERNAME = 'tom' - DOCKER_PASSWORD = credentials('dgitea_tom') + DOCKER_USERNAME = 'jenkins' + DOCKER_PASSWORD = credentials('gitea_jenkins') } agent any diff --git a/Makefile b/Makefile index 210097e..ccd2917 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ DOCKER_USERNAME ?= tom +PACKAGE_OWNER ?= tom DOCKER_PASSWORD ?= $(shell echo "YOUR_DEFAULT_PASSWORD") IMAGE_REGISTRY ?= git.letsstein.de APPLICATION_NAME ?= arbeitszeit @@ -16,12 +17,12 @@ ifdef JENKINS_HOME endif _builder: - docker build --tag ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} -f $(call capitalize, ${_BUILD_ARGS_APP_PART})/Dockerfile $(call capitalize, ${_BUILD_ARGS_APP_PART}) + docker build --tag ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} -f $(call capitalize, ${_BUILD_ARGS_APP_PART})/Dockerfile $(call capitalize, ${_BUILD_ARGS_APP_PART}) _pusher: login_registry - docker push ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} - docker tag ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} - docker push ${IMAGE_REGISTRY}/${DOCKER_USERNAME}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} + docker push ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} + docker tag ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${GIT_COMMIT} ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} + docker push ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/${APPLICATION_NAME}-${_BUILD_ARGS_APP_PART}:${_BUILD_ARGS_TAG} build_%: @@ -43,5 +44,8 @@ generateFrontend: backend: generateFrontend login_registry - docker buildx build --platform linux/amd64,linux/arm64 -t git.letsstein.de/tom/arbeitszeit-backend:latest Backend --push - docker buildx build --platform linux/amd64,linux/arm64 -t git.letsstein.de/tom/arbeitszeit-backend:${GIT_COMMIT} Backend --push + docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeit-backend:latest Backend --push + docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeit-backend:${GIT_COMMIT} Backend --push + +scan: + $(MAKE) -C Backend scan -- 2.49.1 From 3475e6d7c95a3dadbecc1b3e041a1bff4ed117af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 20 Aug 2025 18:55:47 +0200 Subject: [PATCH 09/39] experimenting with Jenkinsbuild --- Backend/.dockerignore | 1 + Backend/Makefile | 6 + Backend/static/css/styles.css | 104 ++++-- Jenkinsfile | 25 +- Makefile | 7 +- package.json | 6 + pnpm-lock.yaml | 655 ++++++++++++++++++++++++++++++++++ 7 files changed, 762 insertions(+), 42 deletions(-) create mode 100644 Backend/Makefile create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/Backend/.dockerignore b/Backend/.dockerignore index 7ab2ceb..c389270 100644 --- a/Backend/.dockerignore +++ b/Backend/.dockerignore @@ -1,2 +1,3 @@ db Dockerfile +templates/*.go diff --git a/Backend/Makefile b/Backend/Makefile new file mode 100644 index 0000000..23da1d0 --- /dev/null +++ b/Backend/Makefile @@ -0,0 +1,6 @@ +test: + mkdir -p .test + go test ./... -coverprofile=.test/coverage.out -json > .test/report.json + +scan: + sonar-scanner diff --git a/Backend/static/css/styles.css b/Backend/static/css/styles.css index f67f5de..e822525 100644 --- a/Backend/static/css/styles.css +++ b/Backend/static/css/styles.css @@ -1,4 +1,5 @@ -/*! tailwindcss v4.0.8 | MIT License | https://tailwindcss.com */ +/*! tailwindcss v4.1.12 | MIT License | https://tailwindcss.com */ +@layer properties; @layer theme, base, components, utilities; @layer theme { :root, :host { @@ -6,18 +7,18 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --color-red-500: oklch(0.637 0.237 25.331); - --color-red-600: oklch(0.577 0.245 27.325); - --color-orange-500: oklch(0.705 0.213 47.604); - --color-purple-600: oklch(0.558 0.288 302.321); - --color-neutral-100: oklch(0.97 0 0); - --color-neutral-200: oklch(0.922 0 0); - --color-neutral-300: oklch(0.87 0 0); - --color-neutral-400: oklch(0.708 0 0); - --color-neutral-500: oklch(0.556 0 0); - --color-neutral-700: oklch(0.371 0 0); - --color-neutral-800: oklch(0.269 0 0); - --color-neutral-900: oklch(0.205 0 0); + --color-red-500: oklch(63.7% 0.237 25.331); + --color-red-600: oklch(57.7% 0.245 27.325); + --color-orange-500: oklch(70.5% 0.213 47.604); + --color-purple-600: oklch(55.8% 0.288 302.321); + --color-neutral-100: oklch(97% 0 0); + --color-neutral-200: oklch(92.2% 0 0); + --color-neutral-300: oklch(87% 0 0); + --color-neutral-400: oklch(70.8% 0 0); + --color-neutral-500: oklch(55.6% 0 0); + --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-800: oklch(26.9% 0 0); + --color-neutral-900: oklch(20.5% 0 0); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -30,17 +31,7 @@ --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); - --default-font-feature-settings: var(--font-sans--font-feature-settings); - --default-font-variation-settings: var( - --font-sans--font-variation-settings - ); --default-mono-font-family: var(--font-mono); - --default-mono-font-feature-settings: var( - --font-mono--font-feature-settings - ); - --default-mono-font-variation-settings: var( - --font-mono--font-variation-settings - ); --color-accent: #0eaf23; } } @@ -55,14 +46,11 @@ line-height: 1.5; -webkit-text-size-adjust: 100%; tab-size: 4; - font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" ); + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); font-feature-settings: var(--default-font-feature-settings, normal); - font-variation-settings: var( --default-font-variation-settings, normal ); + font-variation-settings: var(--default-font-variation-settings, normal); -webkit-tap-highlight-color: transparent; } - body { - line-height: inherit; - } hr { height: 0; color: inherit; @@ -85,9 +73,9 @@ font-weight: bolder; } code, kbd, samp, pre { - font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace ); - font-feature-settings: var( --default-mono-font-feature-settings, normal ); - font-variation-settings: var( --default-mono-font-variation-settings, normal ); + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); font-size: 1em; } small { @@ -151,7 +139,14 @@ } ::placeholder { opacity: 1; - color: color-mix(in oklab, currentColor 50%, transparent); + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } } textarea { resize: vertical; @@ -172,6 +167,9 @@ ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-block: 0; } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } :-moz-ui-invalid { box-shadow: none; } @@ -186,9 +184,6 @@ } } @layer utilities { - .static { - position: static; - } .col-span-2 { grid-column: span 2 / span 2; } @@ -425,7 +420,7 @@ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } .transition { - transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter; + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } @@ -714,7 +709,44 @@ syntax: "*"; inherits: false; } +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} @property --tw-duration { syntax: "*"; inherits: false; } +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-divide-x-reverse: 0; + --tw-border-style: solid; + --tw-divide-y-reverse: 0; + --tw-font-weight: initial; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-duration: initial; + } + } +} diff --git a/Jenkinsfile b/Jenkinsfile index 5e68d30..bfdfc52 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,14 +2,32 @@ pipeline { environment { DOCKER_USERNAME = 'jenkins' DOCKER_PASSWORD = credentials('gitea_jenkins') - } + SONAR_TOKEN = credentials('sonarcube_token') + } agent any stages { - stage ("Building image arbeitszeit-backend"){ + stage('Test') { + agent { + docker { + image 'golang:alpine' + } + } + steps { + sh 'cd Backend && go mod download && go mod verify' + sh 'make test' + } + } + stage('SonarCube Analysis') { + steps { + sh 'make scan' + } + } + } + stage('Building image arbeitszeit-backend') { when { - anyOf{ + anyOf { changeset 'Jenkinsfile' changeset 'Makefile' changeset 'Backend/**' @@ -18,7 +36,6 @@ pipeline { steps { sh 'make backend' } - } } } diff --git a/Makefile b/Makefile index ccd2917..8ad2736 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,8 @@ backend: generateFrontend login_registry docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeit-backend:latest Backend --push docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeit-backend:${GIT_COMMIT} Backend --push -scan: - $(MAKE) -C Backend scan +test: + $(MAKE) -C Backend test + +scan: test + $(MAKE) -C Backend scan diff --git a/package.json b/package.json new file mode 100644 index 0000000..dc47876 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "tailwindcss": "^4.1.12", + "@tailwindcss/cli": "^4.1.12" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..18cb121 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,655 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@tailwindcss/cli': + specifier: ^4.1.12 + version: 4.1.12 + tailwindcss: + specifier: ^4.1.12 + version: 4.1.12 + +packages: + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.12': + resolution: {integrity: sha512-2PyJ5MGh/6JPS+cEaAq6MGDx3UemkX/mJt+/phm7/VOpycpecwNnHuFZbbgx6TNK/aIjvFOhhTVlappM7tmqvQ==} + hasBin: true + + '@tailwindcss/node@4.1.12': + resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} + + '@tailwindcss/oxide-android-arm64@4.1.12': + resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.12': + resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.12': + resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} + engines: {node: '>= 10'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tailwindcss@4.1.12: + resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + +snapshots: + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.12': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.12 + '@tailwindcss/oxide': 4.1.12 + enhanced-resolve: 5.18.3 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.12 + + '@tailwindcss/node@4.1.12': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.12 + + '@tailwindcss/oxide-android-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide@4.1.12': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-x64': 4.1.12 + '@tailwindcss/oxide-freebsd-x64': 4.1.12 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.12 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-x64-musl': 4.1.12 + '@tailwindcss/oxide-wasm32-wasi': 4.1.12 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chownr@3.0.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.0.4: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + graceful-fs@4.2.11: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.5.1: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + mri@1.2.0: {} + + node-addon-api@7.1.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + source-map-js@1.2.1: {} + + tailwindcss@4.1.12: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + yallist@5.0.0: {} -- 2.49.1 From 690a2e48f57fa58da6248b55e9c283508e8b6a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Wed, 20 Aug 2025 18:57:05 +0200 Subject: [PATCH 10/39] fixed Jenkins Syntax --- Jenkinsfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index bfdfc52..868f7ec 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,7 +24,6 @@ pipeline { sh 'make scan' } } - } stage('Building image arbeitszeit-backend') { when { anyOf { -- 2.49.1 From d4330fa193a88160427e92a8b525559e9910b53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 07:11:12 +0200 Subject: [PATCH 11/39] testing gitea actions --- .gitea/workflows/demo.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .gitea/workflows/demo.yaml diff --git a/.gitea/workflows/demo.yaml b/.gitea/workflows/demo.yaml new file mode 100644 index 0000000..c537cc6 --- /dev/null +++ b/.gitea/workflows/demo.yaml @@ -0,0 +1,19 @@ +name: Gitea Actions Demo +run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 +on: [push] + +jobs: + Explore-Gitea-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" + - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." + - name: Check out repository code + uses: actions/checkout@v4 + - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + ls ${{ gitea.workspace }} + - run: echo "🍏 This job's status is ${{ job.status }}." -- 2.49.1 From 17cb3b327c842125a43220273e4bf6ac28db4fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 07:14:54 +0200 Subject: [PATCH 12/39] Create testing.yaml --- .gitea/workflows/testing.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitea/workflows/testing.yaml diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml new file mode 100644 index 0000000..43fc95c --- /dev/null +++ b/.gitea/workflows/testing.yaml @@ -0,0 +1,10 @@ +name: GoLang Tests +run-name: ${{ gitea.actor }} is testing golang Code +on: [push] + +jobs: + Run-Go-Test: + runs-on: golang-alpine + steps: + - run: go version + - run: go test ./... -- 2.49.1 From 61635dff6df0b599716774525cbb9f8a40472d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 07:21:45 +0200 Subject: [PATCH 13/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 43fc95c..1a22a7f 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -4,7 +4,7 @@ on: [push] jobs: Run-Go-Test: - runs-on: golang-alpine + runs-on: ubuntu-latest:docker://golang:alpine steps: - run: go version - run: go test ./... -- 2.49.1 From 8fa5eafd80aa39f7b83f55bbd2d0e5275687c99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 07:46:22 +0200 Subject: [PATCH 14/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 1a22a7f..56ee08c 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -4,7 +4,7 @@ on: [push] jobs: Run-Go-Test: - runs-on: ubuntu-latest:docker://golang:alpine + runs-on: golang steps: - run: go version - run: go test ./... -- 2.49.1 From 8f8f67398b144b6f8f6381058af08261e14b221e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 07:59:41 +0200 Subject: [PATCH 15/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 56ee08c..6a318b3 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -3,8 +3,25 @@ run-name: ${{ gitea.actor }} is testing golang Code on: [push] jobs: - Run-Go-Test: - runs-on: golang + test: + runs-on: docker + container: + image: golang:1.22 steps: - - run: go version - - run: go test ./... + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go cache + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run tests + run: | + go mod tidy + go test ./... -v -- 2.49.1 From c813ba62d4fc52c00defb42e2d30cfcf0b8d3f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 08:01:17 +0200 Subject: [PATCH 16/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 6a318b3..4db6fa5 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -22,6 +22,7 @@ jobs: ${{ runner.os }}-go- - name: Run tests + shell: sh run: | go mod tidy go test ./... -v -- 2.49.1 From e94942181d0acfa2588f4e21bad59a2d35f0cea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 08:05:40 +0200 Subject: [PATCH 17/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 4db6fa5..386f633 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -8,21 +8,15 @@ jobs: container: image: golang:1.22 steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout repository + uses: https://gitea.com/actions/checkout@v4 - - name: Set up Go cache - uses: actions/cache@v3 - with: - path: | - ~/go/pkg/mod - ~/.cache/go-build - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + - name: Go dependencies + shell: sh + run: | + go mod tidy - name: Run tests shell: sh run: | - go mod tidy go test ./... -v -- 2.49.1 From a521377f7fcab4738d98a681a8a4816dbb260980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 08:12:40 +0200 Subject: [PATCH 18/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 43 +++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 386f633..c077009 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -3,20 +3,33 @@ run-name: ${{ gitea.actor }} is testing golang Code on: [push] jobs: - test: - runs-on: docker - container: - image: golang:1.22 + testing: + name: check and test + runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: https://gitea.com/actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # all history for all branches and tags - - name: Go dependencies - shell: sh - run: | - go mod tidy - - - name: Run tests - shell: sh - run: | - go test ./... -v + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: Backend/go.mod + check-latest: true + - uses: https://gitea.com/actions/go-hashfiles@v0.0.1 + id: hash-go + with: + patterns: | + go.mod + go.sum + - name: cache go + id: cache-go + uses: actions/cache@v4 + with: + path: | + /go_path + /go_cache + key: go_path-${{ steps.hash-go.outputs.hash }} + - name: test + run: make test -- 2.49.1 From dd8df1059e050dff6652a2f5502d8030d6f7a640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 08:38:57 +0200 Subject: [PATCH 19/39] added db --- .gitea/workflows/demo.yaml | 19 ------------------- .gitea/workflows/testing.yaml | 11 +++++++++++ 2 files changed, 11 insertions(+), 19 deletions(-) delete mode 100644 .gitea/workflows/demo.yaml diff --git a/.gitea/workflows/demo.yaml b/.gitea/workflows/demo.yaml deleted file mode 100644 index c537cc6..0000000 --- a/.gitea/workflows/demo.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Gitea Actions Demo -run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 -on: [push] - -jobs: - Explore-Gitea-Actions: - runs-on: ubuntu-latest - steps: - - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." - - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" - - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." - - name: Check out repository code - uses: actions/checkout@v4 - - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." - - run: echo "🖥️ The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ gitea.workspace }} - - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index c077009..6b3e195 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -6,6 +6,10 @@ jobs: testing: name: check and test runs-on: ubuntu-latest + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + POSTGRES_DB: arbeitszeitmessung steps: - name: Checkout uses: actions/checkout@v4 @@ -31,5 +35,12 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} + - name: Setup DB + uses: harmon758/postgresql-action@v1 + with: + postgresql version: "16" + postgresql user: $POSTGRES_USER + postgresql password: $POSTGRES_PASSWORD + postgresql db: $POSTGRES_DB - name: test run: make test -- 2.49.1 From cbdd82b72542765f4c9fc19891c009e8298d3aa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 08:46:20 +0200 Subject: [PATCH 20/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 6b3e195..d38d99b 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -39,8 +39,8 @@ jobs: uses: harmon758/postgresql-action@v1 with: postgresql version: "16" - postgresql user: $POSTGRES_USER - postgresql password: $POSTGRES_PASSWORD - postgresql db: $POSTGRES_DB + postgresql user: ${{ env.POSTGRES_USER }} + postgresql password: ${{ env.POSTGRES_PASSWORD }} + postgresql db: ${{ env.POSTGRES_DB }} - name: test run: make test -- 2.49.1 From 5c243e45e118cb7e6cb97b8737d3b40618fbdea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 15:00:25 +0200 Subject: [PATCH 21/39] added Migrate Function for tests --- .gitea/workflows/testing.yaml | 1 + Backend/go.mod | 11 ++++++++++- Backend/go.sum | 20 ++++++++++++++++++++ Backend/models/db_test.go | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index d38d99b..d142795 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -10,6 +10,7 @@ jobs: POSTGRES_USER: root POSTGRES_PASSWORD: password POSTGRES_DB: arbeitszeitmessung + POSTGRES_PORT: 5432 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/Backend/go.mod b/Backend/go.mod index f680c35..53b8b2a 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -10,4 +10,13 @@ require github.com/a-h/templ v0.3.924 require github.com/alexedwards/scs/v2 v2.8.0 -require github.com/joho/godotenv v1.5.1 +require ( + github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + go.uber.org/atomic v1.7.0 // indirect +) diff --git a/Backend/go.sum b/Backend/go.sum index 58f050d..4d25b7c 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -2,9 +2,29 @@ github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k= github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= +github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/Backend/models/db_test.go b/Backend/models/db_test.go index 21d25f5..56ec4a5 100644 --- a/Backend/models/db_test.go +++ b/Backend/models/db_test.go @@ -1,10 +1,17 @@ package models_test import ( + "arbeitszeitmessung/helper" "arbeitszeitmessung/models" "database/sql" + "fmt" + "log" "testing" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/lib/pq" ) @@ -16,11 +23,24 @@ type DBFixture struct { func SetupDBFixture(t *testing.T) *DBFixture { t.Helper() - db, err := sql.Open("postgres", "postgres://postgres:password@localhost:5433/arbeitszeitmessung?sslmode=disable") + dbHost := helper.GetEnv("POSTGRES_HOST", "localhost") + dbPort := helper.GetEnv("POSTGRES_PORT", "5433") + dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung") + dbUser := helper.GetEnv("POSTGRES_USER", "postgres") + dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password") + + connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbPort, dbName) + + db, err := sql.Open("postgres", connStr) if err != nil { t.Fatalf("failed to connect to database: %v", err) } + err = MigrateDB(db, "file://../../migrations") + if err != nil && err != migrate.ErrNoChange { + t.Fatalf("Failed to migrate database: %v", err) + } + tx, err := db.Begin() if err != nil { t.Fatalf("Failed to start transaction: %v", err) @@ -36,3 +56,14 @@ func SetupDBFixture(t *testing.T) *DBFixture { TX: tx, } } + +func MigrateDB(db *sql.DB, fileUrl string) error { + driver, err := postgres.WithInstance(db, &postgres.Config{}) + if err != nil { + log.Fatalln("Error starting migration", err) + } + m, err := migrate.NewWithDatabaseInstance( + fileUrl, + "postgres", driver) + return m.Up() +} -- 2.49.1 From 945f97ef3233790399bfb65edc783d4371b13c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 15:04:44 +0200 Subject: [PATCH 22/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index d142795..96c1d17 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -12,6 +12,12 @@ jobs: POSTGRES_DB: arbeitszeitmessung POSTGRES_PORT: 5432 steps: + - uses: harmon758/postgresql-action@v1 + with: + postgresql version: "16" + postgresql user: ${{ env.POSTGRES_USER }} + postgresql password: ${{ env.POSTGRES_PASSWORD }} + postgresql db: ${{ env.POSTGRES_DB }} - name: Checkout uses: actions/checkout@v4 with: @@ -36,12 +42,5 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - - name: Setup DB - uses: harmon758/postgresql-action@v1 - with: - postgresql version: "16" - postgresql user: ${{ env.POSTGRES_USER }} - postgresql password: ${{ env.POSTGRES_PASSWORD }} - postgresql db: ${{ env.POSTGRES_DB }} - - name: test + - name: Run Go Test run: make test -- 2.49.1 From ccc82527d34d840aeb9707da152b8abbe6e661de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 15:17:52 +0200 Subject: [PATCH 23/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 96c1d17..75a1d8e 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -43,4 +43,4 @@ jobs: /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - name: Run Go Test - run: make test + run: go test ./... -- 2.49.1 From 4974aa981556b1121e0bf20ab6aaed63973135c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 15:20:46 +0200 Subject: [PATCH 24/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 75a1d8e..86cdd5d 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -43,4 +43,4 @@ jobs: /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - name: Run Go Test - run: go test ./... + run: cd Backend && go test ./... -- 2.49.1 From 6c544174d0fcbc4bd7c7accdd526344fe2b55532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 15:22:49 +0200 Subject: [PATCH 25/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 86cdd5d..94b24e1 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -12,12 +12,6 @@ jobs: POSTGRES_DB: arbeitszeitmessung POSTGRES_PORT: 5432 steps: - - uses: harmon758/postgresql-action@v1 - with: - postgresql version: "16" - postgresql user: ${{ env.POSTGRES_USER }} - postgresql password: ${{ env.POSTGRES_PASSWORD }} - postgresql db: ${{ env.POSTGRES_DB }} - name: Checkout uses: actions/checkout@v4 with: @@ -42,5 +36,10 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - - name: Run Go Test - run: cd Backend && go test ./... + - uses: harmon758/postgresql-action@v1 + with: + postgresql version: "16" + postgresql user: ${{ env.POSTGRES_USER }} + postgresql password: ${{ env.POSTGRES_PASSWORD }} + postgresql db: ${{ env.POSTGRES_DB }} + - run: cd Backend && go test ./... -- 2.49.1 From 81cdca42f9a8ab7910b70bc1a3c49dddb08b962d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 22:23:22 +0200 Subject: [PATCH 26/39] hopefully working --- .gitea/workflows/testing.yaml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 94b24e1..aa94de4 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -36,10 +36,18 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - - uses: harmon758/postgresql-action@v1 - with: - postgresql version: "16" - postgresql user: ${{ env.POSTGRES_USER }} - postgresql password: ${{ env.POSTGRES_PASSWORD }} - postgresql db: ${{ env.POSTGRES_DB }} - - run: cd Backend && go test ./... + - name: Setup test database + run: | + docker run -d --rm --name postgres_build_db \ + -p $POSTGRES_PORT:5432 \ + -e POSTGRES_USER=$POSTGRES_USER \ + -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \ + -e POSTGRES_DB=$POSTGRES_DB \ + postgres:16 + while [[ $(docker logs postgres_build_db 2>&1 | grep -c "database system is ready to accept connections") == 0 ]]; do + sleep 1; + done; + - name: Run Go Tests + run: cd Backend && go test ./... + - name: Shutdown Postgres DB + run: docker stop postgres_build_db -- 2.49.1 From e5a1ee1d0e085112a496a9433de48768c44da53c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 22:36:52 +0200 Subject: [PATCH 27/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index aa94de4..fd4a4e3 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -6,7 +6,15 @@ jobs: testing: name: check and test runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: root + POSTGRES_PASSWORD: password + POSTGRES_DB: arbeitszeitmessung env: + POSTGRES_HOST: postgres POSTGRES_USER: root POSTGRES_PASSWORD: password POSTGRES_DB: arbeitszeitmessung @@ -36,18 +44,18 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - - name: Setup test database - run: | - docker run -d --rm --name postgres_build_db \ - -p $POSTGRES_PORT:5432 \ - -e POSTGRES_USER=$POSTGRES_USER \ - -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \ - -e POSTGRES_DB=$POSTGRES_DB \ - postgres:16 - while [[ $(docker logs postgres_build_db 2>&1 | grep -c "database system is ready to accept connections") == 0 ]]; do - sleep 1; - done; + # - name: Setup test database + # run: | + # docker run -d --rm --name postgres_build_db \ + # -p $POSTGRES_PORT:5432 \ + # -e POSTGRES_USER=$POSTGRES_USER \ + # -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \ + # -e POSTGRES_DB=$POSTGRES_DB \ + # postgres:16 + # while [[ $(docker logs postgres_build_db 2>&1 | grep -c "database system is ready to accept connections") == 0 ]]; do + # sleep 1; + # done; - name: Run Go Tests run: cd Backend && go test ./... - - name: Shutdown Postgres DB - run: docker stop postgres_build_db + # - name: Shutdown Postgres DB + # run: docker stop postgres_build_db -- 2.49.1 From 007d30f87442ffeadaaa79e69f9ca43a02029185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 22:51:35 +0200 Subject: [PATCH 28/39] Added Build Step --- .gitea/workflows/testing.yaml | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index fd4a4e3..448ac95 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -4,7 +4,7 @@ on: [push] jobs: testing: - name: check and test + name: Run Go Tests runs-on: ubuntu-latest services: postgres: @@ -22,8 +22,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 # all history for all branches and tags - name: Setup go uses: actions/setup-go@v5 @@ -44,18 +42,18 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} - # - name: Setup test database - # run: | - # docker run -d --rm --name postgres_build_db \ - # -p $POSTGRES_PORT:5432 \ - # -e POSTGRES_USER=$POSTGRES_USER \ - # -e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \ - # -e POSTGRES_DB=$POSTGRES_DB \ - # postgres:16 - # while [[ $(docker logs postgres_build_db 2>&1 | grep -c "database system is ready to accept connections") == 0 ]]; do - # sleep 1; - # done; - name: Run Go Tests run: cd Backend && go test ./... - # - name: Shutdown Postgres DB - # run: docker stop postgres_build_db + build: + needs: testing + name: Build Go Image and Upload + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: git.letsstein.de + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} -- 2.49.1 From 3bd449203c579619d1a32fa0638ccea4c12427b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 22:59:13 +0200 Subject: [PATCH 29/39] tying cache --- .gitea/workflows/testing.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 448ac95..26d6af4 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -42,6 +42,8 @@ jobs: /go_path /go_cache key: go_path-${{ steps.hash-go.outputs.hash }} + restore-keys: |- + go_path-${{ steps.hash-go.outputs.hash }} - name: Run Go Tests run: cd Backend && go test ./... build: -- 2.49.1 From 87d2dd86ebc62625995ab78d4c384c5b0767fec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 23:10:31 +0200 Subject: [PATCH 30/39] trying build --- .gitea/workflows/testing.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 26d6af4..fbce127 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -41,9 +41,9 @@ jobs: path: | /go_path /go_cache - key: go_path-${{ steps.hash-go.outputs.hash }} + key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} restore-keys: |- - go_path-${{ steps.hash-go.outputs.hash }} + arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} - name: Run Go Tests run: cd Backend && go test ./... build: @@ -59,3 +59,13 @@ jobs: registry: git.letsstein.de username: ${{ gitea.actor }} password: ${{ secrets.REGISTRY_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: git.letsstein.de/tom/arbeitszeitmessung:latest -- 2.49.1 From f56fde9bfdad8117ea3d02f5d922fd4f26863ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 23:15:22 +0200 Subject: [PATCH 31/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index fbce127..6f2a08d 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -69,3 +69,4 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: git.letsstein.de/tom/arbeitszeitmessung:latest + context: Docker -- 2.49.1 From 92349dc7d2db94459a6a9a9b8ae92ff75a91bd35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 23:19:48 +0200 Subject: [PATCH 32/39] better context --- .gitea/workflows/testing.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 6f2a08d..2fe794f 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -47,7 +47,7 @@ jobs: - name: Run Go Tests run: cd Backend && go test ./... build: - needs: testing + # needs: testing name: Build Go Image and Upload runs-on: ubuntu-latest steps: @@ -69,4 +69,4 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: git.letsstein.de/tom/arbeitszeitmessung:latest - context: Docker + context: Backend -- 2.49.1 From 119c0c3b3391a166c4346e1fa4e293a9fd7619f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 23:34:31 +0200 Subject: [PATCH 33/39] next try --- Backend/.dockerignore | 1 - Backend/database.go | 12 +++++++++++ Backend/go.mod | 2 +- Backend/go.sum | 48 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Backend/.dockerignore b/Backend/.dockerignore index c389270..7ab2ceb 100644 --- a/Backend/.dockerignore +++ b/Backend/.dockerignore @@ -1,3 +1,2 @@ db Dockerfile -templates/*.go diff --git a/Backend/database.go b/Backend/database.go index 24b7dd6..81e9158 100644 --- a/Backend/database.go +++ b/Backend/database.go @@ -5,6 +5,10 @@ import ( "arbeitszeitmessung/models" "database/sql" "fmt" + "log" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/source/file" ) func OpenDatabase() (models.IDatabase, error) { @@ -21,3 +25,11 @@ func OpenDatabase() (models.IDatabase, error) { defer db.Close() return db, err } + +func MigrateDB(db models.IDatabase, connStr string) { + m, err := migrate.New("file:///../migrations/", connStr) + if err != nil { + log.Fatalln("Error starting migration", err) + } + m.Up() +} diff --git a/Backend/go.mod b/Backend/go.mod index 53b8b2a..6987c36 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.5 require github.com/lib/pq v1.10.9 -require github.com/a-h/templ v0.3.924 +require github.com/a-h/templ v0.3.943 require github.com/alexedwards/scs/v2 v2.8.0 diff --git a/Backend/go.sum b/Backend/go.sum index 4d25b7c..82a9cc4 100644 --- a/Backend/go.sum +++ b/Backend/go.sum @@ -1,10 +1,32 @@ -github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k= -github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY= +github.com/a-h/templ v0.3.943/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= +github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -18,13 +40,35 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -- 2.49.1 From 9f31574f3d68666e623d510e1ae9cdc982cd2844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 23:43:10 +0200 Subject: [PATCH 34/39] testing cache --- .gitea/workflows/testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 2fe794f..99348d2 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -43,7 +43,7 @@ jobs: /go_cache key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} restore-keys: |- - arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} + arbeitszeitmessung- - name: Run Go Tests run: cd Backend && go test ./... build: -- 2.49.1 From c1b937152bcc0c09dee571e14300549a3788f131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 21 Aug 2025 23:49:59 +0200 Subject: [PATCH 35/39] Update testing.yaml --- .gitea/workflows/testing.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 99348d2..62c456c 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -19,6 +19,7 @@ jobs: POSTGRES_PASSWORD: password POSTGRES_DB: arbeitszeitmessung POSTGRES_PORT: 5432 + RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache steps: - name: Checkout uses: actions/checkout@v4 @@ -27,7 +28,6 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: Backend/go.mod - check-latest: true - uses: https://gitea.com/actions/go-hashfiles@v0.0.1 id: hash-go with: @@ -38,7 +38,7 @@ jobs: id: cache-go uses: actions/cache@v4 with: - path: | + path: |- /go_path /go_cache key: arbeitszeitmessung-${{ steps.hash-go.outputs.hash }} @@ -47,7 +47,7 @@ jobs: - name: Run Go Tests run: cd Backend && go test ./... build: - # needs: testing + needs: testing name: Build Go Image and Upload runs-on: ubuntu-latest steps: -- 2.49.1 From 34bd44db5c4668228b90b2bdb0ca8c8fee26c0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 22 Aug 2025 00:13:00 +0200 Subject: [PATCH 36/39] ready for action --- .gitea/workflows/{testing.yaml => build.yaml} | 0 Backend/go.mod | 4 +- Docker/docker-compose.dev.yml | 2 +- Docker/docker-compose.test.yml | 3 +- Docker/docker-compose.yml | 2 +- Jenkinsfile | 64 +++++++++++++------ 6 files changed, 49 insertions(+), 26 deletions(-) rename .gitea/workflows/{testing.yaml => build.yaml} (100%) diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/build.yaml similarity index 100% rename from .gitea/workflows/testing.yaml rename to .gitea/workflows/build.yaml diff --git a/Backend/go.mod b/Backend/go.mod index 6987c36..d5c6953 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -1,8 +1,6 @@ module arbeitszeitmessung -go 1.23.0 - -toolchain go1.24.5 +go 1.24.5 require github.com/lib/pq v1.10.9 diff --git a/Docker/docker-compose.dev.yml b/Docker/docker-compose.dev.yml index 2abeb41..b1884d8 100644 --- a/Docker/docker-compose.dev.yml +++ b/Docker/docker-compose.dev.yml @@ -20,7 +20,7 @@ services: - 8001:8080 backend: build: ../Backend - image: git.letsstein.de/tom/arbeitszeit-backend:0.1.1 + image: git.letsstein.de/tom/arbeitszeitmessung restart: unless-stopped env_file: - .env diff --git a/Docker/docker-compose.test.yml b/Docker/docker-compose.test.yml index fbfeb50..0d5e807 100644 --- a/Docker/docker-compose.test.yml +++ b/Docker/docker-compose.test.yml @@ -7,7 +7,6 @@ services: - .env.test environment: PGDATA: /var/lib/postgresql/data/pg_data - volumes: - - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d + # volumes: //- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d ports: - 5433:5432 diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml index f484d08..272fc49 100644 --- a/Docker/docker-compose.yml +++ b/Docker/docker-compose.yml @@ -17,7 +17,7 @@ services: - 5432:5432 backend: - image: git.letsstein.de/tom/arbeitszeit-backend + image: git.letsstein.de/tom/arbeitszeitmessung env_file: - .env environment: diff --git a/Jenkinsfile b/Jenkinsfile index 868f7ec..a5d5f5c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,9 @@ pipeline { DOCKER_USERNAME = 'jenkins' DOCKER_PASSWORD = credentials('gitea_jenkins') SONAR_TOKEN = credentials('sonarcube_token') + POSTGRES_USER = 'postgres' + POSTGRES_PASSWORD = 'password' + POSTGRES_DB = 'arbeitszeitmessung' } agent any @@ -11,30 +14,53 @@ pipeline { stage('Test') { agent { docker { - image 'golang:alpine' + image '' + args '' + args '' } } steps { - sh 'cd Backend && go mod download && go mod verify' - sh 'make test' - } - } - stage('SonarCube Analysis') { - steps { - sh 'make scan' - } - } - stage('Building image arbeitszeit-backend') { - when { - anyOf { - changeset 'Jenkinsfile' - changeset 'Makefile' - changeset 'Backend/**' + script { + sh ''' + docker run -d --rm \ + --name test-db \ + -e POSTGRES_USER={$POSTGRES_USER} \ + -e POSTGRES_PASSWORD={$POSTGRES_PASSWORD} \ + -e POSTGRES_DB={$POSTGRES_DB} \ + -v ./DB/initdb:/docker-entrypoint-initdb.d\ + -p "5432:5432" \ + postgres:16 + ''' + // docker.image('golang:1.24.5').withRun( + // '-u root:root --network=host' + // ) { go -> + // // wait for DB to start + // sh ''' + // cd Backend \ + // go mod download && go mod tidy \ + // go test ./... -v + + // ''' + // } } } - steps { - sh 'make backend' - } } + stage('SonarCube Analysis') { + steps { + sh 'make scan' + } + } + stage('Building image arbeitszeit-backend') { + when { + anyOf { + changeset 'Jenkinsfile' + changeset 'Makefile' + changeset 'Backend/**' + } + } + steps { + sh 'make backend' + } + } } } -- 2.49.1 From 50bec238a4db2988266af6ee316698cd99bd3a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 22 Aug 2025 00:50:21 +0200 Subject: [PATCH 37/39] edge case at midnight --- Backend/models/db_test.go | 8 ++++---- Backend/models/user_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Backend/models/db_test.go b/Backend/models/db_test.go index 56ec4a5..629bd2d 100644 --- a/Backend/models/db_test.go +++ b/Backend/models/db_test.go @@ -36,10 +36,10 @@ func SetupDBFixture(t *testing.T) *DBFixture { t.Fatalf("failed to connect to database: %v", err) } - err = MigrateDB(db, "file://../../migrations") - if err != nil && err != migrate.ErrNoChange { - t.Fatalf("Failed to migrate database: %v", err) - } + // err = MigrateDB(db, "file://../../migrations") + // if err != nil && err != migrate.ErrNoChange { + // t.Fatalf("Failed to migrate database: %v", err) + // } tx, err := db.Begin() if err != nil { diff --git a/Backend/models/user_test.go b/Backend/models/user_test.go index dd9ccf4..323f925 100644 --- a/Backend/models/user_test.go +++ b/Backend/models/user_test.go @@ -44,12 +44,12 @@ func TestCheckAnwesenheit(t *testing.T) { if actual = testUser.CheckAnwesenheit(); actual != false { t.Errorf("Checkabwesenheit with no booking should be false but is %t", actual) } - tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '2 hour', 'aaaa-aaaa', 1, 1);") + tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '2 minute', 'aaaa-aaaa', 1, 1);") if actual = testUser.CheckAnwesenheit(); actual != true { t.Errorf("Checkabwesenheit with 'kommen' booking should be true but is %t", actual) } - tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '1 hour', 'aaaa-aaaa', 2, 1);") + tc.Database.Exec("INSERT INTO anwesenheit (timestamp, card_uid, check_in_out, geraet_id) VALUES (NOW() - INTERVAL '1 minute', 'aaaa-aaaa', 2, 1);") if actual = testUser.CheckAnwesenheit(); actual != false { t.Errorf("Checkabwesenheit with 'gehen' booking should be false but is %t", actual) } -- 2.49.1 From 23e05b8cb567fc2f1b57dc4210c87b371d0e67ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Fri, 22 Aug 2025 00:51:10 +0200 Subject: [PATCH 38/39] error in db code, closing database before usage --- Backend/database.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Backend/database.go b/Backend/database.go index 81e9158..bad1b77 100644 --- a/Backend/database.go +++ b/Backend/database.go @@ -5,10 +5,8 @@ import ( "arbeitszeitmessung/models" "database/sql" "fmt" - "log" - "github.com/golang-migrate/migrate/v4" - _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/lib/pq" ) func OpenDatabase() (models.IDatabase, error) { @@ -18,18 +16,5 @@ func OpenDatabase() (models.IDatabase, error) { dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password") connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbName) - db, err := sql.Open("postgres", connStr) - if err != nil { - return nil, err - } - defer db.Close() - return db, err -} - -func MigrateDB(db models.IDatabase, connStr string) { - m, err := migrate.New("file:///../migrations/", connStr) - if err != nil { - log.Fatalln("Error starting migration", err) - } - m.Up() + return sql.Open("postgres", connStr) } -- 2.49.1 From fd2c702b5fefaea854743f9702ea9960b5d9a50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=20Tr=C3=B6ger?= Date: Thu, 28 Aug 2025 10:55:46 +0200 Subject: [PATCH 39/39] Add LICENSE --- LICENSE | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file -- 2.49.1