Compare commits
91 Commits
12f61e373f
...
1.5.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
af587b0ac1
|
|||
|
50e3eff294
|
|||
|
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
|
|||
|
b792806481
|
|||
|
ea6a4a7080
|
|||
|
de6fe4991c
|
|||
|
1d068b2655
|
|||
|
ef25adf5ed
|
|||
|
c09c00ec68
|
|||
|
1f4981b0a9
|
|||
|
c9f28612be
|
|||
|
c2d192d8a3
|
|||
|
31734596f6
|
|||
|
850fa0a760
|
|||
|
1c0a9860fa
|
|||
|
7c32f1aad2
|
|||
|
b9e550d6f5
|
|||
|
2a4f126377
|
|||
|
5c932158e9
|
|||
|
1296c9e399
|
|||
|
0f2452eca0
|
|||
|
b6cc98a165
|
|||
|
c5da33f10f
|
|||
|
69f4de6739
|
|||
|
649ac2a3c2
|
|||
|
5587fdaaa8
|
|||
|
8f676f08a9
|
|||
| 28de60d375 | |||
|
d19029b5ad
|
|||
|
1dfd96869d
|
|||
|
a1ba28cacb
|
|||
|
d2bace87af
|
|||
|
9d507b9572
|
|||
|
e89c17806f
|
|||
|
924f76a100
|
|||
|
99ec0933ea
|
|||
|
cceca7f5e1
|
|||
|
54d294a8b4
|
|||
|
41291b9200
|
|||
|
4faf76a073
|
|||
|
715eb8e1cb
|
|||
|
2686bee1d1
|
|||
|
57a3777891
|
|||
|
f6dc33edb4
|
|||
|
e2cd445aeb
|
|||
|
f5debf58fc
|
|||
|
11a9b51197
|
|||
|
c5dc4f7e79
|
|||
|
f9f30e96c7
|
|||
|
eff3354867
|
|||
|
2bfff006ed
|
|||
|
53eab5db94
|
|||
|
af73b35b18
|
|||
|
692f378eab
|
@@ -0,0 +1,46 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "tmp\\main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
tmp
|
||||
docker-compose.yaml
|
||||
.air.toml
|
||||
.air*.toml
|
||||
@@ -0,0 +1,23 @@
|
||||
LOGLEVEL=debug
|
||||
PORT=3000
|
||||
PRODUCION=false
|
||||
APIKEY=lfk
|
||||
EVENTNAME=Lauf für Kaya! 2025
|
||||
CURRENCYSYMBOL=€
|
||||
GOTENBERG_BASEURL=http://localhost:3001
|
||||
REDIS_ADDR=localhost:6379
|
||||
|
||||
CARD_SUBTITLE=Kaya ist cool
|
||||
CARD_BARCODEFORMAT=ean13
|
||||
# CARD_BARCODEPREFIX=
|
||||
|
||||
SPONSORING_RECEIPTMINIMUM=40
|
||||
SPONSORING_DISCLAIMER=Kaya ist cool, aber pass auf, dass du nicht zu viel Geld sammelst!
|
||||
SPONSORING_BARCODEFORMAT=code128
|
||||
# SPONSORING_BARCODEPREFIX=
|
||||
|
||||
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
|
||||
@@ -10,5 +10,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o server
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /app/server /server
|
||||
COPY static /static
|
||||
ADD https://curl.haxx.se/ca/cacert.pem /etc/ssl/certs/ca-certificates.crt
|
||||
ENTRYPOINT [ "/server" ]
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Features
|
||||
|
||||
- 📝 HTML pdf templates
|
||||
- 📝 HTML templates for pdf generation
|
||||
- 📚 OpenAPI/Swagger documentation
|
||||
- ⚡ High-performance with go and gotenberg
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
# Install dependencies
|
||||
go mod download
|
||||
|
||||
# Generate the swagger docs
|
||||
swag init
|
||||
|
||||
# Run the server
|
||||
air
|
||||
```
|
||||
@@ -29,3 +26,71 @@ The project uses:
|
||||
|
||||
- 🏃♂️ go as the language and build tool
|
||||
- 🌐 gofiber for the web framework
|
||||
- 📦 air for live reload
|
||||
- 📝 swaggo for API documentation
|
||||
- 📄 gotenberg for HTML to PDF conversion
|
||||
|
||||
### 📦 Use docker compose for external dependencies
|
||||
|
||||
```shell
|
||||
docker compose -f docker-compose.dev.yaml up
|
||||
```
|
||||
|
||||
### 🏃 Run via air
|
||||
|
||||
> Install air via `go install github.com/air-verse/air@latest`
|
||||
|
||||
```shell
|
||||
# With the default air config
|
||||
air
|
||||
|
||||
# With the config for linux/macOS
|
||||
air -c .air.linux.toml
|
||||
|
||||
# With the config for windows
|
||||
air -c .air.windows.toml
|
||||
```
|
||||
|
||||
### ✒️ Update the swagger docs
|
||||
|
||||
> Install swag via `go install github.com/swaggo/swag/cmd/swag@latest`
|
||||
|
||||
```shell
|
||||
swag init
|
||||
```
|
||||
|
||||
### 🐋 Build container
|
||||
|
||||
```shell
|
||||
# single arch
|
||||
docker build -t registry.odit.services/lfk/document-server:latest .
|
||||
|
||||
# multiarch
|
||||
docker buildx build --platform=linux/amd64,linux/arm64 -t registry.odit.services/lfk/document-server:latest --push .
|
||||
```
|
||||
|
||||
## ⏱️ Benchmarks
|
||||
|
||||
### Barcode Generation
|
||||
|
||||
- Requests: 10000
|
||||
- Concurrency: Unlimited
|
||||
- Data: `123456789123`
|
||||
- Width: 1000
|
||||
- Height: 500
|
||||
|
||||
#### No cache (cold start)
|
||||
|
||||
| Format | Data | Requests/sec | Slowest | Fastest | Average |
|
||||
| ------- | -------------- | ------------ | ------- | ------- | ------- |
|
||||
| Code128 | `123456789123` | 763.3996 | 0.7995 | 0.0172 | 0.0654 |
|
||||
| EAN13 | `123456789123` | 767.1170 | 0.7607 | 0.0171 | 0.0651 |
|
||||
| QR | `123456789123` | 683.8472 | 0.6528 | 0.0178 | 0.0730 |
|
||||
|
||||
#### Redis cache (warm start)
|
||||
|
||||
| Format | Data | Requests/sec | Slowest | Fastest | Average |
|
||||
| ------- | -------------- | ------------ | ------- | ------- | ------- |
|
||||
| Code128 | `123456789123` | 15611.5521 | 0.0965 | 0.0006 | 0.0032 |
|
||||
| EAN13 | `123456789123` | 14985.4401 | 0.0925 | 0.0006 | 0.0033 |
|
||||
| QR | `123456789123` | 14306.2540 | 0.1143 | 0.0005 | 0.0035 |
|
||||
@@ -0,0 +1,9 @@
|
||||
services:
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
ports:
|
||||
- "3001:3000"
|
||||
redis:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
ports:
|
||||
- "6379:6379"
|
||||
+11
-1
@@ -1,5 +1,15 @@
|
||||
services:
|
||||
document-server:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./docker.env:/.env
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:8
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "3001:3000"
|
||||
redis:
|
||||
image: docker.dragonflydb.io/dragonflydb/dragonfly
|
||||
ports:
|
||||
- "6379:6379"
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
PORT=3000
|
||||
PRODUCION=false
|
||||
APIKEY=lfk
|
||||
EVENTNAME=Lauf für Kaya! 2025
|
||||
CURRENCYSYMBOL=€
|
||||
GOTENBERG_BASEURL=http://gotenberg:3000
|
||||
REDIS_ADDR=redis:6379
|
||||
|
||||
CARD_SUBTITLE=Kaya ist cool
|
||||
CARD_BARCODEFORMAT=ean13
|
||||
# CARD_BARCODEPREFIX=
|
||||
|
||||
SPONSOING_RECEIPTMINIMUM=10
|
||||
SPONSORING_DISCLAIMER=Kaya ist cool, aber pass auf, dass du nicht zu viel Geld sammelst!
|
||||
SPONSORING_BARCODEFORMAT=code128
|
||||
# SPONSORING_BARCODEPREFIX=
|
||||
|
||||
CERTIFICATE_FOOTER=Kaya ist cool, danke für deine Unterstützung!
|
||||
+265
-8
@@ -9,14 +9,88 @@ const docTemplate = `{
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"contact": {},
|
||||
"termsOfService": "https://lauf-fuer-kaya.de/datenschutz",
|
||||
"contact": {
|
||||
"name": "ODIT.Services UG (haftungsbeschränkt)",
|
||||
"url": "https://odit.services",
|
||||
"email": "info@odit.services"
|
||||
},
|
||||
"license": {
|
||||
"name": "CC BY-NC-SA 4.0"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/cards": {
|
||||
"/v1/barcodes/{type}/{content}": {
|
||||
"get": {
|
||||
"description": "Generate barcodes based on the provided data",
|
||||
"produces": [
|
||||
"image/png"
|
||||
],
|
||||
"tags": [
|
||||
"barcodes"
|
||||
],
|
||||
"summary": "Generate barcodes",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"ean13",
|
||||
"code128"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Barcode type",
|
||||
"name": "type",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"description": "Barcode content",
|
||||
"name": "content",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"maximum": 10000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"description": "Barcode width",
|
||||
"name": "width",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 10000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"description": "Barcode height",
|
||||
"name": "height",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Padding around the barcode (included in image size)",
|
||||
"name": "padding",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/v1/pdfs/cards": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate cards based on the provided data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@@ -25,7 +99,7 @@ const docTemplate = `{
|
||||
"application/pdf"
|
||||
],
|
||||
"tags": [
|
||||
"cards"
|
||||
"pdfs"
|
||||
],
|
||||
"summary": "Generate runner cards",
|
||||
"parameters": [
|
||||
@@ -42,8 +116,45 @@ const docTemplate = `{
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/contracts": {
|
||||
"/v1/pdfs/certificates": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate certificates based on the provided data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/pdf"
|
||||
],
|
||||
"tags": [
|
||||
"pdfs"
|
||||
],
|
||||
"summary": "Generate runner certificates",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Certificate data",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.CertificateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/v1/pdfs/contracts": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate a contract based on the provided data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@@ -52,7 +163,7 @@ const docTemplate = `{
|
||||
"application/pdf"
|
||||
],
|
||||
"tags": [
|
||||
"contracts"
|
||||
"pdfs"
|
||||
],
|
||||
"summary": "Generate a contract",
|
||||
"parameters": [
|
||||
@@ -73,12 +184,18 @@ const docTemplate = `{
|
||||
"definitions": {
|
||||
"models.Card": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"id",
|
||||
"runner"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
@@ -90,6 +207,10 @@ const docTemplate = `{
|
||||
},
|
||||
"models.CardRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cards",
|
||||
"locale"
|
||||
],
|
||||
"properties": {
|
||||
"cards": {
|
||||
"type": "array",
|
||||
@@ -106,8 +227,34 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CertificateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"locale",
|
||||
"runners"
|
||||
],
|
||||
"properties": {
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"en",
|
||||
"de"
|
||||
]
|
||||
},
|
||||
"runners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.RunnerWithDonations"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ContractRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"locale",
|
||||
"runners"
|
||||
],
|
||||
"properties": {
|
||||
"locale": {
|
||||
"type": "string",
|
||||
@@ -124,22 +271,83 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Group": {
|
||||
"models.DistanceDonation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount_per_distance",
|
||||
"donor",
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amount_per_distance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"donor": {
|
||||
"$ref": "#/definitions/models.Donor"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"paid_amount": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Donor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"first_name",
|
||||
"id",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"middle_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Group": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_group": {
|
||||
"$ref": "#/definitions/models.Group"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Runner": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"first_name",
|
||||
"group",
|
||||
"id",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
@@ -157,6 +365,55 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RunnerWithDonations": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"distance",
|
||||
"first_name",
|
||||
"group",
|
||||
"id",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"distance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"distance_donations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.DistanceDonation"
|
||||
}
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"$ref": "#/definitions/models.Group"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"middle_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_donations": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_per_distance": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"name": "key",
|
||||
"in": "query"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
+265
-8
@@ -3,11 +3,85 @@
|
||||
"info": {
|
||||
"description": "This is the API documentation for the LfK Document Server - a tool for pdf generation.",
|
||||
"title": "LfK Document Server API",
|
||||
"contact": {}
|
||||
"termsOfService": "https://lauf-fuer-kaya.de/datenschutz",
|
||||
"contact": {
|
||||
"name": "ODIT.Services UG (haftungsbeschränkt)",
|
||||
"url": "https://odit.services",
|
||||
"email": "info@odit.services"
|
||||
},
|
||||
"license": {
|
||||
"name": "CC BY-NC-SA 4.0"
|
||||
}
|
||||
},
|
||||
"paths": {
|
||||
"/cards": {
|
||||
"/v1/barcodes/{type}/{content}": {
|
||||
"get": {
|
||||
"description": "Generate barcodes based on the provided data",
|
||||
"produces": [
|
||||
"image/png"
|
||||
],
|
||||
"tags": [
|
||||
"barcodes"
|
||||
],
|
||||
"summary": "Generate barcodes",
|
||||
"parameters": [
|
||||
{
|
||||
"enum": [
|
||||
"ean13",
|
||||
"code128"
|
||||
],
|
||||
"type": "string",
|
||||
"description": "Barcode type",
|
||||
"name": "type",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"minLength": 1,
|
||||
"type": "string",
|
||||
"description": "Barcode content",
|
||||
"name": "content",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"maximum": 10000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"description": "Barcode width",
|
||||
"name": "width",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 10000,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"description": "Barcode height",
|
||||
"name": "height",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"maximum": 100,
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Padding around the barcode (included in image size)",
|
||||
"name": "padding",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/v1/pdfs/cards": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate cards based on the provided data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@@ -16,7 +90,7 @@
|
||||
"application/pdf"
|
||||
],
|
||||
"tags": [
|
||||
"cards"
|
||||
"pdfs"
|
||||
],
|
||||
"summary": "Generate runner cards",
|
||||
"parameters": [
|
||||
@@ -33,8 +107,45 @@
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/contracts": {
|
||||
"/v1/pdfs/certificates": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate certificates based on the provided data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/pdf"
|
||||
],
|
||||
"tags": [
|
||||
"pdfs"
|
||||
],
|
||||
"summary": "Generate runner certificates",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Certificate data",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.CertificateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {}
|
||||
}
|
||||
},
|
||||
"/v1/pdfs/contracts": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Generate a contract based on the provided data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@@ -43,7 +154,7 @@
|
||||
"application/pdf"
|
||||
],
|
||||
"tags": [
|
||||
"contracts"
|
||||
"pdfs"
|
||||
],
|
||||
"summary": "Generate a contract",
|
||||
"parameters": [
|
||||
@@ -64,12 +175,18 @@
|
||||
"definitions": {
|
||||
"models.Card": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"code",
|
||||
"id",
|
||||
"runner"
|
||||
],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
@@ -81,6 +198,10 @@
|
||||
},
|
||||
"models.CardRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cards",
|
||||
"locale"
|
||||
],
|
||||
"properties": {
|
||||
"cards": {
|
||||
"type": "array",
|
||||
@@ -97,8 +218,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.CertificateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"locale",
|
||||
"runners"
|
||||
],
|
||||
"properties": {
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"en",
|
||||
"de"
|
||||
]
|
||||
},
|
||||
"runners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.RunnerWithDonations"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.ContractRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"locale",
|
||||
"runners"
|
||||
],
|
||||
"properties": {
|
||||
"locale": {
|
||||
"type": "string",
|
||||
@@ -115,22 +262,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Group": {
|
||||
"models.DistanceDonation": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"amount_per_distance",
|
||||
"donor",
|
||||
"id"
|
||||
],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"amount_per_distance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"donor": {
|
||||
"$ref": "#/definitions/models.Donor"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"paid_amount": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Donor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"first_name",
|
||||
"id",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"middle_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Group": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_group": {
|
||||
"$ref": "#/definitions/models.Group"
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Runner": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"first_name",
|
||||
"group",
|
||||
"id",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
@@ -148,6 +356,55 @@
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.RunnerWithDonations": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"distance",
|
||||
"first_name",
|
||||
"group",
|
||||
"id",
|
||||
"last_name"
|
||||
],
|
||||
"properties": {
|
||||
"distance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"distance_donations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.DistanceDonation"
|
||||
}
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"$ref": "#/definitions/models.Group"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"middle_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_donations": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_per_distance": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"ApiKeyAuth": {
|
||||
"type": "apiKey",
|
||||
"name": "key",
|
||||
"in": "query"
|
||||
}
|
||||
}
|
||||
}
|
||||
+185
-8
@@ -4,11 +4,16 @@ definitions:
|
||||
code:
|
||||
type: string
|
||||
enabled:
|
||||
default: true
|
||||
type: boolean
|
||||
id:
|
||||
type: integer
|
||||
runner:
|
||||
$ref: '#/definitions/models.Runner'
|
||||
required:
|
||||
- code
|
||||
- id
|
||||
- runner
|
||||
type: object
|
||||
models.CardRequest:
|
||||
properties:
|
||||
@@ -21,6 +26,24 @@ definitions:
|
||||
- en
|
||||
- de
|
||||
type: string
|
||||
required:
|
||||
- cards
|
||||
- locale
|
||||
type: object
|
||||
models.CertificateRequest:
|
||||
properties:
|
||||
locale:
|
||||
enum:
|
||||
- en
|
||||
- de
|
||||
type: string
|
||||
runners:
|
||||
items:
|
||||
$ref: '#/definitions/models.RunnerWithDonations'
|
||||
type: array
|
||||
required:
|
||||
- locale
|
||||
- runners
|
||||
type: object
|
||||
models.ContractRequest:
|
||||
properties:
|
||||
@@ -33,15 +56,55 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/models.Runner'
|
||||
type: array
|
||||
required:
|
||||
- locale
|
||||
- runners
|
||||
type: object
|
||||
models.DistanceDonation:
|
||||
properties:
|
||||
amount:
|
||||
type: integer
|
||||
amount_per_distance:
|
||||
type: integer
|
||||
donor:
|
||||
$ref: '#/definitions/models.Donor'
|
||||
id:
|
||||
type: integer
|
||||
paid_amount:
|
||||
type: integer
|
||||
required:
|
||||
- amount_per_distance
|
||||
- donor
|
||||
- id
|
||||
type: object
|
||||
models.Donor:
|
||||
properties:
|
||||
first_name:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
last_name:
|
||||
type: string
|
||||
middle_name:
|
||||
type: string
|
||||
required:
|
||||
- first_name
|
||||
- id
|
||||
- last_name
|
||||
type: object
|
||||
models.Group:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent_group:
|
||||
$ref: '#/definitions/models.Group'
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
models.Runner:
|
||||
properties:
|
||||
@@ -55,14 +118,99 @@ definitions:
|
||||
type: string
|
||||
middle_name:
|
||||
type: string
|
||||
required:
|
||||
- first_name
|
||||
- group
|
||||
- id
|
||||
- last_name
|
||||
type: object
|
||||
models.RunnerWithDonations:
|
||||
properties:
|
||||
distance:
|
||||
type: integer
|
||||
distance_donations:
|
||||
items:
|
||||
$ref: '#/definitions/models.DistanceDonation'
|
||||
type: array
|
||||
first_name:
|
||||
type: string
|
||||
group:
|
||||
$ref: '#/definitions/models.Group'
|
||||
id:
|
||||
type: integer
|
||||
last_name:
|
||||
type: string
|
||||
middle_name:
|
||||
type: string
|
||||
total_donations:
|
||||
type: integer
|
||||
total_per_distance:
|
||||
type: integer
|
||||
required:
|
||||
- distance
|
||||
- first_name
|
||||
- group
|
||||
- id
|
||||
- last_name
|
||||
type: object
|
||||
info:
|
||||
contact: {}
|
||||
contact:
|
||||
email: info@odit.services
|
||||
name: ODIT.Services UG (haftungsbeschränkt)
|
||||
url: https://odit.services
|
||||
description: This is the API documentation for the LfK Document Server - a tool
|
||||
for pdf generation.
|
||||
license:
|
||||
name: CC BY-NC-SA 4.0
|
||||
termsOfService: https://lauf-fuer-kaya.de/datenschutz
|
||||
title: LfK Document Server API
|
||||
paths:
|
||||
/cards:
|
||||
/v1/barcodes/{type}/{content}:
|
||||
get:
|
||||
description: Generate barcodes based on the provided data
|
||||
parameters:
|
||||
- description: Barcode type
|
||||
enum:
|
||||
- ean13
|
||||
- code128
|
||||
in: path
|
||||
name: type
|
||||
required: true
|
||||
type: string
|
||||
- description: Barcode content
|
||||
in: path
|
||||
minLength: 1
|
||||
name: content
|
||||
required: true
|
||||
type: string
|
||||
- default: 1000
|
||||
description: Barcode width
|
||||
in: query
|
||||
maximum: 10000
|
||||
minimum: 1
|
||||
name: width
|
||||
type: integer
|
||||
- default: 1000
|
||||
description: Barcode height
|
||||
in: query
|
||||
maximum: 10000
|
||||
minimum: 1
|
||||
name: height
|
||||
type: integer
|
||||
- default: 10
|
||||
description: Padding around the barcode (included in image size)
|
||||
in: query
|
||||
maximum: 100
|
||||
minimum: 0
|
||||
name: padding
|
||||
type: integer
|
||||
produces:
|
||||
- image/png
|
||||
responses: {}
|
||||
summary: Generate barcodes
|
||||
tags:
|
||||
- barcodes
|
||||
/v1/pdfs/cards:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
@@ -77,10 +225,32 @@ paths:
|
||||
produces:
|
||||
- application/pdf
|
||||
responses: {}
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Generate runner cards
|
||||
tags:
|
||||
- cards
|
||||
/contracts:
|
||||
- pdfs
|
||||
/v1/pdfs/certificates:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Generate certificates based on the provided data
|
||||
parameters:
|
||||
- description: Certificate data
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.CertificateRequest'
|
||||
produces:
|
||||
- application/pdf
|
||||
responses: {}
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Generate runner certificates
|
||||
tags:
|
||||
- pdfs
|
||||
/v1/pdfs/contracts:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
@@ -95,7 +265,14 @@ paths:
|
||||
produces:
|
||||
- application/pdf
|
||||
responses: {}
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Generate a contract
|
||||
tags:
|
||||
- contracts
|
||||
- pdfs
|
||||
securityDefinitions:
|
||||
ApiKeyAuth:
|
||||
in: query
|
||||
name: key
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
||||
|
||||
@@ -14,26 +14,48 @@ require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/makiuchi-d/gozxing v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oxplot/papersizes v0.0.0-20181201065918-90a3a5ae1915 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/redis/go-redis/v9 v9.7.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.57.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.uber.org/atomic 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/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/tools v0.27.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -4,8 +4,15 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
|
||||
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
@@ -20,6 +27,8 @@ github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ
|
||||
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
@@ -28,6 +37,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I=
|
||||
@@ -39,17 +50,47 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/oxplot/papersizes v0.0.0-20181201065918-90a3a5ae1915 h1:4WzMzgExTgBfuUQ/HegMf+jcHtH+c3fl7eySUQUbfzg=
|
||||
github.com/oxplot/papersizes v0.0.0-20181201065918-90a3a5ae1915/go.mod h1:LJRTnhoARxQgMyT7T9L+ZzwR4OrmyHTy5LPxZEzE1CM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files/v2 v2.0.1 h1:XCVJO/i/VosCDsJu1YLpdejGsGnBE9deRMpjN4pJLHk=
|
||||
github.com/swaggo/files/v2 v2.0.1/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
@@ -62,6 +103,16 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
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/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.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/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
@@ -70,12 +121,18 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// GenerateBarcode godoc
|
||||
//
|
||||
// @Summary Generate barcodes
|
||||
// @Description Generate barcodes based on the provided data
|
||||
// @Tags barcodes
|
||||
// @Param type path string true "Barcode type" Enums(ean13, code128)
|
||||
// @Param content path string true "Barcode content" MinLength(1)
|
||||
// @Param width query int false "Barcode width" Minimum(1) Maximum(10000) default(1000)
|
||||
// @Param height query int false "Barcode height" Minimum(1) Maximum(10000) default(1000)
|
||||
// @Param padding query int false "Padding around the barcode (included in image size)" Minimum(0) Maximum(100) default(10)
|
||||
// @Produce image/png
|
||||
// @Router /v1/barcodes/{type}/{content} [get]
|
||||
func (h *DefaultHandler) GenerateBarcode(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateBarcode")
|
||||
|
||||
// Get the type and content from the URL
|
||||
barcodeType := c.Params("type")
|
||||
barcodeContent := c.Params("content")
|
||||
|
||||
// Get the width and height from the query parameters
|
||||
widthStr := c.Query("width", "1000")
|
||||
heightStr := c.Query("height", "1000")
|
||||
paddingStr := c.Query("padding", "10")
|
||||
|
||||
// Convert width and height to integers
|
||||
width, err := strconv.Atoi(widthStr)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid width parameter", "width", widthStr, "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid width parameter",
|
||||
})
|
||||
}
|
||||
|
||||
height, err := strconv.Atoi(heightStr)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid height parameter", "height", heightStr, "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid height parameter",
|
||||
})
|
||||
}
|
||||
|
||||
padding, err := strconv.Atoi(paddingStr)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid padding parameter", "padding", paddingStr, "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid padding parameter",
|
||||
})
|
||||
}
|
||||
logger = logger.With("type", barcodeType, "content", barcodeContent, "width", width, "height", height, "padding", padding)
|
||||
|
||||
// Generate the barcode
|
||||
logger.Info("Generating barcode")
|
||||
barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to generate barcode", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Barcode generated")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "image/png")
|
||||
return c.Send(barcode.Bytes())
|
||||
}
|
||||
+35
-22
@@ -1,46 +1,52 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"git.odit.services/lfk/document-server/templates"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// GenerateCard godoc
|
||||
// @Summary Generate runner cards
|
||||
// @Description Generate cards based on the provided data
|
||||
// @Tags cards
|
||||
// @Accept json
|
||||
// @Param data body models.CardRequest true "Card data"
|
||||
// @Produce application/pdf
|
||||
// @Router /cards [post]
|
||||
func GenerateCard(c *fiber.Ctx) error {
|
||||
//
|
||||
// @Summary Generate runner cards
|
||||
// @Description Generate cards based on the provided data
|
||||
// @Tags pdfs
|
||||
// @Accept json
|
||||
// @Param data body models.CardRequest true "Card data"
|
||||
// @Produce application/pdf
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /v1/pdfs/cards [post]
|
||||
func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateCard")
|
||||
|
||||
cardRequest := new(models.CardRequest)
|
||||
if err := c.BodyParser(cardRequest); err != nil {
|
||||
logger.Errorw("Invalid request", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) {
|
||||
logger.Errorw("Invalid locale", "locale", cardRequest.Locale)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid locale",
|
||||
})
|
||||
}
|
||||
|
||||
generator := services.DefaultTemplater{}
|
||||
templateString, err := templates.GetTemplate(cardRequest.Locale, "card")
|
||||
logger = logger.With("locale", cardRequest.Locale)
|
||||
|
||||
templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
logger.Errorw("Template not found", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
template, err := generator.StringToTemplate(templateString)
|
||||
template, err := h.Templater.StringToTemplate(templateString)
|
||||
if err != nil {
|
||||
logger.Errorw("Error parsing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
@@ -48,28 +54,35 @@ func GenerateCard(c *fiber.Ctx) error {
|
||||
|
||||
genConfig := &models.CardTemplateOptions{
|
||||
CardSegments: splitCardSegments(cardRequest.Cards),
|
||||
EventName: "Event name",
|
||||
CardSubtitle: "Card subtitle",
|
||||
BarcodeFormat: "ean13",
|
||||
BarcodePrefix: "",
|
||||
EventName: h.Config.EventName,
|
||||
CardSubtitle: h.Config.CardSubtitle,
|
||||
BarcodeFormat: h.Config.CardBarcodeFormat,
|
||||
BarcodePrefix: h.Config.CardBarcodePrefix,
|
||||
}
|
||||
|
||||
result, err := generator.Execute(template, genConfig)
|
||||
logger.Info("Generating card html")
|
||||
result, err := h.Templater.Execute(template, genConfig)
|
||||
if err != nil {
|
||||
logger.Errorw("Error executing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Generated card html")
|
||||
c.Set(fiber.HeaderContentType, "text/html")
|
||||
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"}
|
||||
pdf, err := converter.ToPdf(result, "a4", false)
|
||||
|
||||
logger.Info("Converting html to pdf")
|
||||
pdf, err := h.Converter.ToPdf(result, "a4", false)
|
||||
if err != nil {
|
||||
logger.Errorw("Error converting html to pdf", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Converted html to pdf")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "application/pdf")
|
||||
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")
|
||||
return c.Send(pdf)
|
||||
}
|
||||
|
||||
|
||||
+57
-22
@@ -1,73 +1,108 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"git.odit.services/lfk/document-server/templates"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// GenerateCertificate godoc
|
||||
// @Summary Generate runner certificates
|
||||
// @Description Generate certificates based on the provided data
|
||||
// @Tags certificates
|
||||
// @Accept json
|
||||
// @Param data body models.CertificateRequest true "Certificate data"
|
||||
// @Produce application/pdf
|
||||
// @Router /certificates [post]
|
||||
func GenerateCertificate(c *fiber.Ctx) error {
|
||||
//
|
||||
// @Summary Generate runner certificates
|
||||
// @Description Generate certificates based on the provided data
|
||||
// @Tags pdfs
|
||||
// @Accept json
|
||||
// @Param data body models.CertificateRequest true "Certificate data"
|
||||
// @Produce application/pdf
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /v1/pdfs/certificates [post]
|
||||
func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateCertificate")
|
||||
|
||||
certificateRequest := new(models.CertificateRequest)
|
||||
if err := c.BodyParser(certificateRequest); err != nil {
|
||||
logger.Errorw("Invalid request", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) {
|
||||
logger.Errorw("Invalid locale", "locale", certificateRequest.Locale)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid locale",
|
||||
})
|
||||
}
|
||||
|
||||
generator := services.DefaultTemplater{}
|
||||
templateString, err := templates.GetTemplate(certificateRequest.Locale, "certificate")
|
||||
logger = logger.With("locale", certificateRequest.Locale)
|
||||
|
||||
templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
logger.Errorw("Template not found", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
template, err := generator.StringToTemplate(templateString)
|
||||
template, err := h.Templater.StringToTemplate(templateString)
|
||||
if err != nil {
|
||||
logger.Errorw("Error parsing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
genConfig := &models.CertificateTemplateOptions{
|
||||
Runners: certificateRequest.Runners,
|
||||
EventName: "Event name",
|
||||
Footer: "Footer",
|
||||
CurrencySymbol: "€",
|
||||
Runners: addUpRunnerDonations(certificateRequest.Runners),
|
||||
EventName: h.Config.EventName,
|
||||
Footer: h.Config.CertificateFooter,
|
||||
CurrencySymbol: h.Config.CurrencySymbol,
|
||||
Locale: certificateRequest.Locale,
|
||||
SepaConfig: &models.SepaConfig{
|
||||
BIC: h.Config.SepaBic,
|
||||
HolderName: h.Config.SepaName,
|
||||
IBAN: h.Config.SepaIban,
|
||||
CurrencyIdentifier: h.Config.CurrencyIdentifier,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := generator.Execute(template, genConfig)
|
||||
logger.Info("Generating certificate html")
|
||||
result, err := h.Templater.Execute(template, genConfig)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Generated card html")
|
||||
c.Set(fiber.HeaderContentType, "text/html")
|
||||
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"}
|
||||
pdf, err := converter.ToPdf(result, "a4", false)
|
||||
|
||||
logger.Info("Converting html to pdf")
|
||||
pdf, err := h.Converter.ToPdf(result, "a4", false)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Converted html to pdf")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "application/pdf")
|
||||
c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf")
|
||||
return c.Send(pdf)
|
||||
}
|
||||
|
||||
func addUpRunnerDonations(runners []models.RunnerWithDonations) []models.RunnerWithDonations {
|
||||
for i := 0; i < len(runners); i++ {
|
||||
for j := 0; j < len(runners[i].DistanceDonations); j++ {
|
||||
runners[i].TotalDonations += runners[i].DistanceDonations[j].Amount
|
||||
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
|
||||
}
|
||||
|
||||
+34
-27
@@ -1,80 +1,87 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"git.odit.services/lfk/document-server/templates"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// GenerateContract godoc
|
||||
// @Summary Generate a contract
|
||||
// @Description Generate a contract based on the provided data
|
||||
// @Tags contracts
|
||||
// @Accept json
|
||||
// @Param data body models.ContractRequest true "Contract data"
|
||||
// @Produce application/pdf
|
||||
// @Router /contracts [post]
|
||||
func GenerateContract(c *fiber.Ctx) error {
|
||||
//
|
||||
// @Summary Generate a contract
|
||||
// @Description Generate a contract based on the provided data
|
||||
// @Tags pdfs
|
||||
// @Accept json
|
||||
// @Param data body models.ContractRequest true "Contract data"
|
||||
// @Produce application/pdf
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /v1/pdfs/contracts [post]
|
||||
func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
|
||||
|
||||
logger := h.Logger.Named("GenerateContract")
|
||||
|
||||
contract := new(models.ContractRequest)
|
||||
if err := c.BodyParser(contract); err != nil {
|
||||
logger.Errorw("Invalid request", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
if !slices.Contains([]string{"en", "de"}, contract.Locale) {
|
||||
logger.Errorw("Invalid locale", "locale", contract.Locale)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Invalid locale",
|
||||
})
|
||||
}
|
||||
|
||||
contract.Runners = repeatRunnerArrayItems(contract.Runners, 2)
|
||||
logger = logger.With("locale", contract.Locale)
|
||||
|
||||
generator := services.DefaultTemplater{}
|
||||
templateString, err := templates.GetTemplate(contract.Locale, "contract")
|
||||
templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
logger.Errorw("Template not found", "error", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Template not found",
|
||||
})
|
||||
}
|
||||
template, err := generator.StringToTemplate(templateString)
|
||||
template, err := h.Templater.StringToTemplate(templateString)
|
||||
if err != nil {
|
||||
logger.Errorw("Error parsing template", "error", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
genConfig := &models.ContractTemplateOptions{
|
||||
Runners: contract.Runners,
|
||||
CurrencySymbol: "€",
|
||||
Disclaimer: "This is a disclaimer",
|
||||
ReceiptMinimumAmount: 10,
|
||||
EventName: "Event name",
|
||||
SponsoringHeader: templates.ImageSponsoringHeaderBase64,
|
||||
BarcodeFormat: "ean13",
|
||||
BarcodePrefix: "1",
|
||||
Runners: repeatRunnerArrayItems(contract.Runners, 2),
|
||||
CurrencySymbol: h.Config.CurrencySymbol,
|
||||
Disclaimer: h.Config.SponosringDisclaimer,
|
||||
ReceiptMinimumAmount: h.Config.SponsoringReceiptMinimum,
|
||||
EventName: h.Config.EventName,
|
||||
BarcodeFormat: h.Config.SponsoringBarcodeFormat,
|
||||
BarcodePrefix: h.Config.SponsoringBarcodePrefix,
|
||||
}
|
||||
|
||||
result, err := generator.Execute(template, genConfig)
|
||||
logger.Info("Generating contract html")
|
||||
result, err := h.Templater.Execute(template, genConfig)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Generated contract html")
|
||||
|
||||
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"}
|
||||
pdf, err := converter.ToPdf(result, "a5", true)
|
||||
logger.Info("Converting html to pdf")
|
||||
pdf, err := h.Converter.ToPdf(result, "a5", true)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
logger.Info("Converted html to pdf")
|
||||
|
||||
c.Set(fiber.HeaderContentType, "application/pdf")
|
||||
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")
|
||||
return c.Send(pdf)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"git.odit.services/lfk/document-server/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
GenerateCard(*fiber.Ctx) error
|
||||
GenerateContract(*fiber.Ctx) error
|
||||
GenerateCertificate(*fiber.Ctx) error
|
||||
GenerateBarcode(*fiber.Ctx) error
|
||||
}
|
||||
|
||||
type DefaultHandler struct {
|
||||
Config *models.Config
|
||||
BarcodeService services.BarcodeService
|
||||
Templater services.Templater
|
||||
Converter services.Converter
|
||||
StaticService services.StaticService
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
+1
-1
@@ -2,6 +2,6 @@ package handlers
|
||||
|
||||
import "github.com/gofiber/fiber/v2"
|
||||
|
||||
func NotFoundHandler(c *fiber.Ctx) error {
|
||||
func (h *DefaultHandler) NotFoundHandler(c *fiber.Ctx) error {
|
||||
return c.Status(404).SendString("Not Found")
|
||||
}
|
||||
|
||||
@@ -1,45 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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/models"
|
||||
"git.odit.services/lfk/document-server/services"
|
||||
"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/requestid"
|
||||
"github.com/gofiber/swagger"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var (
|
||||
port = flag.String("port", ":3000", "Port to listen on")
|
||||
prod = flag.Bool("prod", false, "Enable prefork in Production")
|
||||
config *models.Config
|
||||
logger *zap.SugaredLogger
|
||||
)
|
||||
|
||||
// @title LfK Document Server API
|
||||
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
|
||||
func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
|
||||
hashedAPIKey := sha256.Sum256([]byte(config.APIKey))
|
||||
hashedKey := sha256.Sum256([]byte(key))
|
||||
|
||||
if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, keyauth.ErrMissingOrMalformedAPIKey
|
||||
}
|
||||
|
||||
func loadEnv() error {
|
||||
|
||||
viper.SetDefault("LOGLEVEL", "INFO")
|
||||
viper.SetDefault("PRODUCION", false)
|
||||
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_BARCODEPREFIX", "")
|
||||
viper.SetDefault("SPONSORING_RECEIPTMINIMUM", 0)
|
||||
viper.SetDefault("SPONSORING_DISCLAIMER", "Disclaimer")
|
||||
viper.SetDefault("SPONSORING_BARCODEFORMAT", "code128")
|
||||
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
|
||||
viper.SetConfigFile(".env")
|
||||
|
||||
// Load environment variables
|
||||
viper.AutomaticEnv()
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
logger.Warn("No .env file found")
|
||||
}
|
||||
|
||||
// Unmarshal the config from file and env into the config struct
|
||||
err = viper.Unmarshal(&config)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// @title LfK Document Server API
|
||||
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation.
|
||||
// @license.name CC BY-NC-SA 4.0
|
||||
// @termsOfService https://lauf-fuer-kaya.de/datenschutz
|
||||
// @contact.name ODIT.Services UG (haftungsbeschränkt)
|
||||
// @contact.url https://odit.services
|
||||
// @contact.email info@odit.services
|
||||
// @securityDefinitions.apiKey ApiKeyAuth
|
||||
// @in query
|
||||
// @name key
|
||||
func main() {
|
||||
// Parse command-line flags
|
||||
flag.Parse()
|
||||
|
||||
// Init the logger
|
||||
err := initLogger()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = loadEnv()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var redisClient *redis.Client
|
||||
if config.RedisAddr != "" {
|
||||
logger.Infow("Using redis", "redisAddr", config.RedisAddr)
|
||||
redisClient = redis.NewClient(&redis.Options{
|
||||
Addr: config.RedisAddr,
|
||||
})
|
||||
}
|
||||
|
||||
barcodeGenerator := &services.DefaultBarcodeService{
|
||||
RedisClient: redisClient,
|
||||
Logger: logger.Named("DefaultBarcodeService"),
|
||||
}
|
||||
staticService := &services.DefaultStaticService{
|
||||
Cache: make(map[string]string),
|
||||
Logger: logger.Named("DefaultStaticService"),
|
||||
}
|
||||
handler := handlers.DefaultHandler{
|
||||
Config: config,
|
||||
BarcodeService: barcodeGenerator,
|
||||
StaticService: staticService,
|
||||
Templater: &services.DefaultTemplater{
|
||||
BarcodeService: barcodeGenerator,
|
||||
StaticService: staticService,
|
||||
Logger: logger.Named("DefaultTemplater"),
|
||||
},
|
||||
Converter: &services.GotenbergConverter{
|
||||
BaseUrl: config.GotenbergBaseUrl,
|
||||
Logger: logger.Named("GotenbergConverter"),
|
||||
},
|
||||
Logger: logger.Named("DefaultHandler"),
|
||||
}
|
||||
logger.Debug("Initialized services")
|
||||
|
||||
// Create a new Fiber instance
|
||||
app := fiber.New(fiber.Config{
|
||||
Prefork: *prod,
|
||||
Prefork: config.Prod,
|
||||
})
|
||||
|
||||
app.Use(cors.New())
|
||||
app.Use(requestid.New())
|
||||
|
||||
// Swagger documentation route
|
||||
app.Get("/swagger/*", swagger.HandlerDefault)
|
||||
|
||||
v1 := app.Group("/v1")
|
||||
|
||||
v1.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.SendString("Hello, World!")
|
||||
})
|
||||
v1.Post("/contracts", handlers.GenerateContract)
|
||||
v1.Post("/cards", handlers.GenerateCard)
|
||||
v1.Post("/certificates", handlers.GenerateCertificate)
|
||||
pdfv1 := v1.Group("/pdfs")
|
||||
pdfv1.Use(keyauth.New(keyauth.Config{
|
||||
KeyLookup: "query:key",
|
||||
Validator: validateAPIKey,
|
||||
}))
|
||||
|
||||
app.Use(handlers.NotFoundHandler)
|
||||
pdfv1.Post("/contracts", handler.GenerateContract)
|
||||
pdfv1.Post("/cards", handler.GenerateCard)
|
||||
pdfv1.Post("/certificates", handler.GenerateCertificate)
|
||||
|
||||
v1.Get("/barcodes/:type/:content", handler.GenerateBarcode)
|
||||
logger.Debug("Initialized routes")
|
||||
|
||||
app.Use(handler.NotFoundHandler)
|
||||
docs.SwaggerInfo.BasePath = "/"
|
||||
|
||||
log.Fatal(app.Listen(*port))
|
||||
logger.Infow("Starting server", "port", config.Port)
|
||||
logger.Error(app.Listen("0.0.0.0:" + config.Port))
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,15 +1,15 @@
|
||||
package models
|
||||
|
||||
type CardRequest struct {
|
||||
Cards []Card `json:"cards"`
|
||||
Locale string `json:"locale" enums:"en,de"`
|
||||
Cards []Card `json:"cards" validate:"required"`
|
||||
Locale string `json:"locale" enums:"en,de" validate:"required"`
|
||||
}
|
||||
|
||||
type Card struct {
|
||||
ID int `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Runner Runner `json:"runner"`
|
||||
Code string `json:"code"`
|
||||
ID int `json:"id" validate:"required"`
|
||||
Enabled bool `json:"enabled" default:"true"`
|
||||
Runner Runner `json:"runner" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
type CardTemplateOptions struct {
|
||||
|
||||
+30
-19
@@ -1,35 +1,37 @@
|
||||
package models
|
||||
|
||||
type CertificateRequest struct {
|
||||
Runners []RunnerWithDonations `json:"runners"`
|
||||
Locale string `json:"locale" enums:"en,de"`
|
||||
Runners []RunnerWithDonations `json:"runners" validate:"required"`
|
||||
Locale string `json:"locale" enums:"en,de" validate:"required"`
|
||||
}
|
||||
|
||||
type RunnerWithDonations struct {
|
||||
ID int `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
MiddleName string `json:"middle_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Group Group `json:"group"`
|
||||
Distance int `json:"distance"`
|
||||
DistanceDonations []DistanceDonation `json:"distance_donations"`
|
||||
TotalPerDistance int `json:"total_per_distance"`
|
||||
TotalDonations int `json:"total_donations"`
|
||||
ID int `json:"id" validate:"required"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
MiddleName string `json:"middle_name" validate:"optional"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Group Group `json:"group" validate:"required"`
|
||||
CombinedGroupName string `json:"combined_group_name" validate:"optional"`
|
||||
Distance int `json:"distance" validate:"required"`
|
||||
DistanceDonations []DistanceDonation `json:"distance_donations" validate:"optional"`
|
||||
TotalPerDistance int `json:"total_per_distance" validate:"optional"`
|
||||
TotalDonations int `json:"total_donations" validate:"optional"`
|
||||
SelfServiceLink string `json:"self_service_link" validate:"required"`
|
||||
}
|
||||
|
||||
type DistanceDonation struct {
|
||||
ID int `json:"id"`
|
||||
ID int `json:"id" validate:"required"`
|
||||
Amount int `json:"amount"`
|
||||
PaidAmount int `json:"paid_amount"`
|
||||
AmountPerDistance int `json:"amount_per_distance"`
|
||||
Donor Donor `json:"donor"`
|
||||
PaidAmount int `json:"paid_amount" validate:"optional"`
|
||||
AmountPerDistance int `json:"amount_per_distance" validate:"required"`
|
||||
Donor Donor `json:"donor" validate:"required"`
|
||||
}
|
||||
|
||||
type Donor struct {
|
||||
ID int `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
MiddleName string `json:"middle_name"`
|
||||
LastName string `json:"last_name"`
|
||||
ID int `json:"id" validate:"required"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
MiddleName string `json:"middle_name" validate:"optional"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
}
|
||||
|
||||
type CertificateTemplateOptions struct {
|
||||
@@ -37,4 +39,13 @@ type CertificateTemplateOptions struct {
|
||||
EventName string `json:"event_name"`
|
||||
Footer string `json:"footer"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package models
|
||||
|
||||
type Config struct {
|
||||
LogLevel string `mapstructure:"LOGLEVEL"`
|
||||
Prod bool `mapstructure:"PRODUCION"`
|
||||
Port string `mapstructure:"PORT"`
|
||||
APIKey string `mapstructure:"APIKEY"`
|
||||
EventName string `mapstructure:"EVENTNAME"`
|
||||
CurrencySymbol string `mapstructure:"CURRENCYSYMBOL"`
|
||||
CurrencyIdentifier string `mapstructure:"CURRENCYIDENTIFIER"`
|
||||
CardSubtitle string `mapstructure:"CARD_SUBTITLE"`
|
||||
CardBarcodeFormat string `mapstructure:"CARD_BARCODEFORMAT"`
|
||||
CardBarcodePrefix string `mapstructure:"CARD_BARCODEPREFIX"`
|
||||
SponsoringReceiptMinimum string `mapstructure:"SPONSORING_RECEIPTMINIMUM"`
|
||||
SponosringDisclaimer string `mapstructure:"SPONSORING_DISCLAIMER"`
|
||||
SponsoringBarcodeFormat string `mapstructure:"SPONSORING_BARCODEFORMAT"`
|
||||
SponsoringBarcodePrefix string `mapstructure:"SPONSORING_BARCODEPREFIX"`
|
||||
CertificateFooter string `mapstructure:"CERTIFICATE_FOOTER"`
|
||||
GotenbergBaseUrl string `mapstructure:"GOTENBERG_BASEURL"`
|
||||
RedisAddr string `mapstructure:"REDIS_ADDR"`
|
||||
SepaBic string `mapstructure:"SEPA_BIC"`
|
||||
SepaName string `mapstructure:"SEPA_NAME"`
|
||||
SepaIban string `mapstructure:"SEPA_IBAN"`
|
||||
}
|
||||
+12
-12
@@ -1,31 +1,31 @@
|
||||
package models
|
||||
|
||||
type ContractRequest struct {
|
||||
Runners []Runner `json:"runners"`
|
||||
Locale string `json:"locale" enums:"en,de"`
|
||||
Runners []Runner `json:"runners" validate:"required"`
|
||||
Locale string `json:"locale" enums:"en,de" validate:"required"`
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
ID int `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
MiddleName string `json:"middle_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Group Group `json:"group"`
|
||||
ID int `json:"id" validate:"required"`
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
MiddleName string `json:"middle_name" validate:"optional"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Group Group `json:"group" validate:"required"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ParentGroup *Group `json:"parent_group"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
ParentGroup struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
} `json:"parent_group" validate:"optional"`
|
||||
}
|
||||
|
||||
type ContractTemplateOptions struct {
|
||||
Runners []Runner `json:"runners"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
Disclaimer string `json:"disclaimer"`
|
||||
ReceiptMinimumAmount int `json:"receipt_minimum_amount"`
|
||||
ReceiptMinimumAmount string `json:"receipt_minimum_amount"`
|
||||
EventName string `json:"event_name"`
|
||||
SponsoringHeader string `json:"base_url"`
|
||||
BarcodeFormat string `json:"barcode_format"`
|
||||
BarcodePrefix string `json:"barcode_prefix"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/boombuler/barcode"
|
||||
"github.com/boombuler/barcode/code128"
|
||||
"github.com/boombuler/barcode/ean"
|
||||
"github.com/boombuler/barcode/qr"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type BarcodeService interface {
|
||||
GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error)
|
||||
IsTypeSupported(format string) bool
|
||||
}
|
||||
|
||||
type DefaultBarcodeService struct {
|
||||
RedisClient *redis.Client
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) {
|
||||
ctx := context.Background()
|
||||
logger := b.Logger.Named("GenerateBarcode")
|
||||
|
||||
if !b.IsTypeSupported(format) {
|
||||
logger.Errorw("Unsupported barcode type", "type", 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 {
|
||||
logger.Debugw("Checking cache for barcode", "key", cacheKey)
|
||||
cachedBarcode, err := b.RedisClient.Get(ctx, cacheKey).Result()
|
||||
if err == nil {
|
||||
logger.Infow("Barcode found in cache", "key", cacheKey)
|
||||
buf := bytes.Buffer{}
|
||||
buf.Write([]byte(cachedBarcode))
|
||||
return buf, nil
|
||||
}
|
||||
}
|
||||
|
||||
var generatedCode barcode.Barcode
|
||||
var err error
|
||||
|
||||
switch format {
|
||||
case "ean13":
|
||||
generatedCode, err = ean.Encode(content)
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
break
|
||||
case "code128":
|
||||
generatedCode, err = code128.Encode(content)
|
||||
if err != nil {
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
break
|
||||
case "qr":
|
||||
// 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 {
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Create a white background image
|
||||
bg := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
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
|
||||
newWidth := width - 2*padding
|
||||
newHeight := height - 2*padding
|
||||
|
||||
// Scale the barcode to the new size
|
||||
scaledCode, err := barcode.Scale(generatedCode, newWidth, newHeight)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to scale barcode", "error", err)
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
logger.Debug("Scaled barcode")
|
||||
|
||||
// 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)
|
||||
logger.Debug("Drew barcode on background")
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = png.Encode(&buf, bg)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to encode barcode to PNG", "error", err)
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
logger.Debug("Encoded barcode to PNG")
|
||||
|
||||
if b.RedisClient != nil {
|
||||
err = b.RedisClient.Set(ctx, cacheKey, buf.String(), 10*time.Minute).Err()
|
||||
logger.Debugw("Cached barcode", "key", cacheKey)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to cache barcode", "error", err)
|
||||
return bytes.Buffer{}, err
|
||||
}
|
||||
}
|
||||
logger.Info("Generated barcode")
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func (b *DefaultBarcodeService) IsTypeSupported(format string) bool {
|
||||
supportedTypes := []string{"ean13", "code128", "qr"}
|
||||
return slices.Contains(supportedTypes, format)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/oxplot/papersizes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Converter interface {
|
||||
@@ -16,92 +17,115 @@ type Converter interface {
|
||||
|
||||
type GotenbergConverter struct {
|
||||
BaseUrl string
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
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{}
|
||||
defer client.CloseIdleConnections()
|
||||
logger.Debug("Created HTTP client")
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("files", "index.html")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to create form file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = part.Write([]byte(html))
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write to form file", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
size := papersizes.FromName(pageSize)
|
||||
if size == nil {
|
||||
logger.Errorw("Invalid page size", "size", pageSize)
|
||||
return nil, fmt.Errorf("invalid page size: %s", pageSize)
|
||||
}
|
||||
|
||||
err = writer.WriteField("paperWidth", strconv.Itoa(size.Width)+"mm")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write paper width", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("paperHeight", strconv.Itoa(size.Height)+"mm")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write paper height", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("landscape", strconv.FormatBool(landscape))
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write landscape", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginTop", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin top", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginBottom", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin bottom", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginLeft", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin left", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("marginRight", "0")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write margin right", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.WriteField("preferCssPageSize", "true")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to write prefer css page size", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to close writer", "error", 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
logger.Debug("Sending HTTP request")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to send request", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
logger.Debug("Received HTTP response")
|
||||
|
||||
defer resp.Body.Close()
|
||||
data := new(bytes.Buffer)
|
||||
_, err = data.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to read response body", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("Returning PDF data")
|
||||
return data.Bytes(), nil
|
||||
}
|
||||
|
||||
+64
-46
@@ -4,14 +4,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"image/png"
|
||||
"strings"
|
||||
|
||||
"git.odit.services/lfk/document-server/templates"
|
||||
"github.com/boombuler/barcode"
|
||||
"github.com/boombuler/barcode/code128"
|
||||
"github.com/boombuler/barcode/ean"
|
||||
"github.com/boombuler/barcode/qr"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Templater interface {
|
||||
@@ -20,6 +17,9 @@ type Templater interface {
|
||||
}
|
||||
|
||||
type DefaultTemplater struct {
|
||||
BarcodeService BarcodeService
|
||||
StaticService StaticService
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func idToEan13(id string, prefix string) (string, error) {
|
||||
@@ -35,64 +35,82 @@ func idToEan13(id string, prefix string) (string, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
|
||||
var generatedCode barcode.Barcode
|
||||
func (t *DefaultTemplater) GenerateEPC(iban string, bic string, name string, title string, amount int, currency string) (string, error) {
|
||||
var err error
|
||||
|
||||
switch format {
|
||||
case "ean13":
|
||||
encodedEan, err := idToEan13(code, prefix)
|
||||
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) {
|
||||
var err error
|
||||
|
||||
if format == "ean13" {
|
||||
code, err = idToEan13(code, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
generatedCode, err = ean.Encode(encodedEan)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
case "code128":
|
||||
generatedCode, err = code128.Encode(prefix + code)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
case "qr":
|
||||
generatedCode, err = qr.Encode(prefix+code, qr.M, qr.AlphaNumeric)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
break
|
||||
default:
|
||||
return "", errors.New("unknown barcode format")
|
||||
}
|
||||
|
||||
scaledCode, err := barcode.Scale(generatedCode, 1000, 500)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf, err := t.BarcodeService.GenerateBarcode(format, code, 1000, 500, 0)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = png.Encode(&buf, scaledCode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
||||
return base64.StdEncoding.EncodeToString(buf.Bytes()), err
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
|
||||
sponsors := []string{
|
||||
"vrbank",
|
||||
"odit",
|
||||
"sparkasse",
|
||||
logger := t.Logger.Named("SelectSponsorImage")
|
||||
sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
|
||||
if err != nil {
|
||||
logger.Errorw("Failed to list sponsors", "error", err)
|
||||
return "", err
|
||||
}
|
||||
return templates.GetImage(sponsors[id%len(sponsors)]), nil
|
||||
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
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) LoadImage(name string) (string, error) {
|
||||
return t.StaticService.GetImage(name), nil
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) FormatUnit(unit string, locale string, amount int) (string, error) {
|
||||
var formatted string
|
||||
switch unit {
|
||||
case "kilometer":
|
||||
formatted = fmt.Sprintf("%.3f", float64(amount)/1000)
|
||||
case "euro":
|
||||
formatted = fmt.Sprintf("%.2f", float64(amount)/100)
|
||||
default:
|
||||
return "", errors.New("unknown unit")
|
||||
}
|
||||
|
||||
if locale == "de" {
|
||||
return strings.Replace(formatted, ".", ",", -1), nil
|
||||
}
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Template, error) {
|
||||
return template.New("template").Funcs(template.FuncMap{
|
||||
"barcode": t.GenerateBarcode,
|
||||
"sponsorLogo": t.SelectSponsorImage,
|
||||
"formatUnit": t.FormatUnit,
|
||||
"loadImage": t.LoadImage,
|
||||
"epcCode": t.GenerateEPC,
|
||||
}).Parse(templateString)
|
||||
}
|
||||
|
||||
|
||||
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
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>
|
||||
<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">
|
||||
<style>
|
||||
.sheet {
|
||||
@@ -50,8 +50,12 @@
|
||||
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if ne .Runner.FirstName "" }}
|
||||
<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}}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<head>
|
||||
<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">
|
||||
<style>
|
||||
.sheet {
|
||||
@@ -49,8 +49,12 @@
|
||||
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ if ne .Runner.FirstName ""}}
|
||||
<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>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -0,0 +1,119 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Läuferurkunde</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||
<style>
|
||||
.sheet {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
page-break-after: always;
|
||||
padding: 1.2cm 2cm 1.2cm 2cm;
|
||||
background-image: url("data:image/png;base64,{{ loadImage "certificate_background" }}");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 11cm;
|
||||
background-position: 5cm 5cm;
|
||||
color: #000;
|
||||
/* border-style: solid; */
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
color: black;
|
||||
}
|
||||
|
||||
body.A4 .sheet {
|
||||
width: 210mm;
|
||||
height: 296mm
|
||||
}
|
||||
|
||||
.certificate-footer {
|
||||
position: absolute;
|
||||
bottom: 0cm;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid black;
|
||||
font-size: large;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="A4 landscape">
|
||||
{{ range .Runners }}
|
||||
<article class="sheet">
|
||||
<header class="content has-text-centered">
|
||||
<p style="font-size: 3cm; font-weight: bold;">Urkunde</p>
|
||||
</header>
|
||||
<main class="content has-text-centered" style="padding-top: 3cm;">
|
||||
<p style="width: 50%; font-size: 4vw; font-weight: bold; margin-bottom: 0; display: inline;">{{ .FirstName }}
|
||||
{{ .MiddleName }} {{ .LastName }}
|
||||
</p>
|
||||
<p style="font-size: 1cm; margin-bottom: 0;">hat beim {{ $.EventName }}</p>
|
||||
<p style="font-size: 2cm; font-weight: bold; margin-bottom: 0;">{{formatUnit "kilometer" $.Locale .Distance}}km</p>
|
||||
<p style="font-size: 1cm;">für den guten Zweck zurückgelegt</p>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<img src="data:image/png;base64,{{ loadImage "certificate_footer" }}">
|
||||
</footer>
|
||||
</article>
|
||||
<article class="sheet">
|
||||
<header class="content has-text-centered">
|
||||
<p style="font-size: 2.5cm; font-weight: bold;">Sponsorings</p>
|
||||
</header>
|
||||
<main>
|
||||
<table style="border: solid; width: 17cm;">
|
||||
<thead>
|
||||
<td class=".td-head">Sponsor:in</td>
|
||||
<td>Betrag/km</td>
|
||||
<td>Gesamtbetrag</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .DistanceDonations}}
|
||||
<tr>
|
||||
<td>{{ .Donor.FirstName }} {{ .Donor.MiddleName }} {{ .Donor.LastName }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .AmountPerDistance }} {{ $.CurrencySymbol }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .Amount }} {{ $.CurrencySymbol }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<td>Gesamt</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .TotalPerDistance }} {{ $.CurrencySymbol }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .TotalDonations }} {{ $.CurrencySymbol }}</td>
|
||||
</table>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<table style="border-collapse: collapse; border: none; width: 17cm;">
|
||||
<thead>
|
||||
<tr>
|
||||
<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 "Spende LfK " .ID ", " .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="width: 17cm; text-align: center;">
|
||||
Sponsoren überweisen ihre Beträge bitte auf unser Konto: {{ $.SepaConfig.HolderName }} | IBAN: {{ $.SepaConfig.IBAN }} | BIC: {{ $.SepaConfig.BIC }} | Vz: "Spende LfK {{.ID}}, {{ .FirstName }} {{ .LastName }}, {{.CombinedGroupName}}"
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
{{ end }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,120 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<title>Runner certificate</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
|
||||
<style>
|
||||
.sheet {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
page-break-after: always;
|
||||
padding: 1.2cm 2cm 1.2cm 2cm;
|
||||
background-image: url("data:image/png;base64,{{ loadImage "certificate_background" }}");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 11cm;
|
||||
background-position: 5cm 5cm;
|
||||
color: #000;
|
||||
/* border-style: solid; */
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
color: black;
|
||||
}
|
||||
|
||||
body.A4 .sheet {
|
||||
width: 210mm;
|
||||
height: 296mm
|
||||
}
|
||||
|
||||
.certificate-footer {
|
||||
position: absolute;
|
||||
bottom: 0cm;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid black;
|
||||
font-size: large;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="A4 landscape">
|
||||
{{ range .Runners }}
|
||||
<article class="sheet">
|
||||
<header class="content has-text-centered">
|
||||
<p style="font-size: 3cm; font-weight: bold;">Certificate</p>
|
||||
</header>
|
||||
<main class="content has-text-centered" style="padding-top: 3cm;">
|
||||
<p style="width: 50%; font-size: 4vw; font-weight: bold; margin-bottom: 0; display: inline;">{{ .FirstName }}
|
||||
{{ .MiddleName }} {{ .LastName }}
|
||||
</p>
|
||||
<p style="font-size: 1cm; margin-bottom: 0;">Ran</p>
|
||||
<p style="font-size: 2cm; font-weight: bold; margin-bottom: 0;">{{formatUnit "kilometer" $.Locale .Distance}}km</p>
|
||||
<p style="font-size: 1cm;">for our good cause at the {{ $.EventName }}</p>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<img src="data:image/png;base64,{{ loadImage "certificate_footer" }}">
|
||||
</footer>
|
||||
</article>
|
||||
<article class="sheet">
|
||||
<header class="content has-text-centered">
|
||||
<p style="font-size: 2.5cm; font-weight: bold;">Donations</p>
|
||||
</header>
|
||||
<main>
|
||||
<table style="border: solid; width: 17cm;">
|
||||
<thead>
|
||||
<td class=".td-head">Donor</td>
|
||||
<td>Amount / km</td>
|
||||
<td>Total</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .DistanceDonations}}
|
||||
<tr>
|
||||
<td>{{ .Donor.FirstName }} {{ .Donor.MiddleName }} {{ .Donor.LastName }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .AmountPerDistance }} {{ $.CurrencySymbol }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .Amount }} {{ $.CurrencySymbol }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<td>Combined</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .TotalPerDistance }} {{ $.CurrencySymbol }}</td>
|
||||
<td>{{ formatUnit "euro" $.Locale .TotalDonations }} {{ $.CurrencySymbol }}</td>
|
||||
</tfoot>
|
||||
</table>
|
||||
</main>
|
||||
<footer class="certificate-footer">
|
||||
<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 "Spende LfK " .ID ", " .FirstName " " .LastName ", " .CombinedGroupName) .TotalDonations $.SepaConfig.CurrencyIdentifier}}" style="height: 2.5cm; padding: 0.2cm">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="width: 17cm; text-align: center;">
|
||||
Donors, please transfer your donation to our account: {{ $.SepaConfig.HolderName }} | IBAN: {{ $.SepaConfig.IBAN }} | BIC: {{ $.SepaConfig.BIC }} | Ref: "Spende LfK {{.ID}}, {{ .FirstName }} {{ .LastName }}, {{.CombinedGroupName}}"
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
{{ end }}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -27,7 +27,7 @@
|
||||
<body class="A5 landscape">
|
||||
{{ range .Runners }}
|
||||
<div class="sheet">
|
||||
<img id="header_img" width="100%" src="data:image/png;base64,{{ $.SponsoringHeader}}" />
|
||||
<img id="header_img" width="100%" src="data:image/png;base64,{{ loadImage "sponsoringheader" }}" />
|
||||
<div style=" padding: 0 1rem 0 1rem;">
|
||||
<div class="columns">
|
||||
<div class="column is-10">
|
||||
@@ -65,8 +65,8 @@
|
||||
<p style="font-size: x-small; display: block;">Nachname</p>
|
||||
</div>
|
||||
<div class="column is-6">
|
||||
<span style="border-bottom: 1px solid; width: 100%; display: block;">{{ .Group.Name }}</span>
|
||||
<p style="font-size: x-small; display: block;">Team/ Klasse</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin-top: -0.5rem">mit einem Betrag von _____ {{ $.CurrencySymbol }} pro gelaufenem Kilometer zu
|
||||
@@ -27,7 +27,7 @@
|
||||
<body class="A5 landscape">
|
||||
{{ range .Runners }}
|
||||
<div class="sheet">
|
||||
<img id="header_img" width="100%" src="data:image/png;base64,{{ $.SponsoringHeader}}" />
|
||||
<img id="header_img" width="100%" src="data:image/png;base64,{{ loadImage "sponsoringheader" }}" />
|
||||
<div style=" padding: 0 1rem 0 1rem;">
|
||||
<div class="columns">
|
||||
<div class="column is-10">
|
||||
@@ -64,7 +64,7 @@
|
||||
<p style="font-size: x-small; display: block;">Last Name</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 225 KiB |
@@ -1,65 +0,0 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
//go:embed card/en.html
|
||||
var CardTemplateEn string
|
||||
|
||||
//go:embed card/de.html
|
||||
var CardTemplateDe string
|
||||
|
||||
//go:embed contract/en.html
|
||||
var ContractTemplateEn string
|
||||
|
||||
//go:embed contract/de.html
|
||||
var ContractTemplateDe string
|
||||
|
||||
//go:embed certificate/de.html
|
||||
var CertificateTemplateDe string
|
||||
|
||||
//go:embed images/sponsoringheader.base64
|
||||
var ImageSponsoringHeaderBase64 string
|
||||
|
||||
//go:embed images/error.base64
|
||||
var ImageErrorBase64 string
|
||||
|
||||
func GetTemplate(locale, templateName string) (string, error) {
|
||||
switch templateName {
|
||||
case "card":
|
||||
switch locale {
|
||||
case "en":
|
||||
return CardTemplateEn, nil
|
||||
case "de":
|
||||
return CardTemplateDe, nil
|
||||
}
|
||||
case "contract":
|
||||
switch locale {
|
||||
case "en":
|
||||
return ContractTemplateEn, nil
|
||||
case "de":
|
||||
return ContractTemplateDe, nil
|
||||
}
|
||||
case "certificate":
|
||||
switch locale {
|
||||
case "en":
|
||||
return ContractTemplateEn, nil
|
||||
case "de":
|
||||
return CertificateTemplateDe, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("template not found with name %s and locale %s", templateName, locale)
|
||||
}
|
||||
|
||||
func GetImage(imageName string) string {
|
||||
content, err := os.ReadFile("static/images/" + imageName + ".base64")
|
||||
if content == nil || err != nil {
|
||||
log.Printf("error reading image %s: %v", imageName, err)
|
||||
return ImageErrorBase64
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
Reference in New Issue
Block a user