Compare commits

...

79 Commits

Author SHA1 Message Date
niggl 6d2e0241c9 feat(templates): Added selfservice qr
Build release images / build-container (push) Successful in 2m12s
Build latest image / build-container (push) Successful in 2m26s
2025-04-17 21:44:58 +02:00
niggl afc5b1f0c6 fix(models): Add required SelfServiceLink field to RunnerWithDonations struct 2025-04-17 21:43:54 +02:00
niggl 4a76ee469b fix(barcode): Use auto encoding for QR code generation to support all characters 2025-04-17 21:43:44 +02:00
niggl b58bf700df chore(static) Add base64 encoded image for new sponsors
Build latest image / build-container (push) Successful in 2m2s
Build release images / build-container (push) Successful in 2m6s
2025-04-14 18:08:03 +02:00
niggl efd3a35802 fix(templates): Update titles for runner and certificate templates 2025-04-14 17:56:49 +02:00
niggl 0f7e44a42a fix(models): Correct typo in SponsoringReceiptMinimum mapstructure tag
Build latest image / build-container (push) Successful in 2m6s
Build release images / build-container (push) Successful in 2m17s
2025-04-10 15:29:29 +02:00
niggl f90e5d75fa fix(contracts): Minimum was not read correctly
Build release images / build-container (push) Successful in 2m17s
Build latest image / build-container (push) Successful in 2m27s
2025-04-10 15:16:22 +02:00
niggl 31d4ec5f27 fix(templates): Correct spacing in group name display
Build release images / build-container (push) Successful in 1m51s
Build latest image / build-container (push) Successful in 2m13s
2025-03-26 19:29:32 +01:00
niggl d61d4d6e7e refactor(ci): Switch to actions
Build latest image / build-container (push) Successful in 1m39s
2025-03-22 22:48:35 +01:00
niggl 606ce6b940 docs(swagger): Build new docs
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline failed
2024-12-17 17:51:51 +01:00
niggl 750fa70332 feat(models): Support nested groups 2024-12-17 17:51:11 +01:00
niggl 7d503edbc9 feat(templates): Support nested groups 2024-12-17 17:50:54 +01:00
niggl 5c9235df8d fix(templates): Enable blank cards 2024-12-17 17:38:42 +01:00
niggl 11ea0858bb feat(services): Logging
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 16:23:52 +01:00
niggl 4d57cf827d refactor(handler): Move array manipulation
ci/woodpecker/push/build Pipeline was successful
2024-12-17 16:07:40 +01:00
niggl df9f7fdc13 feat(handlers): Added info logging 2024-12-17 16:07:02 +01:00
niggl cdd2b5e250 feat(logging): Debug logging 2024-12-17 15:48:12 +01:00
niggl 94b766f106 feat(logger): Log levels 2024-12-17 15:45:40 +01:00
niggl a2e94f715b refactor(logs): Replaced main logger with zap
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:31:23 +01:00
niggl f64daaf817 feat: Request-IDs for better debugging 2024-12-16 17:23:58 +01:00
niggl b4bb732303 fix(config): typoed defaults
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:02:49 +01:00
niggl 3dee3e72af fix: Bad dependency
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:41:52 +01:00
niggl f5914e5c38 style: Formatting
ci/woodpecker/push/build Pipeline failed
2024-12-16 16:39:56 +01:00
niggl 5a5a7179e9 feat: Use CORS
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline failed
2024-12-16 16:39:02 +01:00
niggl 6c57d63891 fix(config): Added SPONSORING_DISCLAIMER default
ci/woodpecker/push/build Pipeline was successful
2024-12-16 16:31:08 +01:00
niggl b502e2fbd5 fix(config): Defaults for everyone
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:30:02 +01:00
niggl c9475d0093 feat(config): log config on boot
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:17:26 +01:00
niggl 1cc19e0085 fix(config): Default apikey
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:15:11 +01:00
niggl b792806481 docs(swagger): New barcode padding docs
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-12 19:17:29 +01:00
niggl ea6a4a7080 refactor(barcode): Switch to inclusive padding 2024-12-12 19:16:55 +01:00
niggl de6fe4991c feat(barcode): Padding 2024-12-12 19:11:01 +01:00
niggl 1d068b2655 docs(swagger): New swaggerdoc
ci/woodpecker/push/build Pipeline was successful
2024-12-12 18:50:21 +01:00
niggl ef25adf5ed refactor(models): Group now only has a name
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:48:39 +01:00
niggl c09c00ec68 feat(pdfs): Set download names for pdf generation responses
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:24:07 +01:00
niggl 1f4981b0a9 fix(config): Don't fail on missing env
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:18:41 +01:00
niggl c9f28612be docs(README): Updated benchmark section
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:05:52 +01:00
niggl c2d192d8a3 docs(README): Added performance results 2024-12-12 17:59:33 +01:00
niggl 31734596f6 feat(barcode): Implement cache 2024-12-12 17:59:14 +01:00
niggl 850fa0a760 feat(barcode): Baseline for barcode caching 2024-12-12 17:44:41 +01:00
niggl 1c0a9860fa feat(container): Provide default redis 2024-12-12 17:44:26 +01:00
niggl 7c32f1aad2 perf(templates): Cache templates in map 2024-12-12 17:34:55 +01:00
niggl b9e550d6f5 refactor(services): Move staticservice to struct and interface 2024-12-12 17:11:59 +01:00
niggl 2a4f126377 feat(barcodes): Make width and height configureable 2024-12-12 17:03:10 +01:00
niggl 5c932158e9 refactor(handlers): Use shared gotenberg 2024-12-12 16:53:51 +01:00
niggl 1296c9e399 refactor(pdf): Share templater 2024-12-12 16:52:06 +01:00
niggl 0f2452eca0 feat(handlers): Implement barcode generation 2024-12-12 16:49:33 +01:00
niggl b6cc98a165 refactor(services): Extract barcode generation 2024-12-12 16:43:14 +01:00
niggl c5da33f10f chore: Formatting 2024-12-12 16:33:23 +01:00
niggl 69f4de6739 docs(swagger): Move security annotations 2024-12-12 16:33:10 +01:00
niggl 649ac2a3c2 docs(swagger): Added barcode generation docs 2024-12-12 16:31:07 +01:00
niggl 5587fdaaa8 feat(barcodes): Baseline for implementation 2024-12-12 16:30:46 +01:00
niggl 8f676f08a9 refactor: Switch to pdf subpath 2024-12-12 16:28:06 +01:00
niggl 28de60d375 Merge pull request 'go' (#49) from go into main
Reviewed-on: #49
Reviewed-by: Philipp Dormann <philipp@noreply.git.odit.services>
2024-12-12 15:11:30 +00:00
niggl d19029b5ad docs(swagger): Updated swagger metadata 2024-12-11 19:45:05 +01:00
niggl 1dfd96869d docs(swagger): Swagger auth 2024-12-11 19:39:18 +01:00
niggl a1ba28cacb docs(dev): Updated air commands 2024-12-11 19:33:06 +01:00
niggl d2bace87af fix(dev): Add air config for linux/macos 2024-12-11 19:31:48 +01:00
niggl 9d507b9572 feat(ci): Added ci config 2024-12-11 19:30:02 +01:00
niggl e89c17806f docs: Updated readme 2024-12-11 19:29:04 +01:00
niggl 924f76a100 refactor(images): Move sponsor images to folder and load them dynamicly 2024-12-11 19:22:50 +01:00
niggl 99ec0933ea feat(container): New dev compose with just external services 2024-12-11 19:21:32 +01:00
niggl cceca7f5e1 feat(container): Deliver default static stuff 2024-12-11 18:51:37 +01:00
niggl 54d294a8b4 feat(container): Added document-server 2024-12-11 18:51:06 +01:00
niggl 41291b9200 refactor(config): Load gotenberg url from env 2024-12-11 18:45:53 +01:00
niggl 4faf76a073 feat: Config from env 2024-12-11 18:30:46 +01:00
niggl 715eb8e1cb chore(models): Removed unused param 2024-12-11 18:07:52 +01:00
niggl 2686bee1d1 refactor(auth): Switch to query auth for full link support (to be fixed in future) 2024-12-11 17:59:11 +01:00
niggl 57a3777891 fix(templates): Fix string style 2024-12-11 17:56:24 +01:00
niggl f6dc33edb4 feat(v1): header auth for all endpoints 2024-12-11 17:55:29 +01:00
niggl e2cd445aeb feat(docs): Annotated required params 2024-12-11 17:45:51 +01:00
niggl f5debf58fc feat(docs): New swagger doc 2024-12-09 17:29:31 +01:00
niggl 11a9b51197 refactor(templates): Dynamic image loading 2024-12-09 17:28:53 +01:00
niggl c5dc4f7e79 refactor(templates): Dynamic template loading 2024-12-09 17:22:37 +01:00
niggl f9f30e96c7 refactor(templater): Format decimals according to locale 2024-12-09 17:15:06 +01:00
niggl eff3354867 feat(templates): English certificate template 2024-12-09 17:13:01 +01:00
niggl 2bfff006ed fix(templates): Added missing base64 image 2024-12-09 17:10:13 +01:00
niggl 53eab5db94 feat(certificate): Implement adding up 2024-12-09 17:08:01 +01:00
niggl af73b35b18 fix(services): Updated templater comma logic 2024-12-09 17:03:13 +01:00
niggl 692f378eab feat(templater): Implement unit formats 2024-12-09 16:59:39 +01:00
49 changed files with 1984 additions and 378 deletions
+46
View File
@@ -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
View File
+1 -1
View File
@@ -1,3 +1,3 @@
tmp
docker-compose.yaml
.air.toml
.air*.toml
+19
View File
@@ -0,0 +1,19 @@
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!
+27
View File
@@ -0,0 +1,27 @@
name: Build latest image
on:
push:
branches:
- main
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/document-server:latest
platforms: linux/amd64,linux/arm64
+27
View File
@@ -0,0 +1,27 @@
name: Build release images
on:
push:
tags:
- "*.*.*"
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/document-server:${{ github.ref_name }}
platforms: linux/amd64,linux/arm64
+1
View File
@@ -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" ]
+69 -4
View File
@@ -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 |
+9
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+23 -1
View File
@@ -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
)
+57
View File
@@ -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=
+73
View File
@@ -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
View File
@@ -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)
}
+44 -22
View File
@@ -1,73 +1,95 @@
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,
}
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
}
}
return runners
}
+34 -27
View File
@@ -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)
}
+24
View File
@@ -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
View File
@@ -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")
}
+166 -17
View File
@@ -1,45 +1,194 @@
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("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", "")
// 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
View File
@@ -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 {
+21 -19
View File
@@ -1,35 +1,36 @@
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"`
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 +38,5 @@ type CertificateTemplateOptions struct {
EventName string `json:"event_name"`
Footer string `json:"footer"`
CurrencySymbol string `json:"currency_symbol"`
Locale string `json:"locale"`
}
+20
View File
@@ -0,0 +1,20 @@
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"`
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"`
}
+12 -12
View File
@@ -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"`
}
+129
View File
@@ -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)
}
+24
View File
@@ -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
}
+40 -45
View File
@@ -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) {
@@ -36,63 +36,58 @@ func idToEan13(id string, prefix string) (string, error) {
}
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
var generatedCode barcode.Barcode
var err error
switch format {
case "ean13":
encodedEan, err := idToEan13(code, prefix)
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,
}).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>
+119
View File
@@ -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,{{ barcode ".SelfServiceLink" "qr" "" }}" style="height: 2.5cm; padding: 0.2cm"> -->
</td>
</tr>
</tbody>
</table>
<p style="text-align: center;">
{{ $.Footer }}
</p>
</footer>
</article>
{{ end }}
</body>
</html>
+120
View File
@@ -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</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,{{ barcode ".SelfServiceLink" "qr" "" }}" style="height: 2.5cm; padding: 0.2cm"> -->
</td>
</tr>
</tbody>
</table>
<p>
{{ $.Footer }}
</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

-65
View File
@@ -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)
}