66 Commits

Author SHA1 Message Date
b58bf700df chore(static) Add base64 encoded image for new sponsors
All checks were successful
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
efd3a35802 fix(templates): Update titles for runner and certificate templates 2025-04-14 17:56:49 +02:00
0f7e44a42a fix(models): Correct typo in SponsoringReceiptMinimum mapstructure tag
All checks were successful
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
f90e5d75fa fix(contracts): Minimum was not read correctly
All checks were successful
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
31d4ec5f27 fix(templates): Correct spacing in group name display
All checks were successful
Build release images / build-container (push) Successful in 1m51s
Build latest image / build-container (push) Successful in 2m13s
2025-03-26 19:29:32 +01:00
d61d4d6e7e refactor(ci): Switch to actions
All checks were successful
Build latest image / build-container (push) Successful in 1m39s
2025-03-22 22:48:35 +01:00
606ce6b940 docs(swagger): Build new docs
Some checks failed
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline failed
2024-12-17 17:51:51 +01:00
750fa70332 feat(models): Support nested groups 2024-12-17 17:51:11 +01:00
7d503edbc9 feat(templates): Support nested groups 2024-12-17 17:50:54 +01:00
5c9235df8d fix(templates): Enable blank cards 2024-12-17 17:38:42 +01:00
11ea0858bb feat(services): Logging
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 16:23:52 +01:00
4d57cf827d refactor(handler): Move array manipulation
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 16:07:40 +01:00
df9f7fdc13 feat(handlers): Added info logging 2024-12-17 16:07:02 +01:00
cdd2b5e250 feat(logging): Debug logging 2024-12-17 15:48:12 +01:00
94b766f106 feat(logger): Log levels 2024-12-17 15:45:40 +01:00
a2e94f715b refactor(logs): Replaced main logger with zap
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:31:23 +01:00
f64daaf817 feat: Request-IDs for better debugging 2024-12-16 17:23:58 +01:00
b4bb732303 fix(config): typoed defaults
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 17:02:49 +01:00
3dee3e72af fix: Bad dependency
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:41:52 +01:00
f5914e5c38 style: Formatting
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-16 16:39:56 +01:00
5a5a7179e9 feat: Use CORS
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline failed
2024-12-16 16:39:02 +01:00
6c57d63891 fix(config): Added SPONSORING_DISCLAIMER default
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-16 16:31:08 +01:00
b502e2fbd5 fix(config): Defaults for everyone
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:30:02 +01:00
c9475d0093 feat(config): log config on boot
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:17:26 +01:00
1cc19e0085 fix(config): Default apikey
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-16 16:15:11 +01:00
b792806481 docs(swagger): New barcode padding docs
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-12 19:17:29 +01:00
ea6a4a7080 refactor(barcode): Switch to inclusive padding 2024-12-12 19:16:55 +01:00
de6fe4991c feat(barcode): Padding 2024-12-12 19:11:01 +01:00
1d068b2655 docs(swagger): New swaggerdoc
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-12 18:50:21 +01:00
ef25adf5ed refactor(models): Group now only has a name
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:48:39 +01:00
c09c00ec68 feat(pdfs): Set download names for pdf generation responses
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:24:07 +01:00
1f4981b0a9 fix(config): Don't fail on missing env
Some checks failed
ci/woodpecker/push/build Pipeline failed
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:18:41 +01:00
c9f28612be docs(README): Updated benchmark section
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-12 18:05:52 +01:00
c2d192d8a3 docs(README): Added performance results 2024-12-12 17:59:33 +01:00
31734596f6 feat(barcode): Implement cache 2024-12-12 17:59:14 +01:00
850fa0a760 feat(barcode): Baseline for barcode caching 2024-12-12 17:44:41 +01:00
1c0a9860fa feat(container): Provide default redis 2024-12-12 17:44:26 +01:00
7c32f1aad2 perf(templates): Cache templates in map 2024-12-12 17:34:55 +01:00
b9e550d6f5 refactor(services): Move staticservice to struct and interface 2024-12-12 17:11:59 +01:00
2a4f126377 feat(barcodes): Make width and height configureable 2024-12-12 17:03:10 +01:00
5c932158e9 refactor(handlers): Use shared gotenberg 2024-12-12 16:53:51 +01:00
1296c9e399 refactor(pdf): Share templater 2024-12-12 16:52:06 +01:00
0f2452eca0 feat(handlers): Implement barcode generation 2024-12-12 16:49:33 +01:00
b6cc98a165 refactor(services): Extract barcode generation 2024-12-12 16:43:14 +01:00
c5da33f10f chore: Formatting 2024-12-12 16:33:23 +01:00
69f4de6739 docs(swagger): Move security annotations 2024-12-12 16:33:10 +01:00
649ac2a3c2 docs(swagger): Added barcode generation docs 2024-12-12 16:31:07 +01:00
5587fdaaa8 feat(barcodes): Baseline for implementation 2024-12-12 16:30:46 +01:00
8f676f08a9 refactor: Switch to pdf subpath 2024-12-12 16:28:06 +01:00
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
d19029b5ad docs(swagger): Updated swagger metadata 2024-12-11 19:45:05 +01:00
1dfd96869d docs(swagger): Swagger auth 2024-12-11 19:39:18 +01:00
a1ba28cacb docs(dev): Updated air commands 2024-12-11 19:33:06 +01:00
d2bace87af fix(dev): Add air config for linux/macos 2024-12-11 19:31:48 +01:00
9d507b9572 feat(ci): Added ci config 2024-12-11 19:30:02 +01:00
e89c17806f docs: Updated readme 2024-12-11 19:29:04 +01:00
924f76a100 refactor(images): Move sponsor images to folder and load them dynamicly 2024-12-11 19:22:50 +01:00
99ec0933ea feat(container): New dev compose with just external services 2024-12-11 19:21:32 +01:00
cceca7f5e1 feat(container): Deliver default static stuff 2024-12-11 18:51:37 +01:00
54d294a8b4 feat(container): Added document-server 2024-12-11 18:51:06 +01:00
41291b9200 refactor(config): Load gotenberg url from env 2024-12-11 18:45:53 +01:00
4faf76a073 feat: Config from env 2024-12-11 18:30:46 +01:00
715eb8e1cb chore(models): Removed unused param 2024-12-11 18:07:52 +01:00
2686bee1d1 refactor(auth): Switch to query auth for full link support (to be fixed in future) 2024-12-11 17:59:11 +01:00
57a3777891 fix(templates): Fix string style 2024-12-11 17:56:24 +01:00
f6dc33edb4 feat(v1): header auth for all endpoints 2024-12-11 17:55:29 +01:00
40 changed files with 1221 additions and 191 deletions

