Compare commits

..
61 Commits
Author SHA1 Message Date
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
niggl 12f61e373f fix(certificate): Expose german template 2024-12-09 16:48:59 +01:00
niggl 79a0062f60 feat(certificates): Endpoint with handler 2024-12-09 16:48:06 +01:00
niggl 561e7151c6 feat(certificate): German template 2024-12-09 16:44:46 +01:00
niggl 2108c88001 feat(certificates): Base types 2024-12-09 16:36:14 +01:00
niggl 2537235ce6 feat(static): Added more sponsor logos 2024-12-03 20:00:42 +01:00
niggl 4deda8adf1 feat(templates): Get static mounted images 2024-12-03 19:56:57 +01:00
niggl d910a722df feat(templates): Fallback image 2024-12-03 19:48:08 +01:00
45 changed files with 1795 additions and 195 deletions

No files matched your search

+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
File renamed without changes.
+1 -1
View File
@@ -1,3 +1,3 @@
tmp tmp
docker-compose.yaml docker-compose.yaml
.air.toml .air*.toml
+18
View File
@@ -0,0 +1,18 @@
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=
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!
+18
View File
@@ -0,0 +1,18 @@
steps:
- name: build latest
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/document-server
tags:
- latest
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/document-server:latest
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
branch: main
when:
event: push
+17
View File
@@ -0,0 +1,17 @@
steps:
- name: build tag
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/document-server
tags:
- "${CI_COMMIT_TAG}"
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/document-server:latest
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
event:
- tag
+1
View File
@@ -10,5 +10,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o server
FROM scratch FROM scratch
COPY --from=builder /app/server /server COPY --from=builder /app/server /server
COPY static /static
ADD https://curl.haxx.se/ca/cacert.pem /etc/ssl/certs/ca-certificates.crt ADD https://curl.haxx.se/ca/cacert.pem /etc/ssl/certs/ca-certificates.crt
ENTRYPOINT [ "/server" ] ENTRYPOINT [ "/server" ]
+69 -4
View File
@@ -2,7 +2,7 @@
## Features ## Features
- 📝 HTML pdf templates - 📝 HTML templates for pdf generation
- 📚 OpenAPI/Swagger documentation - 📚 OpenAPI/Swagger documentation
- ⚡ High-performance with go and gotenberg - ⚡ High-performance with go and gotenberg
@@ -12,9 +12,6 @@
# Install dependencies # Install dependencies
go mod download go mod download
# Generate the swagger docs
swag init
# Run the server # Run the server
air air
``` ```
@@ -29,3 +26,71 @@ The project uses:
- 🏃‍♂️ go as the language and build tool - 🏃‍♂️ go as the language and build tool
- 🌐 gofiber for the web framework - 🌐 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: services:
document-server:
build: .
ports:
- "3000:3000"
volumes:
- ./docker.env:/.env
gotenberg: gotenberg:
image: gotenberg/gotenberg:8 image: gotenberg/gotenberg:8
ports: 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!
+256 -10
View File
@@ -9,14 +9,88 @@ const docTemplate = `{
"info": { "info": {
"description": "{{escape .Description}}", "description": "{{escape .Description}}",
"title": "{{.Title}}", "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}}" "version": "{{.Version}}"
}, },
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "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": { "post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate cards based on the provided data", "description": "Generate cards based on the provided data",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -25,7 +99,7 @@ const docTemplate = `{
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"cards" "pdfs"
], ],
"summary": "Generate runner cards", "summary": "Generate runner cards",
"parameters": [ "parameters": [
@@ -42,8 +116,45 @@ const docTemplate = `{
"responses": {} "responses": {}
} }
}, },
"/contracts": { "/v1/pdfs/certificates": {
"post": { "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", "description": "Generate a contract based on the provided data",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -52,7 +163,7 @@ const docTemplate = `{
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"contracts" "pdfs"
], ],
"summary": "Generate a contract", "summary": "Generate a contract",
"parameters": [ "parameters": [
@@ -73,12 +184,18 @@ const docTemplate = `{
"definitions": { "definitions": {
"models.Card": { "models.Card": {
"type": "object", "type": "object",
"required": [
"code",
"id",
"runner"
],
"properties": { "properties": {
"code": { "code": {
"type": "string" "type": "string"
}, },
"enabled": { "enabled": {
"type": "boolean" "type": "boolean",
"default": true
}, },
"id": { "id": {
"type": "integer" "type": "integer"
@@ -90,6 +207,10 @@ const docTemplate = `{
}, },
"models.CardRequest": { "models.CardRequest": {
"type": "object", "type": "object",
"required": [
"cards",
"locale"
],
"properties": { "properties": {
"cards": { "cards": {
"type": "array", "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": { "models.ContractRequest": {
"type": "object", "type": "object",
"required": [
"locale",
"runners"
],
"properties": { "properties": {
"locale": { "locale": {
"type": "string", "type": "string",
@@ -124,22 +271,72 @@ const docTemplate = `{
} }
} }
}, },
"models.Group": { "models.DistanceDonation": {
"type": "object", "type": "object",
"required": [
"amount_per_distance",
"donor",
"id"
],
"properties": { "properties": {
"amount": {
"type": "integer"
},
"amount_per_distance": {
"type": "integer"
},
"donor": {
"$ref": "#/definitions/models.Donor"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"name": { "paid_amount": {
"type": "integer"
}
}
},
"models.Donor": {
"type": "object",
"required": [
"first_name",
"id",
"last_name"
],
"properties": {
"first_name": {
"type": "string" "type": "string"
}, },
"parent_group": { "id": {
"$ref": "#/definitions/models.Group" "type": "integer"
},
"last_name": {
"type": "string"
},
"middle_name": {
"type": "string"
}
}
},
"models.Group": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
} }
} }
}, },
"models.Runner": { "models.Runner": {
"type": "object", "type": "object",
"required": [
"first_name",
"group",
"id",
"last_name"
],
"properties": { "properties": {
"first_name": { "first_name": {
"type": "string" "type": "string"
@@ -157,6 +354,55 @@ const docTemplate = `{
"type": "string" "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"
} }
} }
}` }`
+256 -10
View File
@@ -3,11 +3,85 @@
"info": { "info": {
"description": "This is the API documentation for the LfK Document Server - a tool for pdf generation.", "description": "This is the API documentation for the LfK Document Server - a tool for pdf generation.",
"title": "LfK Document Server API", "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": { "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": { "post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate cards based on the provided data", "description": "Generate cards based on the provided data",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -16,7 +90,7 @@
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"cards" "pdfs"
], ],
"summary": "Generate runner cards", "summary": "Generate runner cards",
"parameters": [ "parameters": [
@@ -33,8 +107,45 @@
"responses": {} "responses": {}
} }
}, },
"/contracts": { "/v1/pdfs/certificates": {
"post": { "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", "description": "Generate a contract based on the provided data",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -43,7 +154,7 @@
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"contracts" "pdfs"
], ],
"summary": "Generate a contract", "summary": "Generate a contract",
"parameters": [ "parameters": [
@@ -64,12 +175,18 @@
"definitions": { "definitions": {
"models.Card": { "models.Card": {
"type": "object", "type": "object",
"required": [
"code",
"id",
"runner"
],
"properties": { "properties": {
"code": { "code": {
"type": "string" "type": "string"
}, },
"enabled": { "enabled": {
"type": "boolean" "type": "boolean",
"default": true
}, },
"id": { "id": {
"type": "integer" "type": "integer"
@@ -81,6 +198,10 @@
}, },
"models.CardRequest": { "models.CardRequest": {
"type": "object", "type": "object",
"required": [
"cards",
"locale"
],
"properties": { "properties": {
"cards": { "cards": {
"type": "array", "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": { "models.ContractRequest": {
"type": "object", "type": "object",
"required": [
"locale",
"runners"
],
"properties": { "properties": {
"locale": { "locale": {
"type": "string", "type": "string",
@@ -115,22 +262,72 @@
} }
} }
}, },
"models.Group": { "models.DistanceDonation": {
"type": "object", "type": "object",
"required": [
"amount_per_distance",
"donor",
"id"
],
"properties": { "properties": {
"amount": {
"type": "integer"
},
"amount_per_distance": {
"type": "integer"
},
"donor": {
"$ref": "#/definitions/models.Donor"
},
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"name": { "paid_amount": {
"type": "integer"
}
}
},
"models.Donor": {
"type": "object",
"required": [
"first_name",
"id",
"last_name"
],
"properties": {
"first_name": {
"type": "string" "type": "string"
}, },
"parent_group": { "id": {
"$ref": "#/definitions/models.Group" "type": "integer"
},
"last_name": {
"type": "string"
},
"middle_name": {
"type": "string"
}
}
},
"models.Group": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
} }
} }
}, },
"models.Runner": { "models.Runner": {
"type": "object", "type": "object",
"required": [
"first_name",
"group",
"id",
"last_name"
],
"properties": { "properties": {
"first_name": { "first_name": {
"type": "string" "type": "string"
@@ -148,6 +345,55 @@
"type": "string" "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"
} }
} }
} }
+179 -9
View File
@@ -4,11 +4,16 @@ definitions:
code: code:
type: string type: string
enabled: enabled:
default: true
type: boolean type: boolean
id: id:
type: integer type: integer
runner: runner:
$ref: '#/definitions/models.Runner' $ref: '#/definitions/models.Runner'
required:
- code
- id
- runner
type: object type: object
models.CardRequest: models.CardRequest:
properties: properties:
@@ -21,6 +26,24 @@ definitions:
- en - en
- de - de
type: string 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 type: object
models.ContractRequest: models.ContractRequest:
properties: properties:
@@ -33,15 +56,48 @@ definitions:
items: items:
$ref: '#/definitions/models.Runner' $ref: '#/definitions/models.Runner'
type: array 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 type: object
models.Group: models.Group:
properties: properties:
id:
type: integer
name: name:
type: string type: string
parent_group: required:
$ref: '#/definitions/models.Group' - name
type: object type: object
models.Runner: models.Runner:
properties: properties:
@@ -55,14 +111,99 @@ definitions:
type: string type: string
middle_name: middle_name:
type: string 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 type: object
info: 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 description: This is the API documentation for the LfK Document Server - a tool
for pdf generation. for pdf generation.
license:
name: CC BY-NC-SA 4.0
termsOfService: https://lauf-fuer-kaya.de/datenschutz
title: LfK Document Server API title: LfK Document Server API
paths: 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: post:
consumes: consumes:
- application/json - application/json
@@ -77,10 +218,32 @@ paths:
produces: produces:
- application/pdf - application/pdf
responses: {} responses: {}
security:
- ApiKeyAuth: []
summary: Generate runner cards summary: Generate runner cards
tags: tags:
- cards - pdfs
/contracts: /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: post:
consumes: consumes:
- application/json - application/json
@@ -95,7 +258,14 @@ paths:
produces: produces:
- application/pdf - application/pdf
responses: {} responses: {}
security:
- ApiKeyAuth: []
summary: Generate a contract summary: Generate a contract
tags: tags:
- contracts - pdfs
securityDefinitions:
ApiKeyAuth:
in: query
name: key
type: apiKey
swagger: "2.0" swagger: "2.0"
+22 -1
View File
@@ -14,26 +14,47 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect
github.com/boombuler/barcode v1.0.2 // 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/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference 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/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/google/uuid v1.6.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/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.11 // 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/mailru/easyjson v0.7.7 // indirect
github.com/makiuchi-d/gozxing v0.1.1 // indirect github.com/makiuchi-d/gozxing v0.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // 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/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/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/swaggo/files/v2 v2.0.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.57.0 // indirect github.com/valyala/fasthttp v1.57.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.27.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 gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+53
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/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 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 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= 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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= 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-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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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 h1:4WzMzgExTgBfuUQ/HegMf+jcHtH+c3fl7eySUQUbfzg=
github.com/oxplot/papersizes v0.0.0-20181201065918-90a3a5ae1915/go.mod h1:LJRTnhoARxQgMyT7T9L+ZzwR4OrmyHTy5LPxZEzE1CM= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:XCVJO/i/VosCDsJu1YLpdejGsGnBE9deRMpjN4pJLHk=
github.com/swaggo/files/v2 v2.0.1/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/files/v2 v2.0.1/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
@@ -62,6 +103,12 @@ 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/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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 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=
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 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
@@ -70,12 +117,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.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 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 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 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-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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+64
View File
@@ -0,0 +1,64 @@
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 {
// 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 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid width parameter",
})
}
height, err := strconv.Atoi(heightStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid height parameter",
})
}
padding, err := strconv.Atoi(paddingStr)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid padding parameter",
})
}
// Generate the barcode
barcode, err := h.BarcodeService.GenerateBarcode(barcodeType, barcodeContent, width, height, padding)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "image/png")
return c.Send(barcode.Bytes())
}
+20 -20
View File
@@ -5,20 +5,20 @@ import (
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"git.odit.services/lfk/document-server/templates"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// GenerateCard godoc // GenerateCard godoc
// @Summary Generate runner cards //
// @Description Generate cards based on the provided data // @Summary Generate runner cards
// @Tags cards // @Description Generate cards based on the provided data
// @Accept json // @Tags pdfs
// @Param data body models.CardRequest true "Card data" // @Accept json
// @Produce application/pdf // @Param data body models.CardRequest true "Card data"
// @Router /cards [post] // @Produce application/pdf
func GenerateCard(c *fiber.Ctx) error { // @Security ApiKeyAuth
// @Router /v1/pdfs/cards [post]
func (h *DefaultHandler) GenerateCard(c *fiber.Ctx) error {
cardRequest := new(models.CardRequest) cardRequest := new(models.CardRequest)
if err := c.BodyParser(cardRequest); err != nil { if err := c.BodyParser(cardRequest); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -31,15 +31,14 @@ func GenerateCard(c *fiber.Ctx) error {
}) })
} }
generator := services.DefaultTemplater{} templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
templateString, err := templates.GetTemplate(cardRequest.Locale, "card")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := generator.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
@@ -48,21 +47,21 @@ func GenerateCard(c *fiber.Ctx) error {
genConfig := &models.CardTemplateOptions{ genConfig := &models.CardTemplateOptions{
CardSegments: splitCardSegments(cardRequest.Cards), CardSegments: splitCardSegments(cardRequest.Cards),
EventName: "Event name", EventName: h.Config.EventName,
CardSubtitle: "Card subtitle", CardSubtitle: h.Config.CardSubtitle,
BarcodeFormat: "ean13", BarcodeFormat: h.Config.CardBarcodeFormat,
BarcodePrefix: "", BarcodePrefix: h.Config.CardBarcodePrefix,
} }
result, err := generator.Execute(template, genConfig) result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
c.Set(fiber.HeaderContentType, "text/html") c.Set(fiber.HeaderContentType, "text/html")
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"}
pdf, err := converter.ToPdf(result, "a4", false) pdf, err := h.Converter.ToPdf(result, "a4", false)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
@@ -70,6 +69,7 @@ func GenerateCard(c *fiber.Ctx) error {
} }
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")
return c.Send(pdf) return c.Send(pdf)
} }
+84
View File
@@ -0,0 +1,84 @@
package handlers
import (
"log"
"slices"
"git.odit.services/lfk/document-server/models"
"github.com/gofiber/fiber/v2"
)
// GenerateCertificate godoc
//
// @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 {
certificateRequest := new(models.CertificateRequest)
if err := c.BodyParser(certificateRequest); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(),
})
}
if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale",
})
}
templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
if err != nil {
log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found",
})
}
template, err := h.Templater.StringToTemplate(templateString)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
genConfig := &models.CertificateTemplateOptions{
Runners: addUpRunnerDonations(certificateRequest.Runners),
EventName: h.Config.EventName,
Footer: h.Config.CertificateFooter,
CurrencySymbol: h.Config.CurrencySymbol,
Locale: certificateRequest.Locale,
}
result, err := h.Templater.Execute(template, genConfig)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
c.Set(fiber.HeaderContentType, "text/html")
pdf, err := h.Converter.ToPdf(result, "a4", false)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
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
}
+21 -23
View File
@@ -5,20 +5,20 @@ import (
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"git.odit.services/lfk/document-server/templates"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// GenerateContract godoc // GenerateContract godoc
// @Summary Generate a contract //
// @Description Generate a contract based on the provided data // @Summary Generate a contract
// @Tags contracts // @Description Generate a contract based on the provided data
// @Accept json // @Tags pdfs
// @Param data body models.ContractRequest true "Contract data" // @Accept json
// @Produce application/pdf // @Param data body models.ContractRequest true "Contract data"
// @Router /contracts [post] // @Produce application/pdf
func GenerateContract(c *fiber.Ctx) error { // @Security ApiKeyAuth
// @Router /v1/pdfs/contracts [post]
func (h *DefaultHandler) GenerateContract(c *fiber.Ctx) error {
contract := new(models.ContractRequest) contract := new(models.ContractRequest)
if err := c.BodyParser(contract); err != nil { if err := c.BodyParser(contract); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -33,15 +33,14 @@ func GenerateContract(c *fiber.Ctx) error {
contract.Runners = repeatRunnerArrayItems(contract.Runners, 2) contract.Runners = repeatRunnerArrayItems(contract.Runners, 2)
generator := services.DefaultTemplater{} templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
templateString, err := templates.GetTemplate(contract.Locale, "contract")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := generator.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
@@ -50,24 +49,22 @@ func GenerateContract(c *fiber.Ctx) error {
genConfig := &models.ContractTemplateOptions{ genConfig := &models.ContractTemplateOptions{
Runners: contract.Runners, Runners: contract.Runners,
CurrencySymbol: "€", CurrencySymbol: h.Config.CurrencySymbol,
Disclaimer: "This is a disclaimer", Disclaimer: h.Config.SponosringDisclaimer,
ReceiptMinimumAmount: 10, ReceiptMinimumAmount: h.Config.SponsoringReceiptMinimum,
EventName: "Event name", EventName: h.Config.EventName,
SponsoringHeader: templates.ImageSponsoringHeaderBase64, BarcodeFormat: h.Config.SponsoringBarcodeFormat,
BarcodeFormat: "ean13", BarcodePrefix: h.Config.SponsoringBarcodePrefix,
BarcodePrefix: "1",
} }
result, err := generator.Execute(template, genConfig) result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"} pdf, err := h.Converter.ToPdf(result, "a5", true)
pdf, err := converter.ToPdf(result, "a5", true)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
@@ -75,6 +72,7 @@ func GenerateContract(c *fiber.Ctx) error {
} }
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")
return c.Send(pdf) return c.Send(pdf)
} }
+22
View File
@@ -0,0 +1,22 @@
package handlers
import (
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2"
)
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
}
+1 -1
View File
@@ -2,6 +2,6 @@ package handlers
import "github.com/gofiber/fiber/v2" 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") return c.Status(404).SendString("Not Found")
} }
+112 -15
View File
@@ -1,29 +1,120 @@
package main package main
import ( import (
"flag" "crypto/sha256"
"crypto/subtle"
"log" "log"
"git.odit.services/lfk/document-server/docs" // Correct import path for docs "git.odit.services/lfk/document-server/docs" // Correct import path for docs
"git.odit.services/lfk/document-server/handlers" "git.odit.services/lfk/document-server/handlers"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/keyauth"
"github.com/gofiber/swagger" "github.com/gofiber/swagger"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
) )
var ( var (
port = flag.String("port", ":3000", "Port to listen on") config *models.Config
prod = flag.Bool("prod", false, "Enable prefork in Production")
) )
// @title LfK Document Server API func validateAPIKey(c *fiber.Ctx, key string) (bool, error) {
// @description This is the API documentation for the LfK Document Server - a tool for pdf generation. 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("PRODUCION", true)
viper.SetDefault("PORT", "3000")
viper.SetDefault("APIKEY", "lfk")
viper.SetDefault("EVENT_NAME", "Demo Event")
viper.SetDefault("CURRENCY_SYMBOL", "€")
viper.SetDefault("CARD_SUBTITLE", "Runner Card")
viper.SetDefault("CARD_BARCODEFORMAT", "ean13")
viper.SetDefault("CARD_BARCODEPREFIX", "")
viper.SetDefault("SPONSORING_RECEIPTMINIMUM", 0)
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 {
log.Println("No .env file found")
}
// Unmarshal the config from file and env into the config struct
err = viper.Unmarshal(&config)
if err != nil {
return err
}
log.Printf("Loaded config: %+v\n", config)
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() { func main() {
// Parse command-line flags
flag.Parse() err := loadEnv()
if err != nil {
log.Fatal(err)
}
var redisClient *redis.Client
if config.RedisAddr != "" {
log.Println("Using redis at", config.RedisAddr)
redisClient = redis.NewClient(&redis.Options{
Addr: config.RedisAddr,
})
}
barcodeGenerator := &services.DefaultBarcodeService{
RedisClient: redisClient,
}
staticService := &services.DefaultStaticService{
Cache: make(map[string]string),
}
handler := handlers.DefaultHandler{
Config: config,
BarcodeService: barcodeGenerator,
StaticService: staticService,
Templater: &services.DefaultTemplater{
BarcodeService: barcodeGenerator,
StaticService: staticService,
},
Converter: &services.GotenbergConverter{
BaseUrl: config.GotenbergBaseUrl,
},
}
// Create a new Fiber instance // Create a new Fiber instance
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Prefork: *prod, Prefork: config.Prod,
}) })
// Swagger documentation route // Swagger documentation route
@@ -31,14 +122,20 @@ func main() {
v1 := app.Group("/v1") v1 := app.Group("/v1")
v1.Get("/", func(c *fiber.Ctx) error { pdfv1 := v1.Group("/pdfs")
return c.SendString("Hello, World!") pdfv1.Use(keyauth.New(keyauth.Config{
}) KeyLookup: "query:key",
v1.Post("/contracts", handlers.GenerateContract) Validator: validateAPIKey,
v1.Post("/cards", handlers.GenerateCard) }))
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)
app.Use(handler.NotFoundHandler)
docs.SwaggerInfo.BasePath = "/" docs.SwaggerInfo.BasePath = "/"
log.Fatal(app.Listen(*port)) log.Fatal(app.Listen("0.0.0.0:" + config.Port))
} }
+6 -6
View File
@@ -1,15 +1,15 @@
package models package models
type CardRequest struct { type CardRequest struct {
Cards []Card `json:"cards"` Cards []Card `json:"cards" validate:"required"`
Locale string `json:"locale" enums:"en,de"` Locale string `json:"locale" enums:"en,de" validate:"required"`
} }
type Card struct { type Card struct {
ID int `json:"id"` ID int `json:"id" validate:"required"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled" default:"true"`
Runner Runner `json:"runner"` Runner Runner `json:"runner" validate:"required"`
Code string `json:"code"` Code string `json:"code" validate:"required"`
} }
type CardTemplateOptions struct { type CardTemplateOptions struct {
+41
View File
@@ -0,0 +1,41 @@
package models
type CertificateRequest struct {
Runners []RunnerWithDonations `json:"runners" validate:"required"`
Locale string `json:"locale" enums:"en,de" validate:"required"`
}
type RunnerWithDonations struct {
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"`
}
type DistanceDonation struct {
ID int `json:"id" validate:"required"`
Amount int `json:"amount"`
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" 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 {
Runners []RunnerWithDonations `json:"runners"`
EventName string `json:"event_name"`
Footer string `json:"footer"`
CurrencySymbol string `json:"currency_symbol"`
Locale string `json:"locale"`
}
+19
View File
@@ -0,0 +1,19 @@
package models
type Config struct {
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 int `mapstructure:"SPONSOING_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"`
}
+8 -11
View File
@@ -1,22 +1,20 @@
package models package models
type ContractRequest struct { type ContractRequest struct {
Runners []Runner `json:"runners"` Runners []Runner `json:"runners" validate:"required"`
Locale string `json:"locale" enums:"en,de"` Locale string `json:"locale" enums:"en,de" validate:"required"`
} }
type Runner struct { type Runner struct {
ID int `json:"id"` ID int `json:"id" validate:"required"`
FirstName string `json:"first_name"` FirstName string `json:"first_name" validate:"required"`
MiddleName string `json:"middle_name"` MiddleName string `json:"middle_name" validate:"optional"`
LastName string `json:"last_name"` LastName string `json:"last_name" validate:"required"`
Group Group `json:"group"` Group Group `json:"group" validate:"required"`
} }
type Group struct { type Group struct {
ID int `json:"id"` Name string `json:"name" validate:"required"`
Name string `json:"name"`
ParentGroup *Group `json:"parent_group"`
} }
type ContractTemplateOptions struct { type ContractTemplateOptions struct {
@@ -25,7 +23,6 @@ type ContractTemplateOptions struct {
Disclaimer string `json:"disclaimer"` Disclaimer string `json:"disclaimer"`
ReceiptMinimumAmount int `json:"receipt_minimum_amount"` ReceiptMinimumAmount int `json:"receipt_minimum_amount"`
EventName string `json:"event_name"` EventName string `json:"event_name"`
SponsoringHeader string `json:"base_url"`
BarcodeFormat string `json:"barcode_format"` BarcodeFormat string `json:"barcode_format"`
BarcodePrefix string `json:"barcode_prefix"` BarcodePrefix string `json:"barcode_prefix"`
} }
+109
View File
@@ -0,0 +1,109 @@
package services
import (
"bytes"
"context"
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"log"
"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"
)
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
}
func (b *DefaultBarcodeService) GenerateBarcode(format string, content string, width int, height int, padding int) (bytes.Buffer, error) {
ctx := context.Background()
if !b.IsTypeSupported(format) {
return bytes.Buffer{}, fmt.Errorf("unsupported barcode type: %s", format)
}
if b.RedisClient != nil {
cachedBarcode, err := b.RedisClient.Get(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d:%d", format, content, width, height, padding)).Result()
if err == nil {
log.Printf("Cache hit for barcode:%s:%s:%d:%d", format, content, width, height)
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":
generatedCode, err = qr.Encode(content, qr.M, qr.AlphaNumeric)
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)
// 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 {
return bytes.Buffer{}, err
}
// 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)
var buf bytes.Buffer
err = png.Encode(&buf, bg)
if err != nil {
return bytes.Buffer{}, err
}
if b.RedisClient != nil {
err = b.RedisClient.Set(ctx, fmt.Sprintf("barcode:%s:%s:%d:%d", format, content, width, height), buf.String(), 10*time.Minute).Err()
if err != nil {
return bytes.Buffer{}, err
}
}
return buf, nil
}
func (b *DefaultBarcodeService) IsTypeSupported(format string) bool {
supportedTypes := []string{"ean13", "code128", "qr"}
return slices.Contains(supportedTypes, format)
}
+39 -40
View File
@@ -4,13 +4,9 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"html/template" "html/template"
"image/png" "strings"
"github.com/boombuler/barcode"
"github.com/boombuler/barcode/code128"
"github.com/boombuler/barcode/ean"
"github.com/boombuler/barcode/qr"
) )
type Templater interface { type Templater interface {
@@ -19,6 +15,8 @@ type Templater interface {
} }
type DefaultTemplater struct { type DefaultTemplater struct {
BarcodeService BarcodeService
StaticService StaticService
} }
func idToEan13(id string, prefix string) (string, error) { func idToEan13(id string, prefix string) (string, error) {
@@ -35,54 +33,55 @@ func idToEan13(id string, prefix string) (string, error) {
} }
func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) { func (t *DefaultTemplater) GenerateBarcode(code string, format string, prefix string) (string, error) {
var generatedCode barcode.Barcode
var err error var err error
switch format { if format == "ean13" {
case "ean13": code, err = idToEan13(code, prefix)
encodedEan, err := idToEan13(code, prefix)
if err != nil { if err != nil {
return "", err return "", err
} }
generatedCode, err = ean.Encode(encodedEan) }
if err != nil {
return "", err buf, err := t.BarcodeService.GenerateBarcode(format, code, 1000, 500, 0)
}
break return base64.StdEncoding.EncodeToString(buf.Bytes()), err
case "code128": }
generatedCode, err = code128.Encode(prefix + code)
if err != nil { func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
return "", err sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
} if err != nil {
break return "", err
case "qr": }
generatedCode, err = qr.Encode(prefix+code, qr.M, qr.AlphaNumeric) return t.StaticService.GetImage("sponsors/" + strings.TrimSuffix(sponsors[id%len(sponsors)], ".base64")), nil
if err != nil { }
return "", err
} func (t *DefaultTemplater) LoadImage(name string) (string, error) {
break 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: default:
return "", errors.New("unknown barcode format") return "", errors.New("unknown unit")
} }
scaledCode, err := barcode.Scale(generatedCode, 1000, 500) if locale == "de" {
if err != nil { return strings.Replace(formatted, ".", ",", -1), nil
return "", err
} }
return formatted, nil
var buf bytes.Buffer
err = png.Encode(&buf, scaledCode)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
} }
func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Template, error) { func (t *DefaultTemplater) StringToTemplate(templateString string) (*template.Template, error) {
return template.New("template").Funcs(template.FuncMap{ return template.New("template").Funcs(template.FuncMap{
"barcode": t.GenerateBarcode, "barcode": t.GenerateBarcode,
"sponsorLogo": func(id int) string { return "a" }, "sponsorLogo": t.SelectSponsorImage,
"formatUnit": t.FormatUnit,
"loadImage": t.LoadImage,
}).Parse(templateString) }).Parse(templateString)
} }
+63
View File
@@ -0,0 +1,63 @@
package services
import (
_ "embed"
"fmt"
"log"
"os"
)
type StaticService interface {
GetTemplate(locale, templateName string) (string, error)
ListFilesInStaticSubFolder(folderName string) ([]string, error)
GetImage(imageName string) string
}
type DefaultStaticService struct {
Cache map[string]string
}
func (s *DefaultStaticService) GetTemplate(locale, templateName string) (string, error) {
if s.Cache[locale+templateName] != "" {
log.Printf("returning cached template %s with locale %s", templateName, locale)
return s.Cache[locale+templateName], nil
}
content, err := os.ReadFile(fmt.Sprintf("static/templates/%s/%s.html", templateName, locale))
if content == nil || err != nil {
log.Printf("error reading template %s with locale %s: %v", templateName, locale, err)
return "", err
}
s.Cache[locale+templateName] = string(content)
return string(content), nil
}
func (s *DefaultStaticService) ListFilesInStaticSubFolder(folderName string) ([]string, error) {
files, err := os.ReadDir(fmt.Sprintf("static/%s", folderName))
if err != nil {
log.Printf("error reading files from folder %s: %v", folderName, err)
return nil, err
}
var images []string
for _, file := range files {
if file.IsDir() {
continue
}
images = append(images, file.Name())
}
return images, nil
}
func (s *DefaultStaticService) 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)
}
const ImageErrorBase64 string = "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 truncated
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAAqsAAASrCAYAAAC1y/HLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFHGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTAzLTMxVDEzOjUzOjUyKzAyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wMy0zMVQxNzowMzo1OCswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wMy0zMVQxNzowMzo1OCswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo2MjMzNzM0My1jYjBj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 truncated
+1
View File
@@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAApQAAACWCAYAAABgmBgeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAFHGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTAzLTMxVDE0OjEyOjMzKzAyOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0wMy0zMVQxNDoxMzozMyswMjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0wMy0zMVQxNDoxMzozMyswMjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpmZDhjZTFlZi1lMTAy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 truncated
+1
View File
@@ -0,0 +1 @@
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 truncated
+1
View File
@@ -0,0 +1 @@
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 truncated
+1
View File
@@ -0,0 +1 @@
/9j/4AAQSkZJRgABAQEBLAEsAAD/7RSMUGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAACgcAVoAAxslRxwCAAACAAAcAgUAFExvZ29fU19TcGsgLSBSR0IuY2RyOEJJTQQlAAAAAAAQcF7Dz6j2UhGWN+QGK+9V2zhCSU0EOgAAAAABdQAAABAAAAABAAAAAAALcHJpbnRPdXRwdXQAAAAHAAAAAENsclNlbnVtAAAAAENsclMAAAAAUkdCQwAAAABObSAgVEVYVAAAAB0AQwBhAG4AbwBuACAASABEAFQAVgAgAGcAYQBtAG0AYQAgADEALgA1ACAATQBvAG4AaQB0AG8AcgAAAAAAAEludGVlbnVtAAAAAEludGUAAAAAQ2xybQAAAABNcEJsYm9vbAEAAAAPcHJpbnRTaXh0ZWVuQml0Ym9vbAAAAAALcHJpbnRlck5hbWVURVhUAAAAEABUADAAMgAzAC0ATQB1AFYALQBLAFkANAA1ADUAMQAAAAAAD3ByaW50UHJvb2ZTZXR1cE9iamMAAAASAFAAcgBvAG8AZgAtAEUAaQBuAHMAdABlAGwAbAB1AG4AZwAAAAAACnByb29mU2V0dXAAAAABAAAAAEJsdG5lbnVtAAAADGJ1aWx0aW5Qcm9vZgAAAAlwcm9vZkNNWUsAOEJJTQQ7AAAAAAItAAAAEAAAAAEAAAAAABJwcmludE91dHB1dE9wdGlvbnMAAAAXAAAAAENwdG5ib29sAAAAAABDbGJyYm9vbAAAAAAAUmdzTWJvb2wAAAAAAENybkNib29sAAAAAABDbnRDYm9vbAAAAAAATGJsc2Jvb2wAAAAAAE5ndHZib29sAAAAAABFbWxEYm9vbAAAAAAASW50cmJvb2wAAAAAAEJja2dPYmpjAAAAAQAAAAAAAFJHQkMAAAADAAAAAFJkICBkb3ViQG/gAAAAAAAAAAAAR3JuIGRvdWJAb+AAAAAAAAAAAABCbCAgZG91YkBv4AAAAAAAAAAAAEJyZFRVbnRGI1JsdAAAAAAAAAAAAAAAAEJsZCBVbnRGI1JsdAAAAAAAAAAAAAAAAFJzbHRVbnRGI1B4bEBywAAAAAAAAAAACnZlY3RvckRhdGFib29sAQAAAABQZ1BzZW51bQAAAABQZ1BzAAAAAFBnUEMAAAAATGVmdFVudEYjUmx0AAAAAAAAAAAAAAAAVG9wIFVudEYjUmx0AAAAAAAAAAAAAAAAU2NsIFVudEYjUHJjQFkAAAAAAAAAAAAQY3JvcFdoZW5QcmludGluZ2Jvb2wAAAAADmNyb3BSZWN0Qm90dG9tbG9uZwAAAAAAAAAMY3JvcFJlY3RMZWZ0bG9uZwAAAAAAAAANY3JvcFJlY3RSaWdodGxvbmcAAAAAAAAAC2Nyb3BSZWN0VG9wbG9uZwAAAAAAOEJJTQPtAAAAAAAQASwAAAABAAIBLAAAAAEAAjhCSU0EJgAAAAAADgAAAAAAAAAAAAA/gAAAOEJJTQQNAAAAAAAEAAAAeDhCSU0EGQAAAAAABAAAAB44QklNA/MAAAAAAAkAAAAAAAAAAAEAOEJJTScQAAAAAAAKAAEAAAAAAAAAAjhCSU0D9QAAAAAASAAvZmYAAQBsZmYABgAAAAAAAQAvZmYAAQChmZoABgAAAAAAAQAyAAAAAQBaAAAABgAAAAAAAQA1AAAAAQAtAAAABgAAAAAAAThCSU0D+AAAAAAAcAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAA4QklNBAAAAAAAAAIAADhCSU0EAgAAAAAAAgAAOEJJTQQwAAAAAAABAQA4QklNBC0AAAAAAAYAAQAAAAI4QklNBAgAAAAAABAAAAABAAACQAAAAkAAAAAAOEJJTQQeAAAAAAAEAAAAADhCSU0EGgAAAAADWQAAAAYAAAAAAAAAAAAABJ8AAA6tAAAAEgBMAG8AZwBvAF8AUwBfAFMAcABrACAALQAgAFIARwBCAC0AMQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAOrQAABJ8AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAQAAAAAAAG51bGwAAAACAAAABmJvdW5kc09iamMAAAABAAAAAAAAUmN0MQAAAAQAAAAAVG9wIGxvbmcAAAAAAAAAAExlZnRsb25nAAAAAAAAAABCdG9tbG9uZwAABJ8AAAAAUmdodGxvbmcAAA6tAAAABnNsaWNlc1ZsTHMAAAABT2Jq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 truncated
+1
View File
@@ -0,0 +1 @@
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 truncated
File renamed without changes.
File renamed without changes.
+102
View File
@@ -0,0 +1,102 @@
<html>
<head>
<meta charset="utf8">
<title>Sponsoring contract</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>
</tfoot>
</table>
</main>
<footer class="certificate-footer">
<p>
{{ $.Footer }}
</p>
</footer>
</article>
{{ end }}
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<html>
<head>
<meta charset="utf8">
<title>Sponsoring contract</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">
<p>
{{ $.Footer }}
</p>
</footer>
</article>
{{ end }}
</body>
</html>
@@ -27,7 +27,7 @@
<body class="A5 landscape"> <body class="A5 landscape">
{{ range .Runners }} {{ range .Runners }}
<div class="sheet"> <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 style=" padding: 0 1rem 0 1rem;">
<div class="columns"> <div class="columns">
<div class="column is-10"> <div class="column is-10">
@@ -27,7 +27,7 @@
<body class="A5 landscape"> <body class="A5 landscape">
{{ range .Runners }} {{ range .Runners }}
<div class="sheet"> <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 style=" padding: 0 1rem 0 1rem;">
<div class="columns"> <div class="columns">
<div class="column is-10"> <div class="column is-10">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

-41
View File
@@ -1,41 +0,0 @@
package templates
import (
_ "embed"
"fmt"
)
//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 images/sponsoringheader.base64
var ImageSponsoringHeaderBase64 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
}
}
return "", fmt.Errorf("template not found with name %s and locale %s", templateName, locale)
}