Compare commits
38 Commits
1.2.0
...
bc17f7256b
| Author | SHA1 | Date | |
|---|---|---|---|
|
bc17f7256b
|
|||
|
d2f3eea8a5
|
|||
|
f902c61490
|
|||
|
11e8cc5b1d
|
|||
|
84155b7404
|
|||
|
45b37197ec
|
|||
|
f65848924c
|
|||
|
98d584867e
|
|||
|
376e8de1a4
|
|||
|
2911391fb9
|
|||
|
6d2e0241c9
|
|||
|
afc5b1f0c6
|
|||
|
4a76ee469b
|
|||
|
b58bf700df
|
|||
|
efd3a35802
|
|||
|
0f7e44a42a
|
|||
|
f90e5d75fa
|
|||
|
31d4ec5f27
|
|||
|
d61d4d6e7e
|
|||
|
606ce6b940
|
|||
|
750fa70332
|
|||
|
7d503edbc9
|
|||
|
5c9235df8d
|
|||
|
11ea0858bb
|
|||
|
4d57cf827d
|
|||
|
df9f7fdc13
|
|||
|
cdd2b5e250
|
|||
|
94b766f106
|
|||
|
a2e94f715b
|
|||
|
f64daaf817
|
|||
|
b4bb732303
|
|||
|
3dee3e72af
|
|||
|
f5914e5c38
|
|||
|
5a5a7179e9
|
|||
|
6c57d63891
|
|||
|
b502e2fbd5
|
|||
|
c9475d0093
|
|||
|
1cc19e0085
|
@@ -1,3 +1,4 @@
|
|||||||
|
LOGLEVEL=debug
|
||||||
PORT=3000
|
PORT=3000
|
||||||
PRODUCION=false
|
PRODUCION=false
|
||||||
APIKEY=lfk
|
APIKEY=lfk
|
||||||
@@ -10,9 +11,13 @@ CARD_SUBTITLE=Kaya ist cool
|
|||||||
CARD_BARCODEFORMAT=ean13
|
CARD_BARCODEFORMAT=ean13
|
||||||
# CARD_BARCODEPREFIX=
|
# CARD_BARCODEPREFIX=
|
||||||
|
|
||||||
SPONSOING_RECEIPTMINIMUM=10
|
SPONSORING_RECEIPTMINIMUM=40
|
||||||
SPONSORING_DISCLAIMER=Kaya ist cool, aber pass auf, dass du nicht zu viel Geld sammelst!
|
SPONSORING_DISCLAIMER=Kaya ist cool, aber pass auf, dass du nicht zu viel Geld sammelst!
|
||||||
SPONSORING_BARCODEFORMAT=code128
|
SPONSORING_BARCODEFORMAT=code128
|
||||||
# SPONSORING_BARCODEPREFIX=
|
# SPONSORING_BARCODEPREFIX=
|
||||||
|
|
||||||
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
|
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
|
||||||
|
|
||||||
|
SEPA_BIC=FNOMDEB2
|
||||||
|
SEPA_NAME=ODIT.Services
|
||||||
|
SEPA_IBAN=DE25100180000690238989
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -326,6 +326,17 @@ const docTemplate = `{
|
|||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parent_group": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -317,6 +317,17 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"parent_group": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
+15
-2
@@ -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")
|
||||||
|
|||||||
+26
-2
@@ -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(),
|
||||||
})
|
})
|
||||||
@@ -51,22 +58,32 @@ func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
|
|||||||
Footer: h.Config.CertificateFooter,
|
Footer: h.Config.CertificateFooter,
|
||||||
CurrencySymbol: h.Config.CurrencySymbol,
|
CurrencySymbol: h.Config.CurrencySymbol,
|
||||||
Locale: certificateRequest.Locale,
|
Locale: certificateRequest.Locale,
|
||||||
|
SepaConfig: &models.SepaConfig{
|
||||||
|
BIC: h.Config.SepaBic,
|
||||||
|
HolderName: h.Config.SepaName,
|
||||||
|
IBAN: h.Config.SepaIban,
|
||||||
|
CurrencyIdentifier: h.Config.CurrencyIdentifier,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
@@ -79,6 +96,13 @@ func addUpRunnerDonations(runners []models.RunnerWithDonations) []models.RunnerW
|
|||||||
runners[i].TotalDonations += runners[i].DistanceDonations[j].Amount
|
runners[i].TotalDonations += runners[i].DistanceDonations[j].Amount
|
||||||
runners[i].TotalPerDistance += runners[i].DistanceDonations[j].AmountPerDistance
|
runners[i].TotalPerDistance += runners[i].DistanceDonations[j].AmountPerDistance
|
||||||
}
|
}
|
||||||
|
if runners[i].Group.ParentGroup.Name != "" {
|
||||||
|
runners[i].CombinedGroupName = runners[i].Group.ParentGroup.Name + " - " + runners[i].Group.Name
|
||||||
|
} else {
|
||||||
|
runners[i].CombinedGroupName = runners[i].Group.Name
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return runners
|
return runners
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-4
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +38,26 @@ 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("CURRENCYIDENTIFIER", "EUR")
|
||||||
|
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("CERTIFICATE_FOOTER", "Footer")
|
||||||
|
viper.SetDefault("GOTENBERG_BASEURL", "")
|
||||||
|
viper.SetDefault("REDIS_ADDR", "")
|
||||||
|
viper.SetDefault("SEPA_BIC", "")
|
||||||
|
viper.SetDefault("SEPA_NAME", "")
|
||||||
|
viper.SetDefault("SEPA_IBAN", "")
|
||||||
|
|
||||||
// Load .env file
|
// Load .env file
|
||||||
viper.SetConfigFile(".env")
|
viper.SetConfigFile(".env")
|
||||||
@@ -45,7 +66,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
|
||||||
@@ -54,29 +75,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,
|
||||||
})
|
})
|
||||||
@@ -84,9 +141,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,
|
||||||
@@ -95,17 +154,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)
|
||||||
|
|
||||||
@@ -122,9 +188,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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ type RunnerWithDonations struct {
|
|||||||
MiddleName string `json:"middle_name" validate:"optional"`
|
MiddleName string `json:"middle_name" validate:"optional"`
|
||||||
LastName string `json:"last_name" validate:"required"`
|
LastName string `json:"last_name" validate:"required"`
|
||||||
Group Group `json:"group" validate:"required"`
|
Group Group `json:"group" validate:"required"`
|
||||||
|
CombinedGroupName string `json:"combined_group_name" validate:"optional"`
|
||||||
Distance int `json:"distance" validate:"required"`
|
Distance int `json:"distance" validate:"required"`
|
||||||
DistanceDonations []DistanceDonation `json:"distance_donations" validate:"optional"`
|
DistanceDonations []DistanceDonation `json:"distance_donations" validate:"optional"`
|
||||||
TotalPerDistance int `json:"total_per_distance" validate:"optional"`
|
TotalPerDistance int `json:"total_per_distance" validate:"optional"`
|
||||||
TotalDonations int `json:"total_donations" validate:"optional"`
|
TotalDonations int `json:"total_donations" validate:"optional"`
|
||||||
|
SelfServiceLink string `json:"self_service_link" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DistanceDonation struct {
|
type DistanceDonation struct {
|
||||||
@@ -38,4 +40,12 @@ type CertificateTemplateOptions struct {
|
|||||||
Footer string `json:"footer"`
|
Footer string `json:"footer"`
|
||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
|
SepaConfig *SepaConfig `json:"sepa_config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SepaConfig struct {
|
||||||
|
IBAN string `json:"iban" validate:"required"`
|
||||||
|
HolderName string `json:"holder_name" validate:"required"`
|
||||||
|
BIC string `json:"bic" validate:"required"`
|
||||||
|
CurrencyIdentifier string `json:"currency_identifier" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,19 +1,24 @@
|
|||||||
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"`
|
||||||
EventName string `mapstructure:"EVENTNAME"`
|
EventName string `mapstructure:"EVENTNAME"`
|
||||||
CurrencySymbol string `mapstructure:"CURRENCYSYMBOL"`
|
CurrencySymbol string `mapstructure:"CURRENCYSYMBOL"`
|
||||||
|
CurrencyIdentifier string `mapstructure:"CURRENCYIDENTIFIER"`
|
||||||
CardSubtitle string `mapstructure:"CARD_SUBTITLE"`
|
CardSubtitle string `mapstructure:"CARD_SUBTITLE"`
|
||||||
CardBarcodeFormat string `mapstructure:"CARD_BARCODEFORMAT"`
|
CardBarcodeFormat string `mapstructure:"CARD_BARCODEFORMAT"`
|
||||||
CardBarcodePrefix string `mapstructure:"CARD_BARCODEPREFIX"`
|
CardBarcodePrefix string `mapstructure:"CARD_BARCODEPREFIX"`
|
||||||
SponsoringReceiptMinimum int `mapstructure:"SPONSOING_RECEIPTMINIMUM"`
|
SponsoringReceiptMinimum string `mapstructure:"SPONSORING_RECEIPTMINIMUM"`
|
||||||
SponosringDisclaimer string `mapstructure:"SPONSORING_DISCLAIMER"`
|
SponosringDisclaimer string `mapstructure:"SPONSORING_DISCLAIMER"`
|
||||||
SponsoringBarcodeFormat string `mapstructure:"SPONSORING_BARCODEFORMAT"`
|
SponsoringBarcodeFormat string `mapstructure:"SPONSORING_BARCODEFORMAT"`
|
||||||
SponsoringBarcodePrefix string `mapstructure:"SPONSORING_BARCODEPREFIX"`
|
SponsoringBarcodePrefix string `mapstructure:"SPONSORING_BARCODEPREFIX"`
|
||||||
CertificateFooter string `mapstructure:"CERTIFICATE_FOOTER"`
|
CertificateFooter string `mapstructure:"CERTIFICATE_FOOTER"`
|
||||||
GotenbergBaseUrl string `mapstructure:"GOTENBERG_BASEURL"`
|
GotenbergBaseUrl string `mapstructure:"GOTENBERG_BASEURL"`
|
||||||
RedisAddr string `mapstructure:"REDIS_ADDR"`
|
RedisAddr string `mapstructure:"REDIS_ADDR"`
|
||||||
|
SepaBic string `mapstructure:"SEPA_BIC"`
|
||||||
|
SepaName string `mapstructure:"SEPA_NAME"`
|
||||||
|
SepaIban string `mapstructure:"SEPA_IBAN"`
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -14,14 +14,17 @@ 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 {
|
||||||
Runners []Runner `json:"runners"`
|
Runners []Runner `json:"runners"`
|
||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
Disclaimer string `json:"disclaimer"`
|
Disclaimer string `json:"disclaimer"`
|
||||||
ReceiptMinimumAmount int `json:"receipt_minimum_amount"`
|
ReceiptMinimumAmount string `json:"receipt_minimum_amount"`
|
||||||
EventName string `json:"event_name"`
|
EventName string `json:"event_name"`
|
||||||
BarcodeFormat string `json:"barcode_format"`
|
BarcodeFormat string `json:"barcode_format"`
|
||||||
BarcodePrefix string `json:"barcode_prefix"`
|
BarcodePrefix string `json:"barcode_prefix"`
|
||||||
|
|||||||
+25
-5
@@ -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
|
||||||
@@ -62,7 +69,11 @@ func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, w
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
case "qr":
|
case "qr":
|
||||||
generatedCode, err = qr.Encode(content, qr.M, qr.AlphaNumeric)
|
// Always use qr.Auto encoding to support all characters in the content
|
||||||
|
encoding := qr.Auto
|
||||||
|
|
||||||
|
// QR code generation with error correction level M and auto encoding
|
||||||
|
generatedCode, err = qr.Encode(content, qr.M, encoding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return bytes.Buffer{}, err
|
return bytes.Buffer{}, err
|
||||||
}
|
}
|
||||||
@@ -73,6 +84,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 +93,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -32,6 +35,28 @@ func idToEan13(id string, prefix string) (string, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *DefaultTemplater) GenerateEPC(iban string, bic string, name string, title string, amount int, currency string) (string, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
code := fmt.Sprintf(`BCD
|
||||||
|
002
|
||||||
|
1
|
||||||
|
INST
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
%s%.2f
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
`, bic, name, iban, currency, float64(amount)/100, title,
|
||||||
|
)
|
||||||
|
|
||||||
|
buf, err := t.BarcodeService.GenerateBarcode("qr", code, 500, 500, 0)
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(buf.Bytes()), err
|
||||||
|
}
|
||||||
|
|
||||||
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
|
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -48,10 +73,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +110,7 @@ func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Te
|
|||||||
"sponsorLogo": t.SelectSponsorImage,
|
"sponsorLogo": t.SelectSponsorImage,
|
||||||
"formatUnit": t.FormatUnit,
|
"formatUnit": t.FormatUnit,
|
||||||
"loadImage": t.LoadImage,
|
"loadImage": t.LoadImage,
|
||||||
|
"epcCode": t.GenerateEPC,
|
||||||
}).Parse(templateString)
|
}).Parse(templateString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -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)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf8">
|
<meta charset="utf8">
|
||||||
<title>Sponsoring contract</title>
|
<title>Läuferkarten</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||||
<style>
|
<style>
|
||||||
.sheet {
|
.sheet {
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf8">
|
<meta charset="utf8">
|
||||||
<title>Sponsoring contract</title>
|
<title>Runner cards</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||||
<style>
|
<style>
|
||||||
.sheet {
|
.sheet {
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf8">
|
<meta charset="utf8">
|
||||||
<title>Sponsoring contract</title>
|
<title>Läuferurkunde</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||||
<style>
|
<style>
|
||||||
.sheet {
|
.sheet {
|
||||||
@@ -87,13 +87,30 @@
|
|||||||
<td>Gesamt</td>
|
<td>Gesamt</td>
|
||||||
<td>{{ formatUnit "euro" $.Locale .TotalPerDistance }} {{ $.CurrencySymbol }}</td>
|
<td>{{ formatUnit "euro" $.Locale .TotalPerDistance }} {{ $.CurrencySymbol }}</td>
|
||||||
<td>{{ formatUnit "euro" $.Locale .TotalDonations }} {{ $.CurrencySymbol }}</td>
|
<td>{{ formatUnit "euro" $.Locale .TotalDonations }} {{ $.CurrencySymbol }}</td>
|
||||||
</tfoot>
|
</table>
|
||||||
</table>
|
</main>
|
||||||
</main>
|
<footer class="certificate-footer">
|
||||||
<footer class="certificate-footer">
|
<table style="border-collapse: collapse; border: none; width: 17cm;">
|
||||||
<p>
|
<thead>
|
||||||
{{ $.Footer }}
|
<tr>
|
||||||
</p>
|
<th style="border: none; width: 50%; text-align: center;">Link zu deinen Rundenzeiten</th>
|
||||||
|
<th style="border: none; width: 50%; text-align: center;">Spende überweisen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; text-align: center;">
|
||||||
|
<img src="data:image/png;base64,{{ barcode .SelfServiceLink "qr" "" }}" style="height: 2.5cm; padding: 0.2cm">
|
||||||
|
</td>
|
||||||
|
<td style="border: none; text-align: center;">
|
||||||
|
<img src="data:image/png;base64,{{ epcCode $.SepaConfig.IBAN $.SepaConfig.BIC $.SepaConfig.HolderName (print "Sponsoring für " .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="width: 17cm; text-align: center;">
|
||||||
|
{{ $.Footer }}
|
||||||
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf8">
|
<meta charset="utf8">
|
||||||
<title>Sponsoring contract</title>
|
<title>Runner certificate</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||||
<style>
|
<style>
|
||||||
.sheet {
|
.sheet {
|
||||||
@@ -91,7 +91,25 @@
|
|||||||
</table>
|
</table>
|
||||||
</main>
|
</main>
|
||||||
<footer class="certificate-footer">
|
<footer class="certificate-footer">
|
||||||
<p>
|
<table style="border-collapse: collapse; border: none; width: 17cm;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="border: none; width: 50%; text-align: center;">Link to your lap times</th>
|
||||||
|
<th style="border: none; width: 50%; text-align: center;">Transfer donation via SEPA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border: none; text-align: center;">
|
||||||
|
<img src="data:image/png;base64,{{ barcode .SelfServiceLink "qr" "" }}" style="height: 2.5cm; padding: 0.2cm">
|
||||||
|
</td>
|
||||||
|
<td style="border: none; text-align: center;">
|
||||||
|
<img src="data:image/png;base64,{{ epcCode $.SepaConfig.IBAN $.SepaConfig.BIC $.SepaConfig.HolderName (print "Sponsoring for " .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="width: 17cm; text-align: center;">
|
||||||
{{ $.Footer }}
|
{{ $.Footer }}
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user