46
.air.linux.toml Normal file
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,3 +1,3 @@
tmp tmp
docker-compose.yaml docker-compose.yaml
.air.toml .air*.toml

19
.env Normal file
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
.gitea/workflows/dev.yaml Normal file
View File

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

View File

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

View File

@@ -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" ]

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
docker-compose.dev.yaml Normal file
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"

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
docker.env Normal file
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!

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,13 @@ const docTemplate = `{
"responses": {} "responses": {}
} }
}, },
"/certificates": { "/v1/pdfs/certificates": {
"post": { "post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate certificates based on the provided data", "description": "Generate certificates based on the provided data",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -52,7 +131,7 @@ const docTemplate = `{
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"certificates" "pdfs"
], ],
"summary": "Generate runner certificates", "summary": "Generate runner certificates",
"parameters": [ "parameters": [
@@ -69,8 +148,13 @@ const docTemplate = `{
"responses": {} "responses": {}
} }
}, },
"/contracts": { "/v1/pdfs/contracts": {
"post": { "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"
@@ -79,7 +163,7 @@ const docTemplate = `{
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"contracts" "pdfs"
], ],
"summary": "Generate a contract", "summary": "Generate a contract",
"parameters": [ "parameters": [
@@ -237,18 +321,22 @@ const docTemplate = `{
"models.Group": { "models.Group": {
"type": "object", "type": "object",
"required": [ "required": [
"id",
"name" "name"
], ],
"properties": { "properties": {
"id": {
"type": "integer"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
"parent_group": { "parent_group": {
"$ref": "#/definitions/models.Group" "type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
} }
} }
}, },
@@ -320,6 +408,13 @@ const docTemplate = `{
} }
} }
} }
},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "key",
"in": "query"
}
} }
}` }`

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,13 @@
"responses": {} "responses": {}
} }
}, },
"/certificates": { "/v1/pdfs/certificates": {
"post": { "post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Generate certificates based on the provided data", "description": "Generate certificates based on the provided data",
"consumes": [ "consumes": [
"application/json" "application/json"
@@ -43,7 +122,7 @@
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"certificates" "pdfs"
], ],
"summary": "Generate runner certificates", "summary": "Generate runner certificates",
"parameters": [ "parameters": [
@@ -60,8 +139,13 @@
"responses": {} "responses": {}
} }
}, },
"/contracts": { "/v1/pdfs/contracts": {
"post": { "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"
@@ -70,7 +154,7 @@
"application/pdf" "application/pdf"
], ],
"tags": [ "tags": [
"contracts" "pdfs"
], ],
"summary": "Generate a contract", "summary": "Generate a contract",
"parameters": [ "parameters": [
@@ -228,18 +312,22 @@
"models.Group": { "models.Group": {
"type": "object", "type": "object",
"required": [ "required": [
"id",
"name" "name"
], ],
"properties": { "properties": {
"id": {
"type": "integer"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
"parent_group": { "parent_group": {
"$ref": "#/definitions/models.Group" "type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
}
} }
} }
}, },
@@ -311,5 +399,12 @@
} }
} }
} }
},
"securityDefinitions": {
"ApiKeyAuth": {
"type": "apiKey",
"name": "key",
"in": "query"
}
} }
} }

