20 Commits
1.2.1 ... 1.4.1

Author SHA1 Message Date
31d4ec5f27 fix(templates): Correct spacing in group name display
All checks were successful
Build release images / build-container (push) Successful in 1m51s
Build latest image / build-container (push) Successful in 2m13s
2025-03-26 19:29:32 +01:00
d61d4d6e7e refactor(ci): Switch to actions
All checks were successful
Build latest image / build-container (push) Successful in 1m39s
2025-03-22 22:48:35 +01:00
606ce6b940 docs(swagger): Build new docs
Some checks failed
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline failed
2024-12-17 17:51:51 +01:00
750fa70332 feat(models): Support nested groups 2024-12-17 17:51:11 +01:00
7d503edbc9 feat(templates): Support nested groups 2024-12-17 17:50:54 +01:00
5c9235df8d fix(templates): Enable blank cards 2024-12-17 17:38:42 +01:00
11ea0858bb feat(services): Logging
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 16:23:52 +01:00
4d57cf827d refactor(handler): Move array manipulation
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 16:07:40 +01:00
df9f7fdc13 feat(handlers): Added info logging 2024-12-17 16:07:02 +01:00
cdd2b5e250 feat(logging): Debug logging 2024-12-17 15:48:12 +01:00
94b766f106 feat(logger): Log levels 2024-12-17 15:45:40 +01:00
a2e94f715b refactor(logs): Replaced main logger with zap
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:31:23 +01:00
f64daaf817 feat: Request-IDs for better debugging 2024-12-16 17:23:58 +01:00
b4bb732303 fix(config): typoed defaults
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:02:49 +01:00
3dee3e72af fix: Bad dependency
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:41:52 +01:00
f5914e5c38 style: Formatting
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-16 16:39:56 +01:00
5a5a7179e9 feat: Use CORS
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline failed
2024-12-16 16:39:02 +01:00
6c57d63891 fix(config): Added SPONSORING_DISCLAIMER default
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 16:31:08 +01:00
b502e2fbd5 fix(config): Defaults for everyone
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:30:02 +01:00
c9475d0093 feat(config): log config on boot
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:17:26 +01:00
26 changed files with 299 additions and 71 deletions

1
.env
View File

@@ -1,3 +1,4 @@
LOGLEVEL=debug
PORT=3000 PORT=3000
PRODUCION=false PRODUCION=false
APIKEY=lfk APIKEY=lfk

27
.gitea/workflows/dev.yaml Normal file
View File

@@ -0,0 +1,27 @@
name: Build latest image
on:
push:
branches:
- main
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/document-server:latest
platforms: linux/amd64,linux/arm64

View File

@@ -0,0 +1,27 @@
name: Build release images
on:
push:
tags:
- "*.*.*"
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/document-server:${{ github.ref_name }}
platforms: linux/amd64,linux/arm64

View File

@@ -1,18 +0,0 @@
steps:
- name: build latest
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/document-server
tags:
- latest
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/document-server:latest
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
branch: main
when:
event: push

View File

@@ -1,17 +0,0 @@
steps:
- name: build tag
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/document-server
tags:
- "${CI_COMMIT_TAG}"
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/document-server:latest
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
event:
- tag

View File

@@ -326,6 +326,17 @@ const docTemplate = `{
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string"
},
"parent_group": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
} }
} }
}, },

View File

@@ -317,6 +317,17 @@
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string"
},
"parent_group": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
} }
} }
}, },

View File

@@ -96,6 +96,13 @@ definitions:
properties: properties:
name: name:
type: string type: string
parent_group:
properties:
name:
type: string
required:
- name
type: object
required: required:
- name - name
type: object type: object

3
go.mod
View File

@@ -49,7 +49,8 @@ require (
github.com/valyala/fasthttp v1.57.0 // indirect github.com/valyala/fasthttp v1.57.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.19.0 // indirect

4
go.sum
View File

@@ -107,6 +107,10 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=

View File

@@ -20,6 +20,8 @@ import (
// @Router /v1/barcodes/{type}/{content} [get] // @Router /v1/barcodes/{type}/{content} [get]
func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error { func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
logger := h.Logger.Named("GenerateBarcode")
// Get the type and content from the URL // Get the type and content from the URL
barcodeType := c.Params("type") barcodeType := c.Params("type")
barcodeContent := c.Params("content") barcodeContent := c.Params("content")
@@ -32,6 +34,7 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
// Convert width and height to integers // Convert width and height to integers
width, err := strconv.Atoi(widthStr) width, err := strconv.Atoi(widthStr)
if err != nil { if err != nil {
logger.Errorw("Invalid width parameter", "width", widthStr, "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid width parameter", "error": "Invalid width parameter",
}) })
@@ -39,6 +42,7 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
height, err := strconv.Atoi(heightStr) height, err := strconv.Atoi(heightStr)
if err != nil { if err != nil {
logger.Errorw("Invalid height parameter", "height", heightStr, "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid height parameter", "error": "Invalid height parameter",
}) })
@@ -46,18 +50,23 @@ func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
padding, err := strconv.Atoi(paddingStr) padding, err := strconv.Atoi(paddingStr)
if err != nil { if err != nil {
logger.Errorw("Invalid padding parameter", "padding", paddingStr, "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid padding parameter", "error": "Invalid padding parameter",
}) })
} }
logger = logger.With("type", barcodeType, "content", barcodeContent, "width", width, "height", height, "padding", padding)
// Generate the barcode // Generate the barcode
logger.Info("Generating barcode")
barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding) barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding)
if err != nil { if err != nil {
logger.Errorw("Failed to generate barcode", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Barcode generated")
c.Set(fiber.HeaderContentType, "image/png") c.Set(fiber.HeaderContentType, "image/png")
return c.Send(barcode.Bytes()) return c.Send(barcode.Bytes())

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"log"
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
@@ -19,27 +18,35 @@ import (
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /v1/pdfs/cards [post] // @Router /v1/pdfs/cards [post]
func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error { func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
logger := h.Logger.Named("GenerateCard")
cardRequest := new(models.CardRequest) cardRequest := new(models.CardRequest)
if err := c.BodyParser(cardRequest); err != nil { if err := c.BodyParser(cardRequest); err != nil {
logger.Errorw("Invalid request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) { if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) {
logger.Errorw("Invalid locale", "locale", cardRequest.Locale)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale", "error": "Invalid locale",
}) })
} }
logger = logger.With("locale", cardRequest.Locale)
templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card") templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
if err != nil { if err != nil {
log.Println(err) logger.Errorw("Template not found", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := h.Templater.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
logger.Errorw("Error parsing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
@@ -53,20 +60,26 @@ func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
BarcodePrefix: h.Config.CardBarcodePrefix, BarcodePrefix: h.Config.CardBarcodePrefix,
} }
logger.Info("Generating card html")
result, err := h.Templater.Execute(template, genConfig) result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
logger.Errorw("Error executing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Generated card html")
c.Set(fiber.HeaderContentType, "text/html") c.Set(fiber.HeaderContentType, "text/html")
logger.Info("Converting html to pdf")
pdf, err := h.Converter.ToPdf(result, "a4", false) pdf, err := h.Converter.ToPdf(result, "a4", false)
if err != nil { if err != nil {
logger.Errorw("Error converting html to pdf", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Converted html to pdf")
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf") c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"log"
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
@@ -19,27 +18,35 @@ import (
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /v1/pdfs/certificates [post] // @Router /v1/pdfs/certificates [post]
func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error { func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
logger := h.Logger.Named("GenerateCertificate")
certificateRequest := new(models.CertificateRequest) certificateRequest := new(models.CertificateRequest)
if err := c.BodyParser(certificateRequest); err != nil { if err := c.BodyParser(certificateRequest); err != nil {
logger.Errorw("Invalid request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) { if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) {
logger.Errorw("Invalid locale", "locale", certificateRequest.Locale)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale", "error": "Invalid locale",
}) })
} }
logger = logger.With("locale", certificateRequest.Locale)
templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate") templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
if err != nil { if err != nil {
log.Println(err) logger.Errorw("Template not found", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := h.Templater.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
logger.Errorw("Error parsing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
@@ -53,20 +60,24 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
Locale: certificateRequest.Locale, Locale: certificateRequest.Locale,
} }
logger.Info("Generating certificate html")
result, err := h.Templater.Execute(template, genConfig) result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Generated card html")
c.Set(fiber.HeaderContentType, "text/html") c.Set(fiber.HeaderContentType, "text/html")
logger.Info("Converting html to pdf")
pdf, err := h.Converter.ToPdf(result, "a4", false) pdf, err := h.Converter.ToPdf(result, "a4", false)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Converted html to pdf")
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf") c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf")

View File

@@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"log"
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
@@ -19,36 +18,42 @@ import (
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Router /v1/pdfs/contracts [post] // @Router /v1/pdfs/contracts [post]
func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error { func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
logger := h.Logger.Named("GenerateContract")
contract := new(models.ContractRequest) contract := new(models.ContractRequest)
if err := c.BodyParser(contract); err != nil { if err := c.BodyParser(contract); err != nil {
logger.Errorw("Invalid request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
if !slices.Contains([]string{"en", "de"}, contract.Locale) { if !slices.Contains([]string{"en", "de"}, contract.Locale) {
logger.Errorw("Invalid locale", "locale", contract.Locale)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale", "error": "Invalid locale",
}) })
} }
contract.Runners = repeatRunnerArrayItems(contract.Runners, 2) logger = logger.With("locale", contract.Locale)
templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract") templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
if err != nil { if err != nil {
log.Println(err) logger.Errorw("Template not found", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := h.Templater.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
logger.Errorw("Error parsing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
genConfig := &models.ContractTemplateOptions{ genConfig := &models.ContractTemplateOptions{
Runners: contract.Runners, Runners: repeatRunnerArrayItems(contract.Runners, 2),
CurrencySymbol: h.Config.CurrencySymbol, CurrencySymbol: h.Config.CurrencySymbol,
Disclaimer: h.Config.SponosringDisclaimer, Disclaimer: h.Config.SponosringDisclaimer,
ReceiptMinimumAmount: h.Config.SponsoringReceiptMinimum, ReceiptMinimumAmount: h.Config.SponsoringReceiptMinimum,
@@ -57,19 +62,23 @@ func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
BarcodePrefix: h.Config.SponsoringBarcodePrefix, BarcodePrefix: h.Config.SponsoringBarcodePrefix,
} }
logger.Info("Generating contract html")
result, err := h.Templater.Execute(template, genConfig) result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Generated contract html")
logger.Info("Converting html to pdf")
pdf, err := h.Converter.ToPdf(result, "a5", true) pdf, err := h.Converter.ToPdf(result, "a5", true)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Converted html to pdf")
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf") c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")

View File

@@ -4,6 +4,7 @@ import (
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services" "git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"go.uber.org/zap"
) )
type Handler interface { type Handler interface {
@@ -19,4 +20,5 @@ type DefaultHandler struct {
Templater services.Templater Templater services.Templater
Converter services.Converter Converter services.Converter
StaticService services.StaticService StaticService services.StaticService
Logger *zap.SugaredLogger
} }

83
main.go
View File

@@ -3,21 +3,27 @@ package main
import ( import (
"crypto/sha256" "crypto/sha256"
"crypto/subtle" "crypto/subtle"
"log" "os"
"strings"
"git.odit.services/lfk/document-server/docs" // Correct import path for docs "git.odit.services/lfk/document-server/docs" // Correct import path for docs
"git.odit.services/lfk/document-server/handlers" "git.odit.services/lfk/document-server/handlers"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services" "git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/keyauth" "github.com/gofiber/fiber/v2/middleware/keyauth"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/gofiber/swagger" "github.com/gofiber/swagger"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
) )
var ( var (
config *models.Config config *models.Config
logger *zap.SugaredLogger
) )
func validateAPIKey(c *fiber.Ctx, key string) (bool, error) { func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
@@ -32,12 +38,22 @@ func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
func loadEnv() error { func loadEnv() error {
viper.SetDefault("LOGLEVEL", "INFO")
viper.SetDefault("PRODUCION", false)
viper.SetDefault("PORT", "3000") viper.SetDefault("PORT", "3000")
viper.SetDefault("APIKEY", "lfk")
viper.SetDefault("EVENTNAME", "Demo Event")
viper.SetDefault("CURRENCYSYMBOL", "€")
viper.SetDefault("CARD_SUBTITLE", "Runner Card")
viper.SetDefault("CARD_BARCODEFORMAT", "ean13") viper.SetDefault("CARD_BARCODEFORMAT", "ean13")
viper.SetDefault("CARD_BARCODEPREFIX", "") viper.SetDefault("CARD_BARCODEPREFIX", "")
viper.SetDefault("SPONSORING_RECEIPTMINIMUM", 0)
viper.SetDefault("SPONSORING_DISCLAIMER", "Disclaimer")
viper.SetDefault("SPONSORING_BARCODEFORMAT", "code128") viper.SetDefault("SPONSORING_BARCODEFORMAT", "code128")
viper.SetDefault("SPONSORING_BARCODEPREFIX", "") viper.SetDefault("SPONSORING_BARCODEPREFIX", "")
viper.SetDefault("APIKEY", "lfk") viper.SetDefault("CERTIFICATE_FOOTER", "Footer")
viper.SetDefault("GOTENBERG_BASEURL", "")
viper.SetDefault("REDIS_ADDR", "")
// Load .env file // Load .env file
viper.SetConfigFile(".env") viper.SetConfigFile(".env")
@@ -46,7 +62,7 @@ func loadEnv() error {
viper.AutomaticEnv() viper.AutomaticEnv()
err := viper.ReadInConfig() err := viper.ReadInConfig()
if err != nil { if err != nil {
log.Println("No .env file found") logger.Warn("No .env file found")
} }
// Unmarshal the config from file and env into the config struct // Unmarshal the config from file and env into the config struct
@@ -55,29 +71,65 @@ func loadEnv() error {
return err return err
} }
logger.Infow("Loaded config", "config", &config)
return nil
}
func initLogger() error {
logLevel := os.Getenv("LOGLEVEL")
if logLevel == "" {
logLevel = "INFO"
}
var zapLogLevel zapcore.Level
err := zapLogLevel.UnmarshalText([]byte(strings.ToLower(logLevel)))
if err != nil {
zapLogLevel = zapcore.InfoLevel
}
zapConfig := zap.NewProductionConfig()
zapConfig.Level = zap.NewAtomicLevelAt(zapLogLevel)
zapLogger, err := zapConfig.Build()
if err != nil {
return err
}
defer zapLogger.Sync()
logger = zapLogger.Sugar()
logger.Debug("Initialized logger")
return nil return nil
} }
// @title LfK Document Server API // @title LfK Document Server API
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation. // @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
// @license.name CC BY-NC-SA 4.0 // @license.name CC BY-NC-SA 4.0
// @termsOfService https://lauf-fuer-kaya.de/datenschutz // @termsOfService https://lauf-fuer-kaya.de/datenschutz
// @contact.name ODIT.Services UG (haftungsbeschränkt) // @contact.name ODIT.Services UG (haftungsbeschränkt)
// @contact.url https://odit.services // @contact.url https://odit.services
// @contact.email info@odit.services // @contact.email info@odit.services
// @securityDefinitions.apiKey ApiKeyAuth // @securityDefinitions.apiKey ApiKeyAuth
// @in query // @in query
// @name key // @name key
func main() { func main() {
err := loadEnv() // Init the logger
err := initLogger()
if err != nil { if err != nil {
log.Fatal(err) return
}
err = loadEnv()
if err != nil {
logger.Error(err)
return
} }
var redisClient *redis.Client var redisClient *redis.Client
if config.RedisAddr != "" { if config.RedisAddr != "" {
log.Println("Using redis at", config.RedisAddr) logger.Infow("Using redis", "redisAddr", config.RedisAddr)
redisClient = redis.NewClient(&redis.Options{ redisClient = redis.NewClient(&redis.Options{
Addr: config.RedisAddr, Addr: config.RedisAddr,
}) })
@@ -85,9 +137,11 @@ func main() {
barcodeGenerator := &services.DefaultBarcodeService{ barcodeGenerator := &services.DefaultBarcodeService{
RedisClient: redisClient, RedisClient: redisClient,
Logger: logger.Named("DefaultBarcodeService"),
} }
staticService := &services.DefaultStaticService{ staticService := &services.DefaultStaticService{
Cache: make(map[string]string), Cache: make(map[string]string),
Logger: logger.Named("DefaultStaticService"),
} }
handler := handlers.DefaultHandler{ handler := handlers.DefaultHandler{
Config: config, Config: config,
@@ -96,17 +150,24 @@ func main() {
Templater: &services.DefaultTemplater{ Templater: &services.DefaultTemplater{
BarcodeService: barcodeGenerator, BarcodeService: barcodeGenerator,
StaticService: staticService, StaticService: staticService,
Logger: logger.Named("DefaultTemplater"),
}, },
Converter: &services.GotenbergConverter{ Converter: &services.GotenbergConverter{
BaseUrl: config.GotenbergBaseUrl, BaseUrl: config.GotenbergBaseUrl,
Logger: logger.Named("GotenbergConverter"),
}, },
Logger: logger.Named("DefaultHandler"),
} }
logger.Debug("Initialized services")
// Create a new Fiber instance // Create a new Fiber instance
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Prefork: config.Prod, Prefork: config.Prod,
}) })
app.Use(cors.New())
app.Use(requestid.New())
// Swagger documentation route // Swagger documentation route
app.Get("/swagger/*", swagger.HandlerDefault) app.Get("/swagger/*", swagger.HandlerDefault)
@@ -123,9 +184,11 @@ func main() {
pdfv1.Post("/certificates", handler.GenerateCertificate) pdfv1.Post("/certificates", handler.GenerateCertificate)
v1.Get("/barcodes/:type/:content", handler.GenerateBarcode) v1.Get("/barcodes/:type/:content", handler.GenerateBarcode)
logger.Debug("Initialized routes")
app.Use(handler.NotFoundHandler) app.Use(handler.NotFoundHandler)
docs.SwaggerInfo.BasePath = "/" docs.SwaggerInfo.BasePath = "/"
log.Fatal(app.Listen("0.0.0.0:" + config.Port)) logger.Infow("Starting server", "port", config.Port)
logger.Error(app.Listen("0.0.0.0:" + config.Port))
} }

View File

@@ -1,6 +1,7 @@
package models package models
type Config struct { type Config struct {
LogLevel string `mapstructure:"LOGLEVEL"`
Prod bool `mapstructure:"PRODUCION"` Prod bool `mapstructure:"PRODUCION"`
Port string `mapstructure:"PORT"` Port string `mapstructure:"PORT"`
APIKey string `mapstructure:"APIKEY"` APIKey string `mapstructure:"APIKEY"`

View File

@@ -14,7 +14,10 @@ type Runner struct {
} }
type Group struct { type Group struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
ParentGroup struct {
Name string `json:"name" validate:"required"`
} `json:"parent_group" validate:"optional"`
} }
type ContractTemplateOptions struct { type ContractTemplateOptions struct {

View File

@@ -8,7 +8,6 @@ import (
"image/color" "image/color"
"image/draw" "image/draw"
"image/png" "image/png"
"log"
"slices" "slices"
"time" "time"
@@ -17,6 +16,7 @@ import (
"github.com/boombuler/barcode/ean" "github.com/boombuler/barcode/ean"
"github.com/boombuler/barcode/qr" "github.com/boombuler/barcode/qr"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"go.uber.org/zap"
) )
type BarcodeService interface { type BarcodeService interface {
@@ -26,19 +26,26 @@ type BarcodeService interface {
type DefaultBarcodeService struct { type DefaultBarcodeService struct {
RedisClient *redis.Client RedisClient *redis.Client
Logger *zap.SugaredLogger
} }
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) { func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) {
ctx := context.Background() ctx := context.Background()
logger := b.Logger.Named("GenerateBarcode")
if !b.IsTypeSupported(format) { if !b.IsTypeSupported(format) {
logger.Errorw("Unsupported barcode type", "type", format)
return bytes.Buffer{}, fmt.Errorf("unsupported barcode type: %s", format) return bytes.Buffer{}, fmt.Errorf("unsupported barcode type: %s", format)
} }
logger = logger.With("type", format, "content", content, "width", width, "height", height, "padding", padding)
cacheKey := fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)
if b.RedisClient != nil { if b.RedisClient != nil {
cachedBarcode, err := b.RedisClient.Get(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)).Result() logger.Debugw("Checking cache for barcode", "key", cacheKey)
cachedBarcode, err := b.RedisClient.Get(ctx, cacheKey).Result()
if err == nil { if err == nil {
log.Printf("Cache hit for barcode:%s:%s:%d:%d", format, content, width, height) logger.Infow("Barcode found in cache", "key", cacheKey)
buf := bytes.Buffer{} buf := bytes.Buffer{}
buf.Write([]byte(cachedBarcode)) buf.Write([]byte(cachedBarcode))
return buf, nil return buf, nil
@@ -73,6 +80,7 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
bg := image.NewRGBA(image.Rect(0, 0, width, height)) bg := image.NewRGBA(image.Rect(0, 0, width, height))
white := color.RGBA{255, 255, 255, 255} white := color.RGBA{255, 255, 255, 255}
draw.Draw(bg, bg.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src) draw.Draw(bg, bg.Bounds(), &image.Uniform{white}, image.Point{}, draw.Src)
logger.Debug("Created white background")
// Calculate the new size for the barcode to fit within the padding // Calculate the new size for the barcode to fit within the padding
newWidth := width - 2*padding newWidth := width - 2*padding
@@ -81,24 +89,32 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
// Scale the barcode to the new size // Scale the barcode to the new size
scaledCode, err := barcode.Scale(generatedCode, newWidth, newHeight) scaledCode, err := barcode.Scale(generatedCode, newWidth, newHeight)
if err != nil { if err != nil {
logger.Errorw("Failed to scale barcode", "error", err)
return bytes.Buffer{}, err return bytes.Buffer{}, err
} }
logger.Debug("Scaled barcode")
// Draw the barcode on top of the white background with padding // Draw the barcode on top of the white background with padding
draw.Draw(bg, scaledCode.Bounds().Add(image.Point{padding, padding}), scaledCode, image.Point{}, draw.Over) draw.Draw(bg, scaledCode.Bounds().Add(image.Point{padding, padding}), scaledCode, image.Point{}, draw.Over)
logger.Debug("Drew barcode on background")
var buf bytes.Buffer var buf bytes.Buffer
err = png.Encode(&buf, bg) err = png.Encode(&buf, bg)
if err != nil { if err != nil {
logger.Errorw("Failed to encode barcode to PNG", "error", err)
return bytes.Buffer{}, err return bytes.Buffer{}, err
} }
logger.Debug("Encoded barcode to PNG")
if b.RedisClient != nil { if b.RedisClient != nil {
err = b.RedisClient.Set(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d", format, content, width, height), buf.String(), 10*time.Minute).Err() err = b.RedisClient.Set(ctx, cacheKey, buf.String(), 10*time.Minute).Err()
logger.Debugw("Cached barcode", "key", cacheKey)
if err != nil { if err != nil {
logger.Errorw("Failed to cache barcode", "error", err)
return bytes.Buffer{}, err return bytes.Buffer{}, err
} }
} }
logger.Info("Generated barcode")
return buf, nil return buf, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"github.com/oxplot/papersizes" "github.com/oxplot/papersizes"
"go.uber.org/zap"
) )
type Converter interface { type Converter interface {
@@ -16,92 +17,115 @@ type Converter interface {
type GotenbergConverter struct { type GotenbergConverter struct {
BaseUrl string BaseUrl string
Logger *zap.SugaredLogger
} }
func (g *GotenbergConverter) ToPdf(html string, pageSize string, landscape bool) ([]byte, error) { func (g *GotenbergConverter) ToPdf(html string, pageSize string, landscape bool) ([]byte, error) {
logger := g.Logger.Named("ToPdf").With("page_size", pageSize, "landscape", landscape, "base_url", g.BaseUrl)
client := &http.Client{} client := &http.Client{}
defer client.CloseIdleConnections() defer client.CloseIdleConnections()
logger.Debug("Created HTTP client")
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("files", "index.html") part, err := writer.CreateFormFile("files", "index.html")
if err != nil { if err != nil {
logger.Errorw("Failed to create form file", "error", err)
return nil, err return nil, err
} }
_, err = part.Write([]byte(html)) _, err = part.Write([]byte(html))
if err != nil { if err != nil {
logger.Errorw("Failed to write to form file", "error", err)
return nil, err return nil, err
} }
size := papersizes.FromName(pageSize) size := papersizes.FromName(pageSize)
if size == nil { if size == nil {
logger.Errorw("Invalid page size", "size", pageSize)
return nil, fmt.Errorf("invalid page size: %s", pageSize) return nil, fmt.Errorf("invalid page size: %s", pageSize)
} }
err = writer.WriteField("paperWidth", strconv.Itoa(size.Width)+"mm") err = writer.WriteField("paperWidth", strconv.Itoa(size.Width)+"mm")
if err != nil { if err != nil {
logger.Errorw("Failed to write paper width", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("paperHeight", strconv.Itoa(size.Height)+"mm") err = writer.WriteField("paperHeight", strconv.Itoa(size.Height)+"mm")
if err != nil { if err != nil {
logger.Errorw("Failed to write paper height", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("landscape", strconv.FormatBool(landscape)) err = writer.WriteField("landscape", strconv.FormatBool(landscape))
if err != nil { if err != nil {
logger.Errorw("Failed to write landscape", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("marginTop", "0") err = writer.WriteField("marginTop", "0")
if err != nil { if err != nil {
logger.Errorw("Failed to write margin top", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("marginBottom", "0") err = writer.WriteField("marginBottom", "0")
if err != nil { if err != nil {
logger.Errorw("Failed to write margin bottom", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("marginLeft", "0") err = writer.WriteField("marginLeft", "0")
if err != nil { if err != nil {
logger.Errorw("Failed to write margin left", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("marginRight", "0") err = writer.WriteField("marginRight", "0")
if err != nil { if err != nil {
logger.Errorw("Failed to write margin right", "error", err)
return nil, err return nil, err
} }
err = writer.WriteField("preferCssPageSize", "true") err = writer.WriteField("preferCssPageSize", "true")
if err != nil { if err != nil {
logger.Errorw("Failed to write prefer css page size", "error", err)
return nil, err return nil, err
} }
err = writer.Close() err = writer.Close()
if err != nil { if err != nil {
logger.Errorw("Failed to close writer", "error", err)
return nil, err return nil, err
} }
logger.Debug("Created form data")
logger.Debug("Creating HTTP request")
req, err := http.NewRequest("POST", g.BaseUrl+"/forms/chromium/convert/html", body) req, err := http.NewRequest("POST", g.BaseUrl+"/forms/chromium/convert/html", body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Content-Type", writer.FormDataContentType())
logger.Debug("Sending HTTP request")
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
logger.Errorw("Failed to send request", "error", err)
return nil, err return nil, err
} }
logger.Debug("Received HTTP response")
defer resp.Body.Close() defer resp.Body.Close()
data := new(bytes.Buffer) data := new(bytes.Buffer)
_, err = data.ReadFrom(resp.Body) _, err = data.ReadFrom(resp.Body)
if err != nil { if err != nil {
logger.Errorw("Failed to read response body", "error", err)
return nil, err return nil, err
} }
logger.Debug("Returning PDF data")
return data.Bytes(), nil return data.Bytes(), nil
} }

View File

@@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"strings" "strings"
"go.uber.org/zap"
) )
type Templater interface { type Templater interface {
@@ -17,6 +19,7 @@ type Templater interface {
type DefaultTemplater struct { type DefaultTemplater struct {
BarcodeService BarcodeService BarcodeService BarcodeService
StaticService StaticService StaticService StaticService
Logger *zap.SugaredLogger
} }
func idToEan13(id string, prefix string) (string, error) { func idToEan13(id string, prefix string) (string, error) {
@@ -48,10 +51,13 @@ func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix st
} }
func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) { func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
logger := t.Logger.Named("SelectSponsorImage")
sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors") sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
if err != nil { if err != nil {
logger.Errorw("Failed to list sponsors", "error", err)
return "", err return "", err
} }
logger.Debugw("Selected sponsor", "sponsors", sponsors, "id", id, "selected", sponsors[id%len(sponsors)])
return t.StaticService.GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil return t.StaticService.GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil
} }

View File

@@ -3,8 +3,9 @@ package services
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"log"
"os" "os"
"go.uber.org/zap"
) )
type StaticService interface { type StaticService interface {
@@ -14,47 +15,55 @@ type StaticService interface {
} }
type DefaultStaticService struct { type DefaultStaticService struct {
Cache map[string]string Cache map[string]string
Logger *zap.SugaredLogger
} }
func (s *DefaultStaticService) GetTemplate(locale, templateName string) (string, error) { func (s *DefaultStaticService) GetTemplate(locale, templateName string) (string, error) {
logger := s.Logger.Named("GetTemplate").With("locale", locale, "template_name", templateName)
if s.Cache[locale+templateName] != "" { if s.Cache[locale+templateName] != "" {
log.Printf("returning cached template %s with locale %s", templateName, locale) logger.Debugw("Template found in cache", "key", locale+templateName)
return s.Cache[locale+templateName], nil return s.Cache[locale+templateName], nil
} }
content, err := os.ReadFile(fmt.Sprintf("static/templates/%s/%s.html", templateName, locale)) content, err := os.ReadFile(fmt.Sprintf("static/templates/%s/%s.html", templateName, locale))
if content == nil || err != nil { if content == nil || err != nil {
log.Printf("error reading template %s with locale %s: %v", templateName, locale, err) logger.Errorw("Failed to read template", "error", err)
return "", err return "", err
} }
s.Cache[locale+templateName] = string(content) s.Cache[locale+templateName] = string(content)
logger.Debugw("Saved template to cache", "key", locale+templateName)
return string(content), nil return string(content), nil
} }
func (s *DefaultStaticService) ListFilesInStaticSubFolder(folderName string) ([]string, error) { func (s *DefaultStaticService) ListFilesInStaticSubFolder(folderName string) ([]string, error) {
logger := s.Logger.Named("ListFilesInStaticSubFolder").With("folder_name", folderName)
files, err := os.ReadDir(fmt.Sprintf("static/%s", folderName)) files, err := os.ReadDir(fmt.Sprintf("static/%s", folderName))
if err != nil { if err != nil {
log.Printf("error reading files from folder %s: %v", folderName, err) logger.Errorw("Failed to list files", "error", err)
return nil, err return nil, err
} }
var images []string var images []string
for _, file := range files { for _, file := range files {
if file.IsDir() { if file.IsDir() {
continue logger.Debugw("Skipping directory", "file", file.Name())
} }
images = append(images, file.Name()) images = append(images, file.Name())
} }
logger.Debugw("Listed files", "files", images)
return images, nil return images, nil
} }
func (s *DefaultStaticService) GetImage(imageName string) string { func (s *DefaultStaticService) GetImage(imageName string) string {
logger := s.Logger.Named("GetImage").With("image_name", imageName)
content, err := os.ReadFile("static/images/" + imageName + ".base64") content, err := os.ReadFile("static/images/" + imageName + ".base64")
if content == nil || err != nil { if content == nil || err != nil {
log.Printf("error reading image %s: %v", imageName, err) logger.Errorw("Failed to read image", "error", err)
return ImageErrorBase64 return ImageErrorBase64
} }
return string(content) return string(content)

View File

@@ -50,8 +50,12 @@
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p> <p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div> </div>
</div> </div>
{{ if ne .Runner.FirstName "" }}
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p> <p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
<p>{{ .Runner.Group.Name }}</p> <p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
{{ else }}
<p>Kein Läufer zugewiesen</p>
{{ end}}
{{ end}} {{ end}}
</div> </div>
{{ end }} {{ end }}

View File

@@ -49,8 +49,12 @@
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p> <p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div> </div>
</div> </div>
{{ if ne .Runner.FirstName ""}}
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p> <p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
<p>{{ .Runner.Group.Name }}</p> <p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
{{ else }}
<p>Blank card</p>
{{ end}}
</div> </div>
{{ end }} {{ end }}
</div> </div>

View File

@@ -65,8 +65,8 @@
<p style="font-size: x-small; display: block;">Nachname</p> <p style="font-size: x-small; display: block;">Nachname</p>
</div> </div>
<div class="column is-6"> <div class="column is-6">
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name }}</span> <span style="border-bottom: 1px solid; width: 100%; display: block;"><p>{{ if ne .Group.ParentGroup.Name "" -}}{{ .Group.ParentGroup.Name }}/ {{end -}}{{ .Group.Name }}</p></span>
<p style="font-size: x-small; display: block;">Team/ Klasse</p> <p style="font-size: x-small; display: block;">Team/Klasse</p>
</div> </div>
</div> </div>
<p style="margin-top: -0.5rem">mit einem Betrag von _____ {{ $.CurrencySymbol }} pro gelaufenem Kilometer zu <p style="margin-top: -0.5rem">mit einem Betrag von _____ {{ $.CurrencySymbol }} pro gelaufenem Kilometer zu

View File

@@ -64,7 +64,7 @@
<p style="font-size: x-small; display: block;">Last Name</p> <p style="font-size: x-small; display: block;">Last Name</p>
</div> </div>
<div class="column is-6"> <div class="column is-6">
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name}}</span> <span style="border-bottom: 1px solid; width: 100%; display: block;"><p>{{ if ne .Group.ParentGroup.Name "" -}}{{ .Group.ParentGroup.Name }}/ {{end -}}{{ .Group.Name }}</p></span>
<p style="font-size: x-small; display: block;">Team/class</p> <p style="font-size: x-small; display: block;">Team/class</p>
</div> </div>
</div> </div>