View File

@@ -94,14 +94,16 @@ definitions:
type: object type: object
models.Group: models.Group:
properties: properties:
id:
type: integer
name: name:
type: string type: string
parent_group: parent_group:
$ref: '#/definitions/models.Group' properties:
name:
type: string
required:
- name
type: object
required: required:
- id
- name - name
type: object type: object
models.Runner: models.Runner:
@@ -152,12 +154,63 @@ definitions:
- last_name - 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
@@ -172,10 +225,12 @@ paths:
produces: produces:
- application/pdf - application/pdf
responses: {} responses: {}
security:
- ApiKeyAuth: []
summary: Generate runner cards summary: Generate runner cards
tags: tags:
- cards - pdfs
/certificates: /v1/pdfs/certificates:
post: post:
consumes: consumes:
- application/json - application/json
@@ -190,10 +245,12 @@ paths:
produces: produces:
- application/pdf - application/pdf
responses: {} responses: {}
security:
- ApiKeyAuth: []
summary: Generate runner certificates summary: Generate runner certificates
tags: tags:
- certificates - pdfs
/contracts: /v1/pdfs/contracts:
post: post:
consumes: consumes:
- application/json - application/json
@@ -208,7 +265,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"

24
go.mod
View File

@@ -14,26 +14,48 @@ 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.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/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
) )

57
go.sum
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,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/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=
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 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 +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.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=

73
handlers/barcode.go Normal file
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())
}

View File

@@ -1,45 +1,52 @@
package handlers package handlers
import ( import (
"log"
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"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 {
logger := h.Logger.Named("GenerateCard")
cardRequest := new(models.CardRequest) cardRequest := new(models.CardRequest)
if err := c.BodyParser(cardRequest); err != nil { if err := c.BodyParser(cardRequest); err != nil {
logger.Errorw("Invalid request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) { if !slices.Contains([]string{"en", "de"}, cardRequest.Locale) {
logger.Errorw("Invalid locale", "locale", cardRequest.Locale)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale", "error": "Invalid locale",
}) })
} }
generator := services.DefaultTemplater{} logger = logger.With("locale", cardRequest.Locale)
templateString, err := services.GetTemplate(cardRequest.Locale, "card")
templateString, err := h.StaticService.GetTemplate(cardRequest.Locale, "card")
if err != nil { if err != nil {
log.Println(err) logger.Errorw("Template not found", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := generator.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
logger.Errorw("Error parsing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
@@ -47,28 +54,35 @@ 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) logger.Info("Generating card html")
result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
logger.Errorw("Error executing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Generated card html")
c.Set(fiber.HeaderContentType, "text/html") c.Set(fiber.HeaderContentType, "text/html")
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 { if err != nil {
logger.Errorw("Error converting html to pdf", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Converted html to pdf")
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-cards.pdf")
return c.Send(pdf) return c.Send(pdf)
} }

View File

@@ -1,45 +1,52 @@
package handlers package handlers
import ( import (
"log"
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// GenerateCertificate godoc // GenerateCertificate godoc
// @Summary Generate runner certificates //
// @Description Generate certificates based on the provided data // @Summary Generate runner certificates
// @Tags certificates // @Description Generate certificates based on the provided data
// @Accept json // @Tags pdfs
// @Param data body models.CertificateRequest true "Certificate data" // @Accept json
// @Produce application/pdf // @Param data body models.CertificateRequest true "Certificate data"
// @Router /certificates [post] // @Produce application/pdf
func GenerateCertificate(c *fiber.Ctx) error { // @Security ApiKeyAuth
// @Router /v1/pdfs/certificates [post]
func (h *DefaultHandler) GenerateCertificate(c *fiber.Ctx) error {
logger := h.Logger.Named("GenerateCertificate")
certificateRequest := new(models.CertificateRequest) certificateRequest := new(models.CertificateRequest)
if err := c.BodyParser(certificateRequest); err != nil { if err := c.BodyParser(certificateRequest); err != nil {
logger.Errorw("Invalid request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) { if !slices.Contains([]string{"en", "de"}, certificateRequest.Locale) {
logger.Errorw("Invalid locale", "locale", certificateRequest.Locale)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale", "error": "Invalid locale",
}) })
} }
generator := services.DefaultTemplater{} logger = logger.With("locale", certificateRequest.Locale)
templateString, err := services.GetTemplate(certificateRequest.Locale, "certificate")
templateString, err := h.StaticService.GetTemplate(certificateRequest.Locale, "certificate")
if err != nil { if err != nil {
log.Println(err) logger.Errorw("Template not found", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := generator.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
logger.Errorw("Error parsing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
@@ -47,28 +54,33 @@ func GenerateCertificate(c *fiber.Ctx) error {
genConfig := &models.CertificateTemplateOptions{ genConfig := &models.CertificateTemplateOptions{
Runners: addUpRunnerDonations(certificateRequest.Runners), Runners: addUpRunnerDonations(certificateRequest.Runners),
EventName: "Event name", EventName: h.Config.EventName,
Footer: "Footer", Footer: h.Config.CertificateFooter,
CurrencySymbol: "€", CurrencySymbol: h.Config.CurrencySymbol,
Locale: certificateRequest.Locale, Locale: certificateRequest.Locale,
} }
result, err := generator.Execute(template, genConfig) logger.Info("Generating certificate html")
result, err := h.Templater.Execute(template, genConfig)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Generated card html")
c.Set(fiber.HeaderContentType, "text/html") c.Set(fiber.HeaderContentType, "text/html")
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 { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Converted html to pdf")
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=certificate.pdf")
return c.Send(pdf) return c.Send(pdf)
} }

View File

@@ -1,78 +1,87 @@
package handlers package handlers
import ( import (
"log"
"slices" "slices"
"git.odit.services/lfk/document-server/models" "git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"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 {
logger := h.Logger.Named("GenerateContract")
contract := new(models.ContractRequest) contract := new(models.ContractRequest)
if err := c.BodyParser(contract); err != nil { if err := c.BodyParser(contract); err != nil {
logger.Errorw("Invalid request", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
if !slices.Contains([]string{"en", "de"}, contract.Locale) { if !slices.Contains([]string{"en", "de"}, contract.Locale) {
logger.Errorw("Invalid locale", "locale", contract.Locale)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid locale", "error": "Invalid locale",
}) })
} }
contract.Runners = repeatRunnerArrayItems(contract.Runners, 2) logger = logger.With("locale", contract.Locale)
generator := services.DefaultTemplater{} templateString, err := h.StaticService.GetTemplate(contract.Locale, "contract")
templateString, err := services.GetTemplate(contract.Locale, "contract")
if err != nil { if err != nil {
log.Println(err) logger.Errorw("Template not found", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Template not found", "error": "Template not found",
}) })
} }
template, err := generator.StringToTemplate(templateString) template, err := h.Templater.StringToTemplate(templateString)
if err != nil { if err != nil {
logger.Errorw("Error parsing template", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
genConfig := &models.ContractTemplateOptions{ genConfig := &models.ContractTemplateOptions{
Runners: contract.Runners, Runners: repeatRunnerArrayItems(contract.Runners, 2),
CurrencySymbol: "€", 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,
BarcodeFormat: "ean13", BarcodeFormat: h.Config.SponsoringBarcodeFormat,
BarcodePrefix: "1", 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 { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Generated contract html")
converter := services.GotenbergConverter{BaseUrl: "http://localhost:3001"} logger.Info("Converting html to pdf")
pdf, err := converter.ToPdf(result, "a5", true) pdf, err := h.Converter.ToPdf(result, "a5", true)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(), "error": err.Error(),
}) })
} }
logger.Info("Converted html to pdf")
c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentType, "application/pdf")
c.Set(fiber.HeaderContentDisposition, "attachment; filename=runner-contracts.pdf")
return c.Send(pdf) return c.Send(pdf)
} }

24
handlers/handlers.go Normal file
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
}

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")
} }

183
main.go
View File

@@ -1,45 +1,194 @@
package main package main
import ( import (
"flag" "crypto/sha256"
"log" "crypto/subtle"
"os"
"strings"
"git.odit.services/lfk/document-server/docs" // Correct import path for docs "git.odit.services/lfk/document-server/docs" // Correct import path for docs
"git.odit.services/lfk/document-server/handlers" "git.odit.services/lfk/document-server/handlers"
"git.odit.services/lfk/document-server/models"
"git.odit.services/lfk/document-server/services"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/keyauth"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/gofiber/swagger" "github.com/gofiber/swagger"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
) )
var ( var (
port = flag.String("port", ":3000", "Port to listen on") config *models.Config
prod = flag.Bool("prod", false, "Enable prefork in Production") logger *zap.SugaredLogger
) )
// @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("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() { 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 // Create a new Fiber instance
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Prefork: *prod, Prefork: config.Prod,
}) })
app.Use(cors.New())
app.Use(requestid.New())
// Swagger documentation route // Swagger documentation route
app.Get("/swagger/*", swagger.HandlerDefault) app.Get("/swagger/*", swagger.HandlerDefault)
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) }))
v1.Post("/certificates", handlers.GenerateCertificate)
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 = "/" 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))
} }

20
models/config.go Normal file
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"`
}

View File

@@ -14,18 +14,18 @@ type Runner struct {
} }
type Group struct { type Group struct {
ID int `json:"id" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
ParentGroup *Group `json:"parent_group" validate:"optional"` ParentGroup struct {
Name string `json:"name" validate:"required"`
} `json:"parent_group" validate:"optional"`
} }
type ContractTemplateOptions struct { type ContractTemplateOptions struct {
Runners []Runner `json:"runners"` Runners []Runner `json:"runners"`
CurrencySymbol string `json:"currency_symbol"` CurrencySymbol string `json:"currency_symbol"`
Disclaimer string `json:"disclaimer"` Disclaimer string `json:"disclaimer"`
ReceiptMinimumAmount int `json:"receipt_minimum_amount"` ReceiptMinimumAmount string `json:"receipt_minimum_amount"`
EventName string `json:"event_name"` EventName string `json:"event_name"`
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"`
} }

125
services/barcode.go Normal file
View File

@@ -0,0 +1,125 @@
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":
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)
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)
}

View File

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

View File

@@ -6,13 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"image/png"
"strings" "strings"
"github.com/boombuler/barcode" "go.uber.org/zap"
"github.com/boombuler/barcode/code128"
"github.com/boombuler/barcode/ean"
"github.com/boombuler/barcode/qr"
) )
type Templater interface { type Templater interface {
@@ -21,6 +17,9 @@ type Templater interface {
} }
type DefaultTemplater struct { type DefaultTemplater struct {
BarcodeService BarcodeService
StaticService StaticService
Logger *zap.SugaredLogger
} }
func idToEan13(id string, prefix string) (string, error) { func idToEan13(id string, prefix string) (string, error) {
@@ -37,61 +36,33 @@ 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
}
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) buf, err := t.BarcodeService.GenerateBarcode(format, code, 1000, 500, 0)
if err != nil {
return "", err
}
var buf bytes.Buffer return base64.StdEncoding.EncodeToString(buf.Bytes()), err
err = png.Encode(&buf, scaledCode)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
} }
func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) { func (t *DefaultTemplater) SelectSponsorImage(id int) (string, error) {
sponsors := []string{ logger := t.Logger.Named("SelectSponsorImage")
"vrbank", sponsors, err := t.StaticService.ListFilesInStaticSubFolder("images/sponsors")
"odit", if err != nil {
"sparkasse", logger.Errorw("Failed to list sponsors", "error", err)
return "", err
} }
return 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) { func (t *DefaultTemplater) LoadImage(name string) (string, error) {
return GetImage(name), nil return t.StaticService.GetImage(name), nil
} }
func (t *DefaultTemplater) FormatUnit(unit string, locale string, amount int) (string, error) { func (t *DefaultTemplater) FormatUnit(unit string, locale string, amount int) (string, error) {

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
<head> <head>
<meta charset="utf8"> <meta charset="utf8">
<title>Sponsoring contract</title> <title>Läuferkarten</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<style> <style>
.sheet { .sheet {
@@ -50,8 +50,12 @@
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p> <p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div> </div>
</div> </div>
{{ if ne .Runner.FirstName "" }}
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p> <p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
<p>{{ .Runner.Group.Name }}</p> <p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
{{ else }}
<p>Kein Läufer zugewiesen</p>
{{ end}}
{{ end}} {{ end}}
</div> </div>
{{ end }} {{ end }}

View File

@@ -2,7 +2,7 @@
<head> <head>
<meta charset="utf8"> <meta charset="utf8">
<title>Sponsoring contract</title> <title>Runner cards</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<style> <style>
.sheet { .sheet {
@@ -49,8 +49,12 @@
<p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p> <p style="font-size: 0.6rem; text-align: center; margin: 0; padding: 0;">{{ .Code }}</p>
</div> </div>
</div> </div>
{{ if ne .Runner.FirstName ""}}
<p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p> <p>{{ .Runner.LastName }}, {{ .Runner.FirstName }} {{ .Runner.MiddleName }}</p>
<p>{{ .Runner.Group.Name }}</p> <p>{{ if ne .Runner.Group.ParentGroup.Name "" -}}{{ .Runner.Group.ParentGroup.Name }}/{{end -}}{{ .Runner.Group.Name }}</p>
{{ else }}
<p>Blank card</p>
{{ end}}
</div> </div>
{{ end }} {{ end }}
</div> </div>

View File

@@ -2,7 +2,7 @@
<head> <head>
<meta charset="utf8"> <meta charset="utf8">
<title>Sponsoring contract</title> <title>Läuferurkunde</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<style> <style>
.sheet { .sheet {
@@ -12,7 +12,7 @@
box-sizing: border-box; box-sizing: border-box;
page-break-after: always; page-break-after: always;
padding: 1.2cm 2cm 1.2cm 2cm; padding: 1.2cm 2cm 1.2cm 2cm;
background-image: url("data:image/png;base64,{{ loadImage 'certificate_background' }}"); background-image: url("data:image/png;base64,{{ loadImage "certificate_background" }}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 11cm; background-size: 11cm;
background-position: 5cm 5cm; background-position: 5cm 5cm;

View File

@@ -2,7 +2,7 @@
<head> <head>
<meta charset="utf8"> <meta charset="utf8">
<title>Sponsoring contract</title> <title>Runner certificate</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<style> <style>
.sheet { .sheet {
@@ -12,7 +12,7 @@
box-sizing: border-box; box-sizing: border-box;
page-break-after: always; page-break-after: always;
padding: 1.2cm 2cm 1.2cm 2cm; padding: 1.2cm 2cm 1.2cm 2cm;
background-image: url("data:image/png;base64,{{ loadImage 'certificate_background' }}"); background-image: url("data:image/png;base64,{{ loadImage "certificate_background" }}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 11cm; background-size: 11cm;
background-position: 5cm 5cm; background-position: 5cm 5cm;

View File

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

View File